[PATCH damus-api] Express.js refactor, API improvements, and supertest integration

4 views
Skip to first unread message

Daniel D’Aquino

unread,
Dec 27, 2023, 7:06:16 PM12/27/23
to pat...@damus.io, Daniel D’Aquino
This commit brings the following changes:
- Refactor server to use express.js (instead of a custom router)
- Refactor NIP-98 authentication to become an express.js middleware
- Use supertest for testing (Improves test readability, realism, and makes it possible to test express.js)
- Improve translate endpoint to use NIP-98 for authentication
- Add NIP-98 auth support for GET requests
- Improve account creation to grab the pubkey directly from the auth header, thus removing the need for a custom payload

Testing
--------

All unit tests are passing.

Closes: https://github.com/damus-io/api/issues/1
Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
Hi Will,

These are the changes done to migrate the server from a custom router
into express.js, and to use supertest.

I have decided to tackle this as early as possible while the codebase is
still small, and refactoring is manageable.

I had to make some changes to the client-side code to make it work with
these changes. I will be sending a separate patch for that.

Please let me know if you have any questions or concerns.

Best regards,
Daniel

package-lock.json | 778 +++++++++++++++++++++++++++++++++-
package.json | 3 +
src/index.js | 19 +-
src/nip98_auth.js | 192 +++++----
src/router_config.js | 164 +++----
src/server_helpers.js | 175 +-------
src/translate.js | 224 +++++-----
src/user_management.js | 16 +-
test/helpers.test.js | 82 ----
test/protected_routes.test.js | 698 +++++++++++++-----------------
test/router_config.test.js | 107 ++---
test/translate.test.js | 272 ++++++------
test/utils.js | 26 ++
13 files changed, 1605 insertions(+), 1151 deletions(-)
delete mode 100644 test/helpers.test.js
create mode 100644 test/utils.js

diff --git a/package-lock.json b/package-lock.json
index e0b92d3..e3472d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,13 +9,16 @@
"version": "0.1.0",
"dependencies": {
"@apple/app-store-server-library": "^0.2.0",
+ "body-parser": "^1.20.2",
"dotenv": "^16.3.1",
+ "express": "^4.18.2",
"lmdb": "^2.9.1",
"nostr": "^0.2.8"
},
"devDependencies": {
"@tapjs/sinon": "^1.1.17",
"sinon": "^17.0.1",
+ "supertest": "^6.3.3",
"tap": "^18.6.1"
}
},
@@ -1219,6 +1222,18 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/acorn": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
@@ -1344,6 +1359,17 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true
+ },
"node_modules/async-hook-domain": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-4.0.1.tgz",
@@ -1393,6 +1419,53 @@
"node": ">=8"
}
},
+ "node_modules/body-parser": {
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+ "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -1428,6 +1501,14 @@
"semver": "^7.0.0"
}
},
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/c8": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz",
@@ -1553,6 +1634,19 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+ "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
@@ -1824,12 +1918,40 @@
"node": ">= 0.8"
}
},
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1845,6 +1967,25 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
+ "dev": true
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1882,6 +2023,19 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
+ "node_modules/define-data-property": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+ "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1890,6 +2044,23 @@
"node": ">=0.4.0"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
@@ -1898,6 +2069,16 @@
"node": ">=8"
}
},
+ "node_modules/dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "dev": true,
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
"node_modules/diff": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
@@ -1932,12 +2113,25 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
@@ -1971,6 +2165,11 @@
"node": ">=6"
}
},
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
"node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@@ -1980,6 +2179,14 @@
"node": ">=8"
}
},
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/events-to-array": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-2.0.3.tgz",
@@ -1995,6 +2202,119 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==",
"dev": true
},
+ "node_modules/express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/express/node_modules/body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/express/node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
+ },
+ "node_modules/express/node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+ "dev": true
+ },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -2007,6 +2327,36 @@
"node": ">=8"
}
},
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2052,6 +2402,37 @@
"node": ">= 6"
}
},
+ "node_modules/formidable": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
+ "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
+ "dev": true,
+ "dependencies": {
+ "dezalgo": "^1.0.4",
+ "hexoid": "^1.0.0",
+ "once": "^1.4.0",
+ "qs": "^6.11.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/fromentries": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz",
@@ -2108,7 +2489,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2128,6 +2508,20 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+ "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/glob": {
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
@@ -2162,6 +2556,17 @@
"node": ">= 6"
}
},
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2177,11 +2582,43 @@
"node": ">=8"
}
},
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+ "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -2189,6 +2626,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/hexoid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
+ "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/hosted-git-info": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
@@ -2213,6 +2659,21 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
@@ -2297,8 +2758,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ink": {
"version": "4.4.1",
@@ -2361,6 +2821,14 @@
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
"dev": true
},
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/is-actual-promise": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-actual-promise/-/is-actual-promise-1.0.1.tgz",
@@ -2772,6 +3240,38 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -3036,7 +3536,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -3313,6 +3812,25 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3428,6 +3946,14 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/patch-console": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
@@ -3588,6 +4114,65 @@
"node": ">=10"
}
},
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -3787,8 +4372,7 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "optional": true
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/scheduler": {
"version": "0.23.0",
@@ -3824,6 +4408,75 @@
"node": ">=10"
}
},
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
+ "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+ "dependencies": {
+ "define-data-property": "^1.1.1",
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3845,6 +4498,19 @@
"node": ">=8"
}
},
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -4012,6 +4678,14 @@
"node": ">=10"
}
},
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/string-length": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz",
@@ -4123,6 +4797,52 @@
"node": ">=8"
}
},
+ "node_modules/superagent": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz",
+ "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==",
+ "dev": true,
+ "dependencies": {
+ "component-emitter": "^1.3.0",
+ "cookiejar": "^2.1.4",
+ "debug": "^4.3.4",
+ "fast-safe-stringify": "^2.1.1",
+ "form-data": "^4.0.0",
+ "formidable": "^2.1.2",
+ "methods": "^1.1.2",
+ "mime": "2.6.0",
+ "qs": "^6.11.0",
+ "semver": "^7.3.8"
+ },
+ "engines": {
+ "node": ">=6.4.0 <13 || >=14"
+ }
+ },
+ "node_modules/superagent/node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/supertest": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz",
+ "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==",
+ "dev": true,
+ "dependencies": {
+ "methods": "^1.1.2",
+ "superagent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">=6.4.0"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4364,6 +5084,14 @@
"node": ">=8.0"
}
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -4442,6 +5170,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
@@ -4484,6 +5224,22 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -4545,6 +5301,14 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/walk-up-path": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz",
diff --git a/package.json b/package.json
index 91b23ca..555e95a 100644
--- a/package.json
+++ b/package.json
@@ -15,13 +15,16 @@
},
"dependencies": {
"@apple/app-store-server-library": "^0.2.0",
+ "body-parser": "^1.20.2",
"dotenv": "^16.3.1",
+ "express": "^4.18.2",
"lmdb": "^2.9.1",
"nostr": "^0.2.8"
},
"devDependencies": {
"@tapjs/sinon": "^1.1.17",
"sinon": "^17.0.1",
+ "supertest": "^6.3.3",
"tap": "^18.6.1"
}
}
diff --git a/src/index.js b/src/index.js
index 4598e3e..078a99d 100755
--- a/src/index.js
+++ b/src/index.js
@@ -5,6 +5,7 @@ const http = require('http')
const Router = require('./server_helpers').Router
const config_router = require('./router_config').config_router
const dotenv = require('dotenv')
+const express = require('express')

function PurpleApi(opts = {})
{
@@ -12,11 +13,11 @@ function PurpleApi(opts = {})
return new PurpleApi(opts)

const queue = {}
- const db = lmdb.open({ path: '.', })
+ const db = lmdb.open({ path: '.' })
const translations = db.openDB('translations')
const accounts = db.openDB('accounts')
const dbs = {translations, accounts}
- const router = new Router(process.env.BASE_URL || 'http://localhost:8989')
+ const router = express()

// translation data
this.translation = {queue}
@@ -24,7 +25,6 @@ function PurpleApi(opts = {})
this.db = db
this.dbs = dbs
this.opts = opts
- this.server = http.createServer(this.handler.bind(this.server, this))
this.router = router

return this
@@ -32,14 +32,11 @@ function PurpleApi(opts = {})

PurpleApi.prototype.serve = async function pt_serve(port) {
const theport = this.opts.port || port || 8989
- await this.server.listen(theport)
- console.log(`http server listening on ${theport}`)
+ this.router.listen(theport)
+ console.log(`Server listening on ${theport}`)
}

PurpleApi.prototype.close = async function pt_close() {
- // stop the server
- await this.server.close()
-
// close lmdb
await this.db.close()
}
@@ -48,12 +45,6 @@ PurpleApi.prototype.register_routes = function pt_register_routes() {
config_router(this)
}

-PurpleApi.prototype.handler = function pt_handle_request(app, req, res)
-{
- app.router.handle_request(req, res)
-}
-
-
module.exports = PurpleApi

if (require.main == module) {
diff --git a/src/nip98_auth.js b/src/nip98_auth.js
index 76b6e8a..9ae0554 100644
--- a/src/nip98_auth.js
+++ b/src/nip98_auth.js
@@ -1,11 +1,13 @@
const nostr = require('nostr');
const { hash_sha256, current_time } = require('./utils');
+const { unauthorized_response } = require('./server_helpers');
+const bodyParser = require('body-parser');

// Note: nostr-tools contains NIP-98 related functions, but they do not check the payload hash and do not return the authorized pubkey, so they are not secure enough.
// TODO: Integrate this into a library such as `nostr`


-// nip98_auth
+// nip98_verify_auth_header
//
// Validate the authorization header of a request according to NIP-98
//
@@ -14,77 +16,123 @@ const { hash_sha256, current_time } = require('./utils');
// method: The method of the request
// body: The body of the request
// returns: the pubkey (hex) of the authorized user or null if not authorized
-async function nip98_auth(auth_header, url, method, body) {
- try {
- if(!auth_header) {
- return null;
- }
-
- auth_header_parts = auth_header.split(' ');
- if(auth_header_parts.length != 2) {
- return null;
- }
-
- if(auth_header_parts[0] != 'Nostr') {
- return null;
- }
-
- // Get base64 encoded note
- const base64_encoded_note = auth_header.split(' ')[1];
- if(!base64_encoded_note) {
- return null;
- }
-
- let note = JSON.parse(Buffer.from(base64_encoded_note, 'base64').toString('utf-8'));
- if(!note) {
- return null;
- }
-
- if(note.kind != 27235) {
- return null;
- }
-
- let authorized_url = note.tags.find(tag => tag[0] == 'u')[1];
- let authorized_method = note.tags.find(tag => tag[0] == 'method')[1];
- if(authorized_url != url || authorized_method != method) {
- return null;
- }
-
- if(current_time() - note.created_at > 60 || current_time() - note.created_at < 0) {
- return null;
- }
-
- if(body !== undefined && body !== null) {
- let authorized_content_hash = note.tags.find(tag => tag[0] == 'payload')[1];
-
- let body_hash = hash_sha256(body);
- if(authorized_content_hash != body_hash) {
- return null;
- }
- }
- else {
- // If there is no body, there should be NO payload tag
- if(note.tags.find(tag => tag[0] == 'payload')) {
- return null;
- }
- }
-
- // Verify that the ID corresponds to the note contents
- if(note.id != await nostr.calculateId(note)) {
- return null;
- }
-
- // Verify the ID was signed by the alleged pubkey
- let signature_valid = await nostr.verifyEvent(note);
- if(!signature_valid) {
- return null;
- }
-
- return note.pubkey;
- } catch (error) {
- console.log(error);
- return null;
+async function nip98_verify_auth_header(auth_header, url, method, body) {
+ try {
+ if (!auth_header) {
+ return { authorized_pubkey: null, error: 'Nostr authorization header missing' };
}
+
+ auth_header_parts = auth_header.split(' ');
+ if (auth_header_parts.length != 2) {
+ return { authorized_pubkey: null, error: 'Nostr authorization header does not have 2 parts' };
+ }
+
+ if (auth_header_parts[0] != 'Nostr') {
+ return { authorized_pubkey: null, error: 'Nostr authorization header does not start with `Nostr`' };
+ }
+
+ // Get base64 encoded note
+ const base64_encoded_note = auth_header.split(' ')[1];
+ if (!base64_encoded_note) {
+ return { authorized_pubkey: null, error: 'Nostr authorization header does not have a base64 encoded note' };
+ }
+
+ let note = JSON.parse(Buffer.from(base64_encoded_note, 'base64').toString('utf-8'));
+ if (!note) {
+ return { authorized_pubkey: null, error: 'Could not parse base64 encoded JSON note' };
+ }
+
+ if (note.kind != 27235) {
+ return { authorized_pubkey: null, error: 'Auth note kind is not 27235' };
+ }
+
+ let authorized_url = note.tags.find(tag => tag[0] == 'u')[1];
+ let authorized_method = note.tags.find(tag => tag[0] == 'method')[1];
+ if (authorized_url != url || authorized_method != method) {
+ return { authorized_pubkey: null, error: 'Auth note url and/or method does not match request. Auth note url: ' + authorized_url + '; Request url: ' + url + '; Auth note method: ' + authorized_method + '; Request method: ' + method };
+ }
+
+ if (current_time() - note.created_at > 60 || current_time() - note.created_at < 0) {
+ return { authorized_pubkey: null, error: 'Auth note is too old or too new' };
+ }
+
+ if (body !== undefined && body !== null) {
+ let authorized_content_hash = note.tags.find(tag => tag[0] == 'payload')[1];
+
+ let body_hash = hash_sha256(body);
+ if (authorized_content_hash != body_hash) {
+ return { authorized_pubkey: null, error: 'Auth note payload hash does not match request body hash' };
+ }
+ }
+ else {
+ // If there is no body, there should be NO payload tag
+ if (note.tags.find(tag => tag[0] == 'payload')) {
+ return { authorized_pubkey: null, error: 'Auth note has payload tag but request has no body' };
+ }
+ }
+
+ // Verify that the ID corresponds to the note contents
+ if (note.id != await nostr.calculateId(note)) {
+ return { authorized_pubkey: null, error: 'Auth note id does not match note contents' };
+ }
+
+ // Verify the ID was signed by the alleged pubkey
+ let signature_valid = await nostr.verifyEvent(note);
+ if (!signature_valid) {
+ return { authorized_pubkey: null, error: 'Auth note signature is invalid' };
+ }
+
+ return { authorized_pubkey: note.pubkey, error: null };
+ } catch (error) {
+ return { authorized_pubkey: null, error: "Error when checking auth header: " + error.message };
+ }
+}
+
+// capture_raw_body
+//
+// A middleware to be used as a verify function for the express.js body parser
+// This middleware will capture the raw body of the request and expose it as `req.raw_body`
+function capture_raw_body(req, res, buf, encoding) {
+ req.raw_body = buf;
}

-module.exports = nip98_auth;
+// Custom express.js authentication middleware
+// This middleware will verify the authorization header according to NIP-98
+// and attach the authorized pubkey to the request object
+//
+// Please make sure to use another middleware to capture the raw body of the request and exponse it as `req.raw_body`
+async function optional_nip98_auth(req, res, next) {
+ const auth_header = req.headers.authorization;
+ const full_url = req.protocol + '://' + req.get('Host') + req.originalUrl;
+
+ if ((req.raw_body === undefined || req.raw_body === null) && (req.body !== undefined && req.body !== null && Object.keys(req.body).length > 0)) {
+ throw new Error('raw_body is not defined in request object. Please make sure to use some middleware to capture the raw body and expose it to req.raw_body');
+ }
+
+ const { authorized_pubkey, error } = await nip98_verify_auth_header(auth_header, full_url, req.method, req.raw_body);
+
+ // Attach the public key to the request object
+ req.authorized_pubkey = authorized_pubkey;
+ req.auth_error = error;
+
+ // Proceed to the route handler
+ next();
+}
+
+// require_nip98_auth
+//
+// A simple middleware that rejects the request if there is no authorized_pubkey attached to the request object
+// This is useful for routes that require authentication. Use this middleware alongside nip98_verify
+async function required_nip98_auth(req, res, next) {
+ await optional_nip98_auth(req, res, () => {
+ if (!req.authorized_pubkey) {
+ return unauthorized_response(res, req.auth_error);
+ }
+ next();
+ });
+}
+
+module.exports = {
+ nip98_verify_auth_header, optional_nip98_auth, required_nip98_auth, capture_raw_body
+};
+
diff --git a/src/router_config.js b/src/router_config.js
index d0de287..ad0be5e 100644
--- a/src/router_config.js
+++ b/src/router_config.js
@@ -1,82 +1,98 @@
const { json_response, simple_response, error_response, invalid_request, unauthorized_response } = require('./server_helpers')
-const { create_account, get_account_info_payload } = require('./user_management')
+const { create_account, get_account_info_payload, check_account } = require('./user_management')
const handle_translate = require('./translate')
const verify_receipt = require('./app_store_receipt_verifier').verify_receipt
-
+const bodyParser = require('body-parser')
+const { required_nip98_auth, capture_raw_body } = require('./nip98_auth')

function config_router(app) {
- const router = app.router
-
- // MARK: Translation routes
-
- router.get('/translate', (req, res, capture_groups) => {
- handle_translate(app, req, res)
- })
-
- // MARK: Account management routes
-
- router.get('/accounts/(.+)', (req, res, capture_groups) => {
- const id = capture_groups[0]
- if(!id) {
- error_response(res, 'Could not parse account id')
- return
- }
- let account = app.dbs.accounts.get(id)
-
- if (!account) {
- simple_response(res, 404)
- return
- }
-
- let account_info = get_account_info_payload(account)
-
- json_response(res, account_info)
- })
-
- router.post_authenticated('/accounts', (req, res, capture_groups, auth_pubkey) => {
- let result = create_account(app, auth_pubkey, null)
-
- if (result.request_error) {
- invalid_request(res, result.request_error)
- return
- }
-
- json_response(res, get_account_info_payload(result.account))
- return
- })
-
- router.post_authenticated('/accounts/(.+)/app-store-receipt', async (req, res, capture_groups, auth_pubkey) => {
- const id = capture_groups[0]
- if(!id) {
- error_response(res, 'Could not parse account id')
- return
- }
- if(id != auth_pubkey) {
- unauthorized_response(res, 'You are not authorized to access this account')
- return
- }
-
- let account = app.dbs.accounts.get(id)
-
- if (!account) {
- simple_response(res, 404)
- return
- }
-
- const body = Buffer.from(req.body, 'base64').toString('ascii')
-
- let expiry_date = await verify_receipt(body)
-
- if (!expiry_date) {
- error_response(res, 'Could not verify receipt')
- return
- }
-
- account.expiry = expiry_date
- app.dbs.accounts.put(id, account)
- json_response(res, get_account_info_payload(account))
- return
- })
+ const router = app.router
+
+ router.use(bodyParser.json({ verify: capture_raw_body, type: 'application/json' }))
+ router.use(bodyParser.raw({ verify: capture_raw_body, type: 'application/octet-stream' }))
+
+ router.use((req, res, next) => {
+ res.on('finish', () => {
+ console.log(`[ ${req.method} ] ${req.url}: ${res.statusCode}`)
+ });
+ next()
+ })
+
+ // MARK: Translation routes
+
+ router.get('/translate', required_nip98_auth, async (req, res) => {
+ const check_account_result = check_account(app, req.authorized_pubkey)
+ if (!check_account_result.ok) {
+ unauthorized_response(res, check_account_result.message)
+ return
+ }
+ handle_translate(app, req, res)
+ })
+
+ // MARK: Account management routes
+
+ router.get('/accounts/:pubkey', (req, res) => {
+ const id = req.params.pubkey
+ if (!id) {
+ error_response(res, 'Could not parse account id')
+ return
+ }
+ let account = app.dbs.accounts.get(id)
+
+ if (!account) {
+ simple_response(res, 404)
+ return
+ }
+
+ let account_info = get_account_info_payload(account)
+
+ json_response(res, account_info)
+ })
+
+ router.post('/accounts', required_nip98_auth, (req, res) => {
+ let result = create_account(app, req.authorized_pubkey, null)
+
+ if (result.request_error) {
+ invalid_request(res, result.request_error)
+ return
+ }
+
+ json_response(res, get_account_info_payload(result.account))
+ return
+ })
+
+ router.post('/accounts/:pubkey/app-store-receipt', required_nip98_auth, async (req, res) => {
+ const id = req.params.pubkey
+ if (!id) {
+ error_response(res, 'Could not parse account id')
+ return
+ }
+ if (id != req.authorized_pubkey) {
+ unauthorized_response(res, 'You are not authorized to access this account')
+ return
+ }
+
+ let account = app.dbs.accounts.get(id)
+
+ if (!account) {
+ simple_response(res, 404)
+ return
+ }
+
+ const body = Buffer.from(req.body, 'base64').toString('ascii')
+
+ let expiry_date = await verify_receipt(body)
+
+ if (!expiry_date) {
+ error_response(res, 'Could not verify receipt')
+ return
+ }
+
+ account.expiry = expiry_date
+ app.dbs.accounts.put(id, account)
+ json_response(res, get_account_info_payload(account))
+ return
+ })
}

module.exports = { config_router }
diff --git a/src/server_helpers.js b/src/server_helpers.js
index c3155ad..9e541b3 100644
--- a/src/server_helpers.js
+++ b/src/server_helpers.js
@@ -1,176 +1,3 @@
-const nip98_auth = require('./nip98_auth');
-
-class Router {
- // MARK: - Constructors
-
- constructor(base_url) {
- this.routes = [];
- this.base_url = base_url;
- }
-
- // MARK: - Registering Routes
-
- get(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'GET'
- });
- }
-
- get_authenticated(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'GET',
- authenticated: true
- });
- }
-
- post(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'POST'
- })
- }
-
- post_authenticated(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'POST',
- authenticated: true
- })
- }
-
- put(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'PUT'
- })
- }
-
- put_authenticated(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'PUT',
- authenticated: true
- })
- }
-
- delete(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'DELETE'
- })
- }
-
- delete_authenticated(path, handler) {
- this.routes.push({
- path,
- handler,
- method: 'DELETE',
- authenticated: true
- })
- }
-
- // MARK: - Handling Requests
-
- handle_request(req, res) {
- console.log(`[ ${req.method} ] ${req.url}`);
- const route_match_info = this.find_route(req.url, req.method);
-
- if (!route_match_info || !route_match_info.route) {
- simple_response(res, 404);
- return;
- }
-
- var body = undefined;
- req.on('data', chunk => {
- if(!chunk) return;
- if(chunk instanceof String) {
- body = body ? body + chunk : chunk;
- return;
- }
- else if(chunk instanceof Buffer) {
- body = body ? Buffer.concat([body, chunk]) : chunk;
- return;
- }
- else {
- body = body ? body + chunk.toString() : chunk.toString();
- }
- })
- req.on('end', async () => {
- req.body = body;
- if(!route_match_info.route.authenticated) {
- route_match_info.route.handler(req, res, route_match_info.capture_groups);
- return;
- }
- else if(route_match_info.route.authenticated) {
- // Get Authorization header
- const auth_header = req.headers.authorization;
- // Check if it is valid
- let auth_pubkey = await nip98_auth(auth_header, this.base_url + req.url, req.method, req.body);
- if(!auth_pubkey) {
- unauthorized_response(res, 'Nostr authorization header invalid');
- return;
- }
- route_match_info.route.handler(req, res, route_match_info.capture_groups, auth_pubkey);
- }
- })
- }
-
- match_route(query_route, route_pattern) {
- const route_pattern_parts = route_pattern.split('/');
- const query_route_parts = query_route.split('/');
-
- if (route_pattern_parts.length !== query_route_parts.length) {
- return false;
- }
-
- const capture_groups = [];
-
- for (let i = 0; i < route_pattern_parts.length; i++) {
- const route_part = route_pattern_parts[i];
- const query_part = query_route_parts[i];
-
- let regex = new RegExp(route_part);
-
- if (regex.test(query_part)) {
- const capture_group = regex.exec(query_part)[1];
- capture_groups.push(capture_group);
- } else {
- return false;
- }
- }
-
- return capture_groups.filter(group => group !== undefined);
- }
-
- find_route(url, method) {
- for (let i = 0; i < this.routes.length; i++) {
- const route = this.routes[i];
-
- if (route.method === method) {
- const capture_groups = this.match_route(url, route.path);
-
- if (capture_groups) {
- return {
- route,
- capture_groups
- };
- }
- }
- }
-
- return undefined;
- }
-}
-
// MARK: - Response Helpers

function invalid_request(res, message) {
@@ -199,5 +26,5 @@ function json_response(res, json, code=200) {
}

module.exports = {
- json_response, simple_response, invalid_request, error_response, Router
+ json_response, simple_response, invalid_request, error_response, unauthorized_response
}
diff --git a/src/translate.js b/src/translate.js
index 1d7ef27..fde3ae4 100644
--- a/src/translate.js
+++ b/src/translate.js
@@ -1,143 +1,123 @@

const util = require('./server_helpers')
const crypto = require('crypto')
-const nostr = require('nostr')
-const check_account = require('./user_management').check_account
const current_time = require('./utils').current_time

-const translate_sources = new Set(['BG' ,'CS' ,'DA' ,'DE' ,'EL' ,'EN' ,'ES' ,'ET' ,'FI' ,'FR' ,'HU' ,'ID' ,'IT' ,'JA' ,'KO' ,'LT' ,'LV' ,'NB' ,'NL' ,'PL' ,'PT' ,'RO' ,'RU' ,'SK' ,'SL' ,'SV' ,'TR' ,'UK' ,'ZH'])
-const translate_targets = new Set(['BG' ,'CS' ,'DA' ,'DE' ,'EL' ,'EN' ,'EN-GB' ,'EN-US' ,'ES' ,'ET' ,'FI' ,'FR' ,'HU' ,'ID' ,'IT' ,'JA' ,'KO' ,'LT' ,'LV' ,'NB' ,'NL' ,'PL' ,'PT' ,'PT-BR' ,'PT-PT' ,'RO' ,'RU' ,'SK' ,'SL' ,'SV' ,'TR' ,'UK' ,'ZH'])
+const translate_sources = new Set(['BG', 'CS', 'DA', 'DE', 'EL', 'EN', 'ES', 'ET', 'FI', 'FR', 'HU', 'ID', 'IT', 'JA', 'KO', 'LT', 'LV', 'NB', 'NL', 'PL', 'PT', 'RO', 'RU', 'SK', 'SL', 'SV', 'TR', 'UK', 'ZH'])
+const translate_targets = new Set(['BG', 'CS', 'DA', 'DE', 'EL', 'EN', 'EN-GB', 'EN-US', 'ES', 'ET', 'FI', 'FR', 'HU', 'ID', 'IT', 'JA', 'KO', 'LT', 'LV', 'NB', 'NL', 'PL', 'PT', 'PT-BR', 'PT-PT', 'RO', 'RU', 'SK', 'SL', 'SV', 'TR', 'UK', 'ZH'])

const DEEPL_KEY = process.env.DEEPL_KEY
const DEEPL_URL = process.env.DEEPL_URL || 'https://api.deepl.com/v2/translate'

if (!DEEPL_KEY)
- throw new Error("expected DEEPL_KEY env var")
-
-async function validate_payload(api, note, payload)
-{
- if (!payload.source)
- return 'missing source'
- if (!payload.target)
- return 'missing target'
- if (!payload.q)
- return 'missing q'
- if (!translate_sources.has(payload.source))
- return 'invalid translation source'
- if (!translate_targets.has(payload.target))
- return 'invalid translation target'
-
- // validate the signature before we check account status to prevent
- // people from probing account info
- const valid = await nostr.verifyEvent(note)
- if (!valid)
- return 'invalid note signature'
-
- // Make sure the request was created within a range of 10 seconds
- // to prevent replay attacks
- if (Math.abs(current_time() - note.created_at) >= 10) // TODO: change to 10
- return 'request too old'
-
- const account_ok = check_account(api, note)
- if (account_ok !== 'ok')
- return account_ok
-
- return 'valid'
+ throw new Error("expected DEEPL_KEY env var")
+
+async function validate_payload(payload) {
+ if (!payload.source)
+ return { ok: false, message: 'missing source' }
+ if (!payload.target)
+ return { ok: false, message: 'missing target' }
+ if (!payload.q)
+ return { ok: false, message: 'missing q' }
+ if (!translate_sources.has(payload.source))
+ return { ok: false, message: 'invalid translation source' }
+ if (!translate_targets.has(payload.target))
+ return { ok: false, message: 'invalid translation target' }
+
+ return { ok: true, message: 'valid' }
}

-function hash_payload(payload)
-{
- const hash = crypto.createHash('sha256')
- hash.update(payload.q)
- hash.update(payload.source)
- hash.update(payload.target)
- return hash.digest()
+function hash_payload(payload) {
+ const hash = crypto.createHash('sha256')
+ hash.update(payload.q)
+ hash.update(payload.source)
+ hash.update(payload.target)
+ return hash.digest()
}

-async function deepl_translate_text(payload)
-{
- let resp = await fetch(DEEPL_URL, {
- method: 'POST',
- headers: {
- 'Authorization': `DeepL-Auth-Key ${DEEPL_KEY}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- text: [payload.q],
- source_lang: payload.source,
- target_lang: payload.target,
- })
- })
-
- let data = await resp.json()
-
- if (data.translations && data.translations.length > 0) {
- return data.translations[0].text;
- }
-
- return null
+async function deepl_translate_text(payload) {
+ let resp = await fetch(DEEPL_URL, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `DeepL-Auth-Key ${DEEPL_KEY}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ text: [payload.q],
+ source_lang: payload.source,
+ target_lang: payload.target,
+ })
+ })
+
+ let data = await resp.json()
+
+ if (data.translations && data.translations.length > 0) {
+ return data.translations[0].text;
+ }
+
+ return null
}

-async function translate_payload(api, res, note, payload, trans_id)
-{
- // we might already be translating this
- const job = api.translation.queue[trans_id]
- if (job) {
- let text = await job
- if (text === null)
- return util.error_response(res, 'deepl translation error')
-
- return util.json_response(res, { text })
- }
-
- // we might have it in the database already
- const translation = api.dbs.translations.get(trans_id)
- if (translation) {
- const text = translation.text
- if (text == null)
- return util.error_response(res, 'translation fetch error')
- return util.json_response(res, { text })
- }
-
- const new_job = deepl_translate_text(payload)
- api.translation.queue[trans_id] = new_job
-
- let text = await new_job
- if (text === null) {
- delete api.translation.queue[trans_id]
- return util.error_response(res, 'deepl translation error')
- }
-
- // return results immediately
- util.json_response(res, { text })
-
- // write result to db
- await api.dbs.translations.put(trans_id, {
- text: text,
- translated_at: current_time(),
- payload: payload
- })
-
- delete api.translation.queue[trans_id]
+async function translate_payload(api, res, payload, trans_id) {
+ // we might already be translating this
+ const job = api.translation.queue[trans_id]
+ if (job) {
+ let text = await job
+ if (text === null)
+ return util.error_response(res, 'deepl translation error')
+
+ return util.json_response(res, { text })
+ }
+
+ // we might have it in the database already
+ const translation = api.dbs.translations.get(trans_id)
+ if (translation) {
+ const text = translation.text
+ if (text == null)
+ return util.error_response(res, 'translation fetch error')
+ return util.json_response(res, { text })
+ }
+
+ const new_job = deepl_translate_text(payload)
+ api.translation.queue[trans_id] = new_job
+
+ let text = await new_job
+ if (text === null) {
+ delete api.translation.queue[trans_id]
+ return util.error_response(res, 'deepl translation error')
+ }
+
+ // return results immediately
+ util.json_response(res, { text })
+
+ // write result to db
+ await api.dbs.translations.put(trans_id, {
+ text: text,
+ translated_at: current_time(),
+ payload: payload
+ })
+
+ delete api.translation.queue[trans_id]
}

-async function handle_translate(api, req, res)
-{
- let id
- try {
- const note = JSON.parse(req.body)
- const payload = JSON.parse(note.content)
- const validation_res = await validate_payload(api, note, payload)
- //if (validation_res !== 'valid')
- //return util.invalid_request(res, validation_res)
- id = hash_payload(payload)
- return translate_payload(api, res, note, payload, id)
- } catch (err) {
- if (id)
- delete api.translation.queue[id]
- util.invalid_request(res, `error processing request: ${err}`)
- throw err
- }
+async function handle_translate(api, req, res) {
+ let id
+ try {
+ const source = req.query.source
+ const target = req.query.target
+ const q = req.query.q
+ const payload = { source, target, q }
+ const validation_res = await validate_payload(payload)
+ if (validation_res.ok === false)
+ return util.invalid_request(res, validation_res.message)
+ id = hash_payload(payload)
+ return translate_payload(api, res, payload, id)
+ } catch (err) {
+ if (id)
+ delete api.translation.queue[id]
+ util.invalid_request(res, `error processing request: ${err}`)
+ throw err
+ }
}

module.exports = handle_translate
diff --git a/src/user_management.js b/src/user_management.js
index 7837968..0958de3 100644
--- a/src/user_management.js
+++ b/src/user_management.js
@@ -1,22 +1,16 @@
const { current_time } = require('./utils')

-// check to see if the account is active and is allowed to
-// translate stuff
-function check_account(api, note) {
- return check_account_by_pubkey_hex(api, note.pubkey)
-}
-
-function check_account_by_pubkey_hex(api, pubkey) {
+function check_account(api, pubkey) {
const id = Buffer.from(pubkey)
const account = api.dbs.accounts.get(id)

if (!account)
- return 'account not found'
+ return { ok: false, message: 'Account not found' }

if (!account.expiry || current_time() >= account.expiry)
- return 'account expired'
+ return { ok: false, message: 'Account expired' }

- return 'ok'
+ return { ok: true, message: null }
}

function create_account(api, pubkey, expiry) {
@@ -48,4 +42,4 @@ function get_account_info_payload(account) {
}
}

-module.exports = { check_account, check_account_by_pubkey_hex, create_account, get_account_info_payload }
+module.exports = { check_account, create_account, get_account_info_payload }
diff --git a/test/helpers.test.js b/test/helpers.test.js
deleted file mode 100644
index 26a6690..0000000
--- a/test/helpers.test.js
+++ /dev/null
@@ -1,82 +0,0 @@
-const test = require('tap').test;
-const Router = require('../src/server_helpers.js').Router;
-
-
-test('Router - Registering Routes', (t) => {
- const router = new Router("http://localhost:8989");
-
- t.test('should register a GET route', (t) => {
- router.get('/users', (req, res) => {
- res.end('GET /users');
- });
-
- const route = router.routes.find((r) => r.method === 'GET' && r.path === '/users');
- t.ok(route, 'GET route should be registered');
- t.end();
- });
-
- t.test('should register a POST route', (t) => {
- router.post('/users', (req, res) => {
- res.end('POST /users');
- });
-
- const route = router.routes.find((r) => r.method === 'POST' && r.path === '/users');
- t.ok(route, 'POST route should be registered');
- t.end();
- });
-
- t.end();
-});
-
-test('Router - Handling Requests', (t) => {
- const router = new Router("http://localhost:8989");
-
- t.test('should handle a valid GET request', (t) => {
- router.get('/users/(.+)', (req, res, captureGroups) => {
- res.end(`GET /users/${captureGroups[0]}`);
- });
-
- const req = {
- url: '/users/123',
- method: 'GET',
- on: (event, callback) => {
- if (event === 'data') {
- callback({}); // Simulate a request body
- }
- if (event === 'end') {
- callback();
- }
- }
-
- };
- const res = {
- end: (data) => {
- t.equal(data, 'GET /users/123', 'Response should match expected value');
- t.end();
- },
- writeHead: () => {}
- };
-
- router.handle_request(req, res);
- });
-
- // Add tests for handling other HTTP methods and error cases here
-
- t.end();
-});
-
-test('Router - Matching Routes', (t) => {
- const router = new Router("http://localhost:8989");
-
- t.same(router.match_route('/users', '/users'), [], 'Route should be matched with no capture groups');
- t.same(router.match_route('/users/123', '/users/\\d+'), [], 'Route should be matched with no capture groups');
- t.same(router.match_route('/users/123', '/users/(.+)'), ["123"], 'Route should be matched with one capture group');
- t.same(router.match_route('/users/123', '/users'), false, 'Route should not be matched');
- t.same(router.match_route('/users/123', '/users/(.+)/(.+)'), false, 'Route should not be matched');
- t.same(router.match_route('/users/123/receipt/456', '/users/(.+)/receipt/(.+)'), ['123', '456'], 'Route should be matched with multiple capture groups');
- t.same(router.match_route('/users/123/receipt/456', '/users/(.+)/receipt'), false, 'Route should not be matched');
- t.same(router.match_route('/users/123/receipt/456', '/users/(.+)/receipt/(.+)/(.+)'), false, 'Route should not be matched');
- t.same(router.match_route('/users/123/receipt/abc', '/users/(.+)/receipt/(\d+)'), false, 'Route should not be matched');
-
- t.end();
-});
diff --git a/test/protected_routes.test.js b/test/protected_routes.test.js
index abd9722..0d80b30 100644
--- a/test/protected_routes.test.js
+++ b/test/protected_routes.test.js
@@ -1,442 +1,342 @@
const test = require('tap').test;
-const Router = require('../src/server_helpers.js').Router;
+const express = require('express');
const nostr = require('nostr');
const { current_time, hash_sha256 } = require('../src/utils.js');
+const { capture_raw_body, required_nip98_auth } = require('../src/nip98_auth.js');
+const bodyParser = require('body-parser');
+const { supertest_client } = require('./utils.js');

test('Router – Protected POST route should accept valid NIP-98 auth header (include payload)', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data({
- endpoint: '/test-good-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-good-auth',
- }, (data) => {
- t.same(data, 'OK', 'Response should match expected value');
- resolve();
- }, (status_code, headers) => {
- t.same(status_code, 200, 'Response should match expected value');
- });
-
- // Register a protected POST route for our successful test case
- router.post_authenticated('/test-good-auth', (req, res, capture_groups, auth_pubkey) => {
- t.same(auth_pubkey, test_data.good_pubkey, 'Auth pubkey should match test pubkey');
- t.same(req.body, test_data.good_body, 'Payload should match test payload');
- res.end('OK');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-good-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: app.base_url + '/test-good-auth',
+ });
+
+ app.post('/test-good-auth', required_nip98_auth, (req, res) => {
+ t.same(req.authorized_pubkey, test_data.good_pubkey, 'Auth pubkey should match test pubkey');
+ t.same(req.body, test_data.good_body, 'Payload should match test payload');
+ res.end('OK');
+ });
+
+ const response = await request
+ .post('/test-good-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`)
+ .send(test_data.good_body);
+
+ t.same(response.status, 200, 'Response should match expected value');
+ t.same(response.text, 'OK', 'Response should match expected value');
+ t.end();
});

test('Router – Protected POST route should accept valid NIP-98 auth header (no payload)', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data({
- endpoint: '/test-good-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-good-auth',
- omit_body: true,
- }, (data) => {
- t.same(data, 'OK', 'Response should match expected value');
- resolve();
- }, (status_code, headers) => {
- t.same(status_code, 200, 'Response should match expected value');
- });
-
- // Register a protected POST route for our successful test case
- router.post_authenticated('/test-good-auth', (req, res, capture_groups, auth_pubkey) => {
- t.same(auth_pubkey, test_data.good_pubkey, 'Auth pubkey should match test pubkey');
- res.end('OK');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-good-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: app.base_url + '/test-good-auth',
+ omit_body: true,
+ });
+
+ app.post('/test-good-auth', required_nip98_auth, (req, res) => {
+ t.same(req.authorized_pubkey, test_data.good_pubkey, 'Auth pubkey should match test pubkey');
+ res.end('OK');
+ });
+
+ const response = await request
+ .post('/test-good-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`);
+
+ t.same(response.status, 200, 'Response should match expected value');
+ t.same(response.text, 'OK', 'Response should match expected value');
+ t.end();
});

-
test('Router – Protected POST route should not accept NIP-98 header with invalid signature', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- fake_sig: true // Use a fake signature
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-bad-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: app.base_url + '/test-bad-auth',
+ fake_sig: true,
+ });
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-
test('Router – Protected POST route should not accept NIP-98 header with expired note', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-auth',
- expired_time: true // Use an expired timestamp
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- // Register a protected POST route for our expired test case
- router.post_authenticated('/test-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: app.base_url + '/test-auth',
+ expired_time: true,
+ });
+
+ app.post('/test-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-
test('Router – Protected POST route should not accept NIP-98 header with incorrect URL', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- incorrect_url: true // Use an incorrect URL
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-bad-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: 'http://127.0.0.1/test-bad-auth',
+ incorrect_url: true,
+ });
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-
test('Router – Protected POST route should not accept NIP-98 header with incorrect METHOD', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'PUT', // Simulate an incorrect method on the authorization header
- url: 'http://localhost:8989/test-bad-auth',
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-bad-auth',
+ method: 'POST',
+ authorized_method: 'PUT',
+ url: 'http://127.0.0.1/test-bad-auth',
+ });
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-
test('Router – Protected POST route should not accept NIP-98 header with incorrect SHA-256', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- fake_body: true // Use a fake body
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-bad-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: 'http://127.0.0.1/test-bad-auth',
+ fake_body: true,
+ });
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`)
+ .send(test_data.fake_body);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-
test('Router – Protected POST route should not accept empty NIP-98 header', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- empty_auth_header: true // Use an empty Authorization header
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr `);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

test('Router – Protected POST route should not accept NIP-98 header with bogus base64', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- bogus_base64: true // Use a bogus base64 string
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-bad-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: 'http://127.0.0.1/test-bad-auth',
+ bogus_base64: true,
+ });
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-
test('Router – Protected POST route should not accept no NIP-98 auth header', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- omit_auth_header: true // Omit the Authorization header
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth');
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

test('Router – Protected POST route should not accept if there is a payload but none is declared in the auth header', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- omit_payload_tag: true // Omit the payload tag
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-bad-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: 'http://127.0.0.1/test-bad-auth',
+ omit_payload_tag: true,
+ });
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`)
+ .send(test_data.good_body);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-test('Router – Protected POST route should not accept if there is no payload one is declared in the auth header', async (t) => {
- const router = new Router("http://localhost:8989");
-
- return new Promise(async (resolve, reject) => {
- const test_data = await generate_test_data(
- {
- endpoint: '/test-bad-auth',
- method: 'POST',
- authorized_method: 'POST',
- url: 'http://localhost:8989/test-bad-auth',
- omit_body: true, // Omit the body
- force_include_payload_tag: true // Force include the payload tag
- },
- (data) => { },
- (status_code, headers) => {
- t.same(status_code, 401, 'Response should match expected value');
- resolve();
- }
- );
-
- router.post_authenticated('/test-bad-auth', (req, res, capture_groups, auth_pubkey) => {
- t.fail('Should not be called');
- });
-
- router.handle_request(test_data.auth_req, test_data.auth_res);
- });
+test('Router – Protected POST route should not accept if there is no payload but one is declared in the auth header', async (t) => {
+ const app = express();
+ app.use(bodyParser.json({ verify: capture_raw_body }));
+ const request = await supertest_client(app, t);
+
+ const test_data = await generate_test_data({
+ endpoint: '/test-bad-auth',
+ method: 'POST',
+ authorized_method: 'POST',
+ url: 'http://127.0.0.1/test-bad-auth',
+ omit_body: true,
+ force_include_payload_tag: true,
+ });
+
+ app.post('/test-bad-auth', required_nip98_auth, (req, res) => {
+ t.fail('Should not be called');
+ });
+
+ const response = await request
+ .post('/test-bad-auth')
+ .set('Authorization', `Nostr ${test_data.note_base64}`);
+
+ t.same(response.status, 401, 'Response should match expected value');
+ t.end();
});

-
-async function generate_test_data(options, end_callback, writehead_callback) {
- let test_privkey = '10a9842fadc0aae2a649a1b707bf97e48c787b8517af4728ba3ec304089451be';
- let test_pubkey = nostr.getPublicKey(test_privkey)
- let villain_privkey = 'eb538cb990641cbde30e63601a18ceb63ed2a8da08b32d85ae1ae4941e163fec';
-
- let body = good_test_body();
- let fake_body = JSON.stringify({ foo: 'baz' });
-
- // Follows NIP-98
- const note_template = {
- pubkey: test_pubkey,
- created_at: options.expired_time ? current_time() - 61 : current_time(),
- kind: options.wrong_kind ? 1 : 27235,
- tags: [
- ["u", options.incorrect_url ? "http://localhost:8989/some-different-url" : options.url],
- ["method", options.authorized_method],
- ],
- content: '',
- }
-
- if((!options.omit_body || options.force_include_payload_tag) && !options.omit_payload_tag) {
- note_template.tags.push(["payload", options.fake_body ? hash_sha256(fake_body) : hash_sha256(body)]);
- }
-
- const note_id = await nostr.calculateId(note_template);
- const note_sig = await nostr.signId(options.fake_sig ? villain_privkey : test_privkey, note_id);
-
- const note = {
- ...note_template,
- id: note_id,
- sig: note_sig
- }
- const note_base64 = options.bogus_base64 ? "aaaaaaaaaaa" : Buffer.from(JSON.stringify(note)).toString('base64');
-
- const auth_req = generate_mock_request(
- options.endpoint,
- options.method,
- options.omit_body ? undefined : body,
- options.omit_auth_header ?
- { 'Content-Type': 'application/json' }
- :
- options.empty_auth_header ?
- {
- 'Content-Type': 'application/json',
- 'Authorization': `Nostr `
- }
- :
- {
- 'Content-Type': 'application/json',
- 'Authorization': `Nostr ${note_base64}`
- }
- );
-
- const auth_res = generate_mock_response(end_callback, writehead_callback);
-
- return {
- auth_req,
- auth_res,
- good_pubkey: test_pubkey,
- good_body: body,
- };
+async function generate_test_data(options) {
+ let test_privkey = '10a9842fadc0aae2a649a1b707bf97e48c787b8517af4728ba3ec304089451be';
+ let test_pubkey = nostr.getPublicKey(test_privkey);
+ let villain_privkey = 'eb538cb990641cbde30e63601a18ceb63ed2a8da08b32d85ae1ae4941e163fec';
+
+ let body = good_test_body();
+ let fake_body = { foo: 'baz' };
+
+ // Follows NIP-98
+ const note_template = {
+ pubkey: test_pubkey,
+ created_at: options.expired_time ? current_time() - 61 : current_time(),
+ kind: options.wrong_kind ? 1 : 27235,
+ tags: [
+ ["u", options.incorrect_url ? "http://127.0.0.1/some-different-url" : options.url],
+ ["method", options.authorized_method],
+ ],
+ content: '',
+ }
+
+ if ((!options.omit_body || options.force_include_payload_tag) && !options.omit_payload_tag) {
+ note_template.tags.push(["payload", options.fake_body ? hash_sha256(JSON.stringify(fake_body)) : hash_sha256(JSON.stringify(body))]);
+ }
+
+ const note_id = await nostr.calculateId(note_template);
+ const note_sig = await nostr.signId(options.fake_sig ? villain_privkey : test_privkey, note_id);
+
+ const note = {
+ ...note_template,
+ id: note_id,
+ sig: note_sig
+ }
+ const note_base64 = options.bogus_base64 ? "aaaaaaaaaaa" : Buffer.from(JSON.stringify(note)).toString('base64');
+
+ return {
+ note_base64,
+ good_pubkey: test_pubkey,
+ good_body: body,
+ };
}

function good_test_body() {
- return JSON.stringify({ foo: 'bar' });
-}
-
-function generate_mock_request(url, method, body, headers) {
- if (headers["Authorization"]) {
- headers.authorization = headers["Authorization"];
- delete headers["Authorization"];
- }
- return {
- url,
- method,
- body,
- headers,
- on: (event, callback) => {
- if (event === 'data') {
- callback(body);
- }
- if (event === 'end') {
- callback();
- }
- }
- };
-}
-
-function generate_mock_response(end_callback, writehead_callback) {
- return {
- end: end_callback,
- writeHead: (statusCode, headers) => {
- // Ensure statusCode is a number
- if (typeof statusCode !== 'number') {
- throw new Error('Status code must be a number');
- }
- // Ensure headers is an object
- if (typeof headers !== 'object') {
- throw new Error('Headers must be an object');
- }
- writehead_callback(statusCode, headers);
- }
- };
+ return { foo: 'bar' };
}
diff --git a/test/router_config.test.js b/test/router_config.test.js
index 1cb3643..cfac585 100644
--- a/test/router_config.test.js
+++ b/test/router_config.test.js
@@ -1,28 +1,11 @@
const test = require('tap').test;
-const Router = require('../src/server_helpers.js').Router;
+const express = require('express');
const config_router = require('../src/router_config.js').config_router;
const nostr = require('nostr');
const current_time = require('../src/utils.js').current_time;
+const { supertest_client } = require('./utils.js');

-test('config_router - Translation routes', (t) => {
- const app = {
- router: new Router(),
- dbs: {
- accounts: {
- get: () => { }
- }
- }
- };
-
- config_router(app);
-
- const route = app.router.find_route('/translate', 'GET')
-
- t.ok(route, 'GET /translate route should be registered');
- t.end();
-});
-
-test('config_router - Account management routes', (t) => {
+test('config_router - Account management routes', async (t) => {
const account_info = {
pubkey: 'abc123',
created_at: Date.now() - 60 * 60 * 24 * 30 * 1000, // 30 days ago
@@ -33,7 +16,7 @@ test('config_router - Account management routes', (t) => {
}

const app = {
- router: new Router('http://localhost:8989'),
+ router: express(),
dbs: {
accounts: {
get: (id) => {
@@ -46,33 +29,23 @@ test('config_router - Account management routes', (t) => {
}
};

+ const request = await supertest_client(app.router, t);
+
config_router(app);

- t.test('should handle a valid GET request for an existing account ', (t) => {
- const req = {
- url: '/accounts/abc123',
- method: 'GET',
- on: (event, callback) => {
- if (event === 'end') {
- callback();
- }
- }
- };
- const res = {
- end: (data) => {
- const expectedData = JSON.stringify({
- pubkey: account_info.pubkey,
- created_at: account_info.created_at,
- expiry: account_info.expiry,
- active: true,
- }) + '\n';
- t.same(data, expectedData, 'Response should match expected value');
- t.end();
- },
- writeHead: () => { }
- };
+ t.test('should handle a valid GET request for an existing account ', async (t) => {
+ const res = await request
+ .get('/accounts/abc123')
+ .expect(200);

- app.router.handle_request(req, res);
+ const expectedData = {
+ pubkey: account_info.pubkey,
+ created_at: account_info.created_at,
+ expiry: account_info.expiry,
+ active: true,
+ };
+ t.same(res.body, expectedData, 'Response should match expected value');
+ t.end();
});

t.test('should handle a valid POST request to create an account', async (t) => {
@@ -84,8 +57,8 @@ test('config_router - Account management routes', (t) => {
created_at: current_time(),
kind: 27235,
tags: [
- ["u", app.router.base_url + "/accounts"],
- ["method", "POST"]
+ ["u", app.router.base_url + "/accounts"],
+ ["method", "POST"]
],
content: '',
}
@@ -99,36 +72,16 @@ test('config_router - Account management routes', (t) => {
}
let auth_note_base64 = Buffer.from(JSON.stringify(auth_note)).toString('base64');

- return new Promise((resolve, reject) => {
- const req = {
- url: '/accounts',
- headers: {
- 'authorization': 'Nostr ' + auth_note_base64
- },
- method: 'POST',
- on: (event, callback) => {
- if (event === 'data') {
- // No data
- }
- if (event === 'end') {
- callback();
- }
- }
- };
- const res = {
- end: (data) => {
- data = JSON.parse(data)
- t.equal(data.pubkey, test_pubkey, 'Pubkey should match requested value');
- t.equal(data.active, false, 'Account should be inactive before payment is made');
- t.equal(data.expiry, null, 'Account should not have an expiry before payment is made');
- resolve();
- },
- writeHead: () => { }
- };
-
- app.router.handle_request(req, res);
- });
+ const res = await request
+ .post('/accounts')
+ .set('authorization', 'Nostr ' + auth_note_base64)
+ .expect(200);
+
+ t.equal(res.body.pubkey, test_pubkey, 'Pubkey should match requested value');
+ t.equal(res.body.active, false, 'Account should be inactive before payment is made');
+ t.equal(res.body.expiry, null, 'Account should not have an expiry before payment is made');
+ t.end();
});

t.end();
-});
\ No newline at end of file
+});
diff --git a/test/translate.test.js b/test/translate.test.js
index 9554cb0..33ec259 100644
--- a/test/translate.test.js
+++ b/test/translate.test.js
@@ -1,148 +1,182 @@
const tap = require('tap');
-const translate_payload = require('../src/translate');
+const express = require('express');
const sinon = require('sinon');
const nostr = require('nostr');
+const { config_router } = require('../src/router_config');
+const current_time = require('../src/utils').current_time;
+const { supertest_client, TEST_BASE_URL } = require('./utils');


// MARK: - Tests

tap.test('translate_payload - Existing translation in database (mocked db)', async (t) => {
- const api = generate_test_api({
- simulate_existing_translation_in_db: "<EXISTING_TRANSLATION>",
- simulate_account_found_in_db: true,
- });
-
- const res = {
- writeHead: () => { },
- end: (response) => {
- // Check JSON response
- t.same(response, '{\"text\":\"<EXISTING_TRANSLATION>\"}\n');
- },
- };
- const req = await generate_test_req();
-
- await translate_payload(api, req, res);
-
- t.end();
+ const api = await generate_test_api(t, {
+ simulate_existing_translation_in_db: "<EXISTING_TRANSLATION>",
+ simulate_account_found_in_db: true,
+ });
+
+ const test_data = await generate_test_request_data(api);
+ const expected_result = {
+ text: "<EXISTING_TRANSLATION>",
+ };
+
+ const res = await api.test_request
+ .get(test_data.query_url)
+ .set('Authorization', 'Nostr ' + test_data.auth_note_base64)
+
+ t.same(res.statusCode, 200, 'Response should be 200');
+ t.same(res.body, expected_result, 'Response should match expected value');
+ t.end();
});

tap.test('translate_payload - New translation (mocked server)', async (t) => {
- const api = generate_test_api({
- simulate_existing_translation_in_db: false,
- simulate_account_found_in_db: true,
- });
-
- const res = {
- writeHead: () => { },
- end: (response) => {
- // Check JSON response
- t.same(response, '{\"text\":\"Mock translation\"}\n');
- },
- };
- const req = await generate_test_req();
+ const api = await generate_test_api(t, {
+ simulate_existing_translation_in_db: false,
+ simulate_account_found_in_db: true,
+ });
+
+ const expected_result = {
+ text: "Mock translation",
+ };
+
+ // Create a stub for fetch
+ const fetchStub = sinon.stub(global, 'fetch').returns(Promise.resolve({
+ json: async () => {
+ return {
+ translations: [
+ expected_result
+ ]
+ };
+ }
+ }));

- // Create a stub for fetch
- const fetchStub = sinon.stub(global, 'fetch').returns(Promise.resolve({
- json: async () => {
- return {
- translations: [
- { text: 'Mock translation' }
- ]
- };
- }
- }));
+ const test_data = await generate_test_request_data(api);
+
+ const res = await api.test_request
+ .get(test_data.query_url)
+ .set('Authorization', 'Nostr ' + test_data.auth_note_base64)

- await translate_payload(api, req, res);
+ t.same(res.statusCode, 200, 'Response should be 200');
+ t.same(res.body, expected_result, 'Response should match expected value');

- // Restore fetch
- fetchStub.restore();
+ // Restore fetch
+ fetchStub.restore();

- t.end();
+ t.end();
+});
+
+tap.test('translate - Account not found (mocked db)', async (t) => {
+ const api = await generate_test_api(t, {
+ simulate_existing_translation_in_db: false,
+ simulate_account_found_in_db: false,
+ });
+ const test_data = await generate_test_request_data(api);
+ const res = await api.test_request
+ .get(test_data.query_url)
+ .set('Authorization', 'Nostr ' + test_data.auth_note_base64)
+ t.same(res.statusCode, 401, 'Response should be 401');
+ t.end();
+});
+
+tap.test('translate - Account expired (mocked db)', async (t) => {
+ const api = await generate_test_api(t, {
+ simulate_existing_translation_in_db: false,
+ simulate_account_expired: true,
+ });
+ const test_data = await generate_test_request_data(api);
+ const res = await api.test_request
+ .get(test_data.query_url)
+ .set('Authorization', 'Nostr ' + test_data.auth_note_base64)
+ t.same(res.statusCode, 401, 'Response should be 401');
+ t.end();
});


// MARK: - Helpers


-function generate_test_api(config) {
- return {
- translation: {
- queue: {},
+async function generate_test_api(t, config) {
+ const api = {
+ router: express(),
+ translation: {
+ queue: {},
+ },
+ dbs: {
+ translations: {
+ get: (trans_id) => {
+ if (config.simulate_existing_translation_in_db) {
+ // Simulate existing translation in the database
+ return {
+ text: config.simulate_existing_translation_in_db,
+ };
+ } else {
+ // Simulate translation not found in the database
+ return null;
+ }
},
- dbs: {
- translations: {
- get: (trans_id) => {
- if (config.simulate_existing_translation_in_db) {
- // Simulate existing translation in the database
- return {
- text: config.simulate_existing_translation_in_db,
- };
- } else {
- // Simulate translation not found in the database
- return null;
- }
- },
- put: () => { },
- },
- accounts: {
- get: (id) => {
- if (config.simulate_account_found_in_db) {
- // Simulate account found in the database
- return {
- expiry: Date.now() + 60 * 60 * 24 * 30 * 1000 // 30 days
- };
- }
- else if (config.simulate_account_expired) {
- // Simulate account found in the database
- return {
- expiry: Date.now() - 60 * 60 * 24 * 30 * 1000 // 30 days ago
- };
- }
- else {
- // Simulate account not found in the database
- return null;
- }
- },
- },
+ put: () => { },
+ },
+ accounts: {
+ get: (id) => {
+ if (config.simulate_account_found_in_db) {
+ // Simulate account found in the database
+ return {
+ expiry: current_time() + 60 * 60 * 24 * 30 * 1000 // 30 days
+ };
+ }
+ else if (config.simulate_account_expired) {
+ // Simulate account found in the database
+ return {
+ expiry: current_time() - 60 * 60 * 24 * 30 * 1000 // 30 days ago
+ };
+ }
+ else {
+ // Simulate account not found in the database
+ return null;
+ }
},
- };
-}
-
-
-async function generate_test_req() {
- const note = await generate_test_event();
- const req = {
- body: JSON.stringify(note),
- };
- return req;
-}
+ },
+ },
+ };

+ config_router(api);

-async function generate_test_event(payload) {
- let sk = '10a9842fadc0aae2a649a1b707bf97e48c787b8517af4728ba3ec304089451be'
- let pk = nostr.getPublicKey(sk)
+ api.test_request = await supertest_client(api.router, t);

- let event = {
- kind: 1,
- created_at: Math.floor(Date.now() / 1000),
- tags: [],
- content: JSON.stringify(payload || generate_translation_request_payload()),
- pubkey: pk,
- }
-
- event.id = await nostr.calculateId(event)
- event.sig = await nostr.signId(sk, event.id)
-
- return event
+ return api;
}

-function generate_translation_request_payload() {
-
- let payload = {
- source: 'EN',
- target: 'JA',
- q: "Hello"
- }
-
- return payload
+async function generate_test_request_data(api) {
+ let test_privkey = '10a9842fadc0aae2a649a1b707bf97e48c787b8517af4728ba3ec304089451be';
+ let test_pubkey = nostr.getPublicKey(test_privkey);
+
+ let query_url = '/translate?source=EN&target=JA&q=Hello'
+ let full_query_url = api.router.base_url + query_url;
+
+ let auth_note_template = {
+ pubkey: test_pubkey,
+ created_at: current_time(),
+ kind: 27235,
+ tags: [
+ ["u", full_query_url],
+ ["method", "GET"],
+ ],
+ content: '',
+ }
+
+ let auth_note_id = await nostr.calculateId(auth_note_template);
+ let auth_note_sig = await nostr.signId(test_privkey, auth_note_id);
+ let auth_note = {
+ ...auth_note_template,
+ id: auth_note_id,
+ sig: auth_note_sig
+ }
+ let auth_note_base64 = Buffer.from(JSON.stringify(auth_note)).toString('base64');
+
+ return {
+ auth_note_base64,
+ query_url,
+ full_query_url,
+ };
}
+
diff --git a/test/utils.js b/test/utils.js
new file mode 100644
index 0000000..76e5d18
--- /dev/null
+++ b/test/utils.js
@@ -0,0 +1,26 @@
+const supertest = require('supertest');
+
+async function create_http_server(app, t) {
+ return new Promise((resolve, reject) => {
+ const random_port = Math.floor(Math.random() * 10000) + 10000;
+ const http_server = app.listen(random_port, () => {
+ app.port = random_port;
+ app.base_url = 'http://127.0.0.1:' + random_port;
+ resolve(http_server);
+ });
+
+ // Close server after test is done
+ t.teardown(() => {
+ http_server.close();
+ });
+ });
+}
+
+async function supertest_client(app, t) {
+ const http_server = await create_http_server(app, t);
+ return supertest(http_server);
+}
+
+module.exports = {
+ supertest_client
+}
--
2.39.1


William Casarin

unread,
Dec 27, 2023, 9:41:11 PM12/27/23
to Daniel D’Aquino, pat...@damus.io
On Thu, Dec 28, 2023 at 12:06:08AM +0000, Daniel D’Aquino wrote:
>This commit brings the following changes:
>- Refactor server to use express.js (instead of a custom router)
>- Refactor NIP-98 authentication to become an express.js middleware

perfect!

>- Use supertest for testing (Improves test readability, realism, and makes it possible to test express.js)

nice

>- Improve translate endpoint to use NIP-98 for authentication
>- Add NIP-98 auth support for GET requests
>- Improve account creation to grab the pubkey directly from the auth header, thus removing the need for a custom payload
>
>Testing
>--------
>
>All unit tests are passing.
>
>Closes: https://github.com/damus-io/api/issues/1
>Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
>---
>Hi Will,
>
>These are the changes done to migrate the server from a custom router
>into express.js, and to use supertest.
>
>I have decided to tackle this as early as possible while the codebase is
>still small, and refactoring is manageable.

good call!

>I had to make some changes to the client-side code to make it work with
>these changes. I will be sending a separate patch for that.

ok sounds good.

Reviewed-by: William Casarin <jb...@jb55.com>
Reply all
Reply to author
Forward
0 new messages