diff --git a/.env.example b/.env.example index ee1b377..0195190 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ HOST="127.0.0.1" PORT="1234" +JWT_SECRET="foo" +DB_CONNECTION_STRING="postgresql://user:password@127.0.0.1:5432/data-uploader" W3UP_PRINCIPAL_KEY="foo" W3UP_DELEGATION_PROOF="foo" S3_ENDPOINT="http://foo.bar" diff --git a/Dockerfile b/Dockerfile index b1b50a2..8e7b064 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,12 @@ ENV HOST=$HOST ARG PORT ENV PORT=$PORT +ARG JWT_SECRET +ENV JWT_SECRET=$JWT_SECRET + +ARG DB_CONNECTION_STRING +ENV DB_CONNECTION_STRING=$DB_CONNECTION_STRING + ARG W3UP_PRINCIPAL_KEY ENV W3UP_PRINCIPAL_KEY=$W3UP_PRINCIPAL_KEY diff --git a/README.md b/README.md index ec2839a..ebc1e7c 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ The required env variables are: - `HOST`: the server's host. - `PORT`: the server's port. +- `DB_CONNECTION_STRING`: a connection string to a Postgres database. +- `JWT_SECRET`: the secret used to sign the issued JWTs. It's of utmost + importance to keep this value secret. - `W3UP_PRINCIPAL_KEY`: a key identifying a principal that was previously delegated by a w3up space owner to access the space itself. - `W3UP_DELEGATION_PROOF`: a proof that proves the delegation of `store` and @@ -60,6 +63,19 @@ In order to get the correct values for `W3UP_PRINCIPAL_KEY` and `W3UP_DELEGATION_PROOF` follow [this procedure](https://github.com/web3-storage/w3up/tree/main/packages/w3up-client#bringing-your-own-agent-and-delegation). +Once the `.env` file has been created, it's necessary to have all the correlated +infrastructure up and running in order to properly test the server. In +particular we need a `Postgres` database in which the server can store nonces to +avoid signature replay attacks. + +For convenience all the needed infrastructure can easily be spun up using the +provided `docker-compose.yaml` file at the root of the package. Run the +following command to bootstrap everything: + +``` +docker compose up +``` + Once the `.env` file has been created you can go ahead and start the server using the following command launched from the package's root: diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..fd45046 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,10 @@ +version: "3" +services: + postgres: + container_name: postgres + image: postgres:latest + ports: + - 127.0.0.1:5432:5432 + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password diff --git a/package.json b/package.json index c5918ee..3b11154 100644 --- a/package.json +++ b/package.json @@ -17,27 +17,31 @@ "devDependencies": { "@commitlint/cli": "^18.4.4", "@commitlint/config-conventional": "^18.4.4", - "@hapi/inert": "^7.1.0", - "@hapi/vision": "^7.0.3", "dotenv": "^16.3.1", "esbuild": "^0.19.11", "eslint": "^8.56.0", "eslint-config-custom": "*", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "hapi-swagger": "^17.2.0", "husky": "^8.0.3", + "pino": "^8.17.2", "prettier": "^3.2.4" }, "dependencies": { "@aws-sdk/client-s3": "^3.490.0", "@hapi/boom": "^10.0.1", "@hapi/hapi": "^21.3.2", + "@hapi/inert": "^7.1.0", + "@hapi/vision": "^7.0.3", "@ipld/car": "^5.2.6", "@ucanto/core": "^9.0.1", "@ucanto/principal": "^9.0.0", "@web3-storage/w3up-client": "^12.0.0", "hapi-pino": "^12.1.0", - "joi": "^17.11.1" + "hapi-swagger": "^17.2.0", + "joi": "^17.11.1", + "jsonwebtoken": "^9.0.2", + "pg": "^8.11.3", + "viem": "^2.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d721ad..b3425f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ dependencies: '@hapi/hapi': specifier: ^21.3.2 version: 21.3.2 + '@hapi/inert': + specifier: ^7.1.0 + version: 7.1.0 + '@hapi/vision': + specifier: ^7.0.3 + version: 7.0.3 '@ipld/car': specifier: ^5.2.6 version: 5.2.6 @@ -29,9 +35,21 @@ dependencies: hapi-pino: specifier: ^12.1.0 version: 12.1.0 + hapi-swagger: + specifier: ^17.2.0 + version: 17.2.0(@hapi/hapi@21.3.2)(joi@17.11.1)(openapi-types@12.1.3) joi: specifier: ^17.11.1 version: 17.11.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + pg: + specifier: ^8.11.3 + version: 8.11.3 + viem: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.3.3) devDependencies: '@commitlint/cli': @@ -40,12 +58,6 @@ devDependencies: '@commitlint/config-conventional': specifier: ^18.4.4 version: 18.4.4 - '@hapi/inert': - specifier: ^7.1.0 - version: 7.1.0 - '@hapi/vision': - specifier: ^7.0.3 - version: 7.0.3 dotenv: specifier: ^16.3.1 version: 16.3.1 @@ -64,12 +76,12 @@ devDependencies: eslint-plugin-prettier: specifier: ^5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.2.4) - hapi-swagger: - specifier: ^17.2.0 - version: 17.2.0(@hapi/hapi@21.3.2)(joi@17.11.1)(openapi-types@12.1.3) husky: specifier: ^8.0.3 version: 8.0.3 + pino: + specifier: ^8.17.2 + version: 8.17.2 prettier: specifier: ^3.2.4 version: 3.2.4 @@ -81,6 +93,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@adraffy/ens-normalize@1.10.0: + resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + dev: false + /@apidevtools/json-schema-ref-parser@11.1.0: resolution: {integrity: sha512-g/VW9ZQEFJAOwAyUb8JFf7MLiLy2uEB4rU270rGzDwICxnxMlPy0O11KVePSgS36K1NI29gSlK84n5INGhd4Ag==} engines: {node: '>= 16'} @@ -90,7 +106,7 @@ packages: '@types/lodash.clonedeep': 4.5.9 js-yaml: 4.1.0 lodash.clonedeep: 4.5.0 - dev: true + dev: false /@apidevtools/json-schema-ref-parser@9.1.2: resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} @@ -99,16 +115,16 @@ packages: '@types/json-schema': 7.0.15 call-me-maybe: 1.0.2 js-yaml: 4.1.0 - dev: true + dev: false /@apidevtools/openapi-schemas@2.1.0: resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} engines: {node: '>=10'} - dev: true + dev: false /@apidevtools/swagger-methods@3.0.2: resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} - dev: true + dev: false /@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3): resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} @@ -122,7 +138,7 @@ packages: call-me-maybe: 1.0.2 openapi-types: 12.1.3 z-schema: 5.0.5 - dev: true + dev: false /@aws-crypto/crc32@3.0.0: resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} @@ -1151,42 +1167,50 @@ packages: dependencies: '@hapi/boom': 10.0.1 '@hapi/hoek': 11.0.4 + dev: false /@hapi/ammo@6.0.1: resolution: {integrity: sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==} dependencies: '@hapi/hoek': 11.0.4 + dev: false /@hapi/b64@6.0.1: resolution: {integrity: sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==} dependencies: '@hapi/hoek': 11.0.4 + dev: false /@hapi/boom@10.0.1: resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==} dependencies: '@hapi/hoek': 11.0.4 + dev: false /@hapi/bounce@3.0.1: resolution: {integrity: sha512-G+/Pp9c1Ha4FDP+3Sy/Xwg2O4Ahaw3lIZFSX+BL4uWi64CmiETuZPxhKDUD4xBMOUZbBlzvO8HjiK8ePnhBadA==} dependencies: '@hapi/boom': 10.0.1 '@hapi/hoek': 11.0.4 + dev: false /@hapi/bourne@3.0.0: resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + dev: false /@hapi/call@9.0.1: resolution: {integrity: sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==} dependencies: '@hapi/boom': 10.0.1 '@hapi/hoek': 11.0.4 + dev: false /@hapi/catbox-memory@6.0.1: resolution: {integrity: sha512-sVb+/ZxbZIvaMtJfAbdyY+QJUQg9oKTwamXpEg/5xnfG5WbJLTjvEn4kIGKz9pN3ENNbIL/bIdctmHmqi/AdGA==} dependencies: '@hapi/boom': 10.0.1 '@hapi/hoek': 11.0.4 + dev: false /@hapi/catbox@12.1.1: resolution: {integrity: sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==} @@ -1195,20 +1219,24 @@ packages: '@hapi/hoek': 11.0.4 '@hapi/podium': 5.0.1 '@hapi/validate': 2.0.1 + dev: false /@hapi/content@6.0.0: resolution: {integrity: sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==} dependencies: '@hapi/boom': 10.0.1 + dev: false /@hapi/cryptiles@6.0.1: resolution: {integrity: sha512-9GM9ECEHfR8lk5ASOKG4+4ZsEzFqLfhiryIJ2ISePVB92OHLp/yne4m+zn7z9dgvM98TLpiFebjDFQ0UHcqxXQ==} engines: {node: '>=14.0.0'} dependencies: '@hapi/boom': 10.0.1 + dev: false /@hapi/file@3.0.0: resolution: {integrity: sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==} + dev: false /@hapi/hapi@21.3.2: resolution: {integrity: sha512-tbm0zmsdUj8iw4NzFV30FST/W4qzh/Lsw6Q5o5gAhOuoirWvxm8a4G3o60bqBw8nXvRNJ8uLtE0RKLlZINxHcQ==} @@ -1232,6 +1260,7 @@ packages: '@hapi/teamwork': 6.0.0 '@hapi/topo': 6.0.2 '@hapi/validate': 2.0.1 + dev: false /@hapi/heavy@8.0.1: resolution: {integrity: sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==} @@ -1239,12 +1268,15 @@ packages: '@hapi/boom': 10.0.1 '@hapi/hoek': 11.0.4 '@hapi/validate': 2.0.1 + dev: false /@hapi/hoek@11.0.4: resolution: {integrity: sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==} + dev: false /@hapi/hoek@9.3.0: resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + dev: false /@hapi/inert@7.1.0: resolution: {integrity: sha512-5X+cl/Ozm0U9uPGGX1dSKhnhTQIf161bH/kkTN9OBVAZKFG+nrj8j/NMj6S1zBBZWmQrkVRNPfCUGrXzB4fCFQ==} @@ -1255,7 +1287,7 @@ packages: '@hapi/hoek': 11.0.4 '@hapi/validate': 2.0.1 lru-cache: 7.18.3 - dev: true + dev: false /@hapi/iron@7.0.1: resolution: {integrity: sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==} @@ -1265,12 +1297,14 @@ packages: '@hapi/bourne': 3.0.0 '@hapi/cryptiles': 6.0.1 '@hapi/hoek': 11.0.4 + dev: false /@hapi/mimos@7.0.1: resolution: {integrity: sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==} dependencies: '@hapi/hoek': 11.0.4 mime-db: 1.52.0 + dev: false /@hapi/nigel@5.0.1: resolution: {integrity: sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==} @@ -1278,6 +1312,7 @@ packages: dependencies: '@hapi/hoek': 11.0.4 '@hapi/vise': 5.0.1 + dev: false /@hapi/pez@6.1.0: resolution: {integrity: sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg==} @@ -1287,6 +1322,7 @@ packages: '@hapi/content': 6.0.0 '@hapi/hoek': 11.0.4 '@hapi/nigel': 5.0.1 + dev: false /@hapi/podium@5.0.1: resolution: {integrity: sha512-eznFTw6rdBhAijXFIlBOMJJd+lXTvqbrBIS4Iu80r2KTVIo4g+7fLy4NKp/8+UnSt5Ox6mJtAlKBU/Sf5080TQ==} @@ -1294,18 +1330,21 @@ packages: '@hapi/hoek': 11.0.4 '@hapi/teamwork': 6.0.0 '@hapi/validate': 2.0.1 + dev: false /@hapi/shot@6.0.1: resolution: {integrity: sha512-s5ynMKZXYoDd3dqPw5YTvOR/vjHvMTxc388+0qL0jZZP1+uwXuUD32o9DuuuLsmTlyXCWi02BJl1pBpwRuUrNA==} dependencies: '@hapi/hoek': 11.0.4 '@hapi/validate': 2.0.1 + dev: false /@hapi/somever@4.1.1: resolution: {integrity: sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==} dependencies: '@hapi/bounce': 3.0.1 '@hapi/hoek': 11.0.4 + dev: false /@hapi/statehood@8.1.1: resolution: {integrity: sha512-YbK7PSVUA59NArAW5Np0tKRoIZ5VNYUicOk7uJmWZF6XyH5gGL+k62w77SIJb0AoAJ0QdGQMCQ/WOGL1S3Ydow==} @@ -1317,6 +1356,7 @@ packages: '@hapi/hoek': 11.0.4 '@hapi/iron': 7.0.1 '@hapi/validate': 2.0.1 + dev: false /@hapi/subtext@8.1.0: resolution: {integrity: sha512-PyaN4oSMtqPjjVxLny1k0iYg4+fwGusIhaom9B2StinBclHs7v46mIW706Y+Wo21lcgulGyXbQrmT/w4dus6ww==} @@ -1328,31 +1368,37 @@ packages: '@hapi/hoek': 11.0.4 '@hapi/pez': 6.1.0 '@hapi/wreck': 18.0.1 + dev: false /@hapi/teamwork@6.0.0: resolution: {integrity: sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==} engines: {node: '>=14.0.0'} + dev: false /@hapi/topo@5.1.0: resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} dependencies: '@hapi/hoek': 9.3.0 + dev: false /@hapi/topo@6.0.2: resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} dependencies: '@hapi/hoek': 11.0.4 + dev: false /@hapi/validate@2.0.1: resolution: {integrity: sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==} dependencies: '@hapi/hoek': 11.0.4 '@hapi/topo': 6.0.2 + dev: false /@hapi/vise@5.0.1: resolution: {integrity: sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==} dependencies: '@hapi/hoek': 11.0.4 + dev: false /@hapi/vision@7.0.3: resolution: {integrity: sha512-1UM3Xej7HZQPaxzWkefvMfcuXoF9R8kIiDTl+Pfdv8f5mJwAv0zIB4R/UvNoQP1+JYgQT+QeUDxcGD8QdIUDyg==} @@ -1361,7 +1407,7 @@ packages: '@hapi/bounce': 3.0.1 '@hapi/hoek': 11.0.4 '@hapi/validate': 2.0.1 - dev: true + dev: false /@hapi/wreck@18.0.1: resolution: {integrity: sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg==} @@ -1369,6 +1415,7 @@ packages: '@hapi/boom': 10.0.1 '@hapi/bourne': 3.0.0 '@hapi/hoek': 11.0.4 + dev: false /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} @@ -1445,7 +1492,7 @@ packages: /@jsdevtools/ono@7.1.3: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - dev: true + dev: false /@multiformats/murmur3@2.1.8: resolution: {integrity: sha512-6vId1C46ra3R1sbJUOFCZnsUIveR9oF20yhPmAFxPm0JfrX3/ZRCgP3YDrBzlGoEppOXnA9czHeYc0T9mB6hbA==} @@ -1461,6 +1508,12 @@ packages: glob: 7.1.7 dev: true + /@noble/curves@1.2.0: + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + dependencies: + '@noble/hashes': 1.3.2 + dev: false + /@noble/curves@1.3.0: resolution: {integrity: sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==} dependencies: @@ -1471,6 +1524,11 @@ packages: resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} dev: false + /@noble/hashes@1.3.2: + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + dev: false + /@noble/hashes@1.3.3: resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} engines: {node: '>= 16'} @@ -1560,6 +1618,21 @@ packages: resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==} dev: false + /@scure/bip32@1.3.2: + resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} + dependencies: + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.5 + dev: false + + /@scure/bip39@1.2.1: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + dependencies: + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.5 + dev: false + /@scure/bip39@1.2.2: resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} dependencies: @@ -1571,12 +1644,15 @@ packages: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: '@hapi/hoek': 9.3.0 + dev: false /@sideway/formula@3.0.1: resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + dev: false /@sideway/pinpoint@2.0.0: resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + dev: false /@smithy/abort-controller@2.0.16: resolution: {integrity: sha512-4foO7738k8kM9flMHu3VLabqu7nPgvIj8TB909S0CnKx0YZz/dcDH3pZ/4JHdatfxlZdKF1JWOYCw9+v3HVVsw==} @@ -2035,7 +2111,7 @@ packages: /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true + dev: false /@types/json5@0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -2045,11 +2121,11 @@ packages: resolution: {integrity: sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==} dependencies: '@types/lodash': 4.14.202 - dev: true + dev: false /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} - dev: true + dev: false /@types/minimatch@3.0.5: resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} @@ -2307,12 +2383,25 @@ packages: through: 2.3.8 dev: true + /abitype@1.0.0(typescript@5.3.3): + resolution: {integrity: sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + dependencies: + typescript: 5.3.3 + dev: false + /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} dependencies: event-target-shim: 5.0.1 - dev: false /abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -2389,7 +2478,6 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true /aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -2496,7 +2584,6 @@ packages: /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - dev: false /atomically@2.0.2: resolution: {integrity: sha512-Xfmb4q5QV7uqTlVdMSTtO5eF4DCHfNOdaPyKlbFShkzeNP+3lj3yjjcbdjSmEY4+pDBKJ9g26aP+ImTe88UHoQ==} @@ -2526,7 +2613,6 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false /bigint-mod-arith@3.3.1: resolution: {integrity: sha512-pX/cYW3dCa87Jrzv6DAr8ivbbJRzEX5yGhdt8IutnX/PCIXfpx+mabWNK/M8qqh+zQ0J3thftUBHW0ByuUlG0w==} @@ -2554,12 +2640,20 @@ packages: resolution: {integrity: sha512-+12sHB+Br8HIh6VAMVEG5r3UXCyESIgDW7kzk3BjIXa43DVqVwL7GC5TW3jeh+72dtcH99pPVpw0X8i0jt+/kw==} dev: false + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + dev: false + /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: false /call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} @@ -2571,7 +2665,7 @@ packages: /call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - dev: true + dev: false /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -2648,7 +2742,7 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} requiresBuild: true - dev: true + dev: false optional: true /compare-func@2.0.0: @@ -2858,6 +2952,12 @@ packages: engines: {node: '>=12'} dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /electron-fetch@1.9.1: resolution: {integrity: sha512-M9qw6oUILGVrcENMSRRefE1MbHPIz0h79EKIeJWK9v563aT9Qkh8aEHPO1H5vi970wPirNY+jO9OpFoLiMsMGA==} engines: {node: '>=6'} @@ -3376,12 +3476,10 @@ packages: /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - dev: false /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - dev: false /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} @@ -3431,7 +3529,6 @@ packages: /fast-redact@3.3.0: resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} engines: {node: '>=6'} - dev: false /fast-xml-parser@4.2.5: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} @@ -3653,7 +3750,7 @@ packages: wordwrap: 1.0.0 optionalDependencies: uglify-js: 3.17.4 - dev: true + dev: false /hapi-pino@12.1.0: resolution: {integrity: sha512-qPfigyVQjPM1cfGZuF/rSo82hS/s02Dtvvq0RvKFKBlJoilxCvx5zBLOI29odeKsft1h+lj6yOWIKT9Vz7M54g==} @@ -3682,7 +3779,7 @@ packages: swagger-ui-dist: 5.11.0 transitivePeerDependencies: - openapi-types - dev: true + dev: false /hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} @@ -3747,7 +3844,7 @@ packages: /http-status@1.7.3: resolution: {integrity: sha512-GS8tL1qHT2nBCMJDYMHGkkkKQLNkIAHz37vgO68XKvzv+XyqB4oh/DfmMHdtRzfqSJPj1xKG2TaELZtlCz6BEQ==} engines: {node: '>= 0.4.0'} - dev: true + dev: false /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} @@ -3769,7 +3866,6 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false /ignore@5.3.0: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} @@ -4048,6 +4144,14 @@ packages: engines: {node: '>=12'} dev: false + /isows@1.0.3(ws@8.13.0): + resolution: {integrity: sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==} + peerDependencies: + ws: '*' + dependencies: + ws: 8.13.0 + dev: false + /it-all@1.0.6: resolution: {integrity: sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A==} dev: false @@ -4093,6 +4197,7 @@ packages: '@sideway/address': 4.1.4 '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + dev: false /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4103,7 +4208,6 @@ packages: hasBin: true dependencies: argparse: 2.0.1 - dev: true /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -4140,6 +4244,22 @@ packages: engines: {'0': node >= 0.2.0} dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.5.4 + dev: false + /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -4150,6 +4270,21 @@ packages: object.values: 1.1.7 dev: true + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -4204,23 +4339,42 @@ packages: /lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - dev: true + dev: false /lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true + dev: false + + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false /lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: true + dev: false /lodash.isfunction@3.0.9: resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} dev: true + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - dev: true + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false /lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -4234,6 +4388,10 @@ packages: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} dev: true @@ -4274,7 +4432,7 @@ packages: /lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - dev: true + dev: false /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} @@ -4335,6 +4493,7 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + dev: false /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -4367,7 +4526,6 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -4375,7 +4533,6 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true /multiformats@11.0.2: resolution: {integrity: sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg==} @@ -4416,7 +4573,7 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true + dev: false /node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -4526,7 +4683,6 @@ packages: /on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} - dev: false /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4547,7 +4703,7 @@ packages: /openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - dev: true + dev: false /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} @@ -4619,6 +4775,10 @@ packages: engines: {node: '>=6'} dev: true + /packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + dev: false + /parallel-transform-web@1.0.1: resolution: {integrity: sha512-RtPU/7IuwPZ4ePcqoPxNCpjtaXYOkCVtnhh5tW3O78wy9jqVoV2hQHms17kUeu8DTYoOP+mykFLg2agwVKlwBw==} dev: false @@ -4664,6 +4824,70 @@ packages: engines: {node: '>=8'} dev: true + /pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + requiresBuild: true + dev: false + optional: true + + /pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + dev: false + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-pool@3.6.1(pg@8.11.3): + resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.11.3 + dev: false + + /pg-protocol@1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /pg@8.11.3: + resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.6.2 + pg-pool: 3.6.1(pg@8.11.3) + pg-protocol: 1.6.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + dev: false + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + dev: false + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -4674,11 +4898,9 @@ packages: dependencies: readable-stream: 4.5.2 split2: 4.2.0 - dev: false /pino-std-serializers@6.2.2: resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} - dev: false /pino@8.17.2: resolution: {integrity: sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==} @@ -4695,6 +4917,27 @@ packages: safe-stable-stringify: 2.4.3 sonic-boom: 3.8.0 thread-stream: 2.4.1 + + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 dev: false /prelude-ls@1.2.1: @@ -4717,12 +4960,10 @@ packages: /process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} - dev: false /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: false /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4761,7 +5002,6 @@ packages: /quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - dev: false /quick-lru@4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} @@ -4818,12 +5058,10 @@ packages: events: 3.3.0 process: 0.11.10 string_decoder: 1.3.0 - dev: false /real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - dev: false /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} @@ -4950,7 +5188,6 @@ packages: /safe-stable-stringify@2.4.3: resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} engines: {node: '>=10'} - dev: false /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -5026,12 +5263,11 @@ packages: resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} dependencies: atomic-sleep: 1.0.0 - dev: false /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true + dev: false /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -5187,11 +5423,11 @@ packages: '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3) transitivePeerDependencies: - openapi-types - dev: true + dev: false /swagger-ui-dist@5.11.0: resolution: {integrity: sha512-j0PIATqQSEFGOLmiJOJZj1X1Jt6bFIur3JpY7+ghliUnfZs0fpWDdHEkn9q7QUlBtKbkn6TepvSxTqnE8l3s0A==} - dev: true + dev: false /sync-multihash-sha2@1.0.0: resolution: {integrity: sha512-A5gVpmtKF0ov+/XID0M0QRJqF2QxAsj3x/LlDC8yivzgoYCoWkV+XaZPfVu7Vj1T/hYzYS1tfjwboSbXjqocug==} @@ -5220,7 +5456,6 @@ packages: resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} dependencies: real-require: 0.2.0 - dev: false /through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} @@ -5352,14 +5587,13 @@ packages: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true - dev: true /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true - dev: true + dev: false optional: true /uint8arrays@4.0.10: @@ -5403,12 +5637,35 @@ packages: /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} engines: {node: '>= 0.10'} - dev: true + dev: false /varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} dev: false + /viem@2.2.0(typescript@5.3.3): + resolution: {integrity: sha512-3wfpJTDwtO1f+OWdRQ+2Gr51AQ0rR1FPmBT19LZW8+QLFzXnYASR52tfapF1MfmitSIxZSwVoBEihekC8aAJqA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@adraffy/ens-normalize': 1.10.0 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 + abitype: 1.0.0(typescript@5.3.3) + isows: 1.0.3(ws@8.13.0) + typescript: 5.3.3 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false @@ -5482,7 +5739,7 @@ packages: /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true + dev: false /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} @@ -5497,6 +5754,24 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5543,7 +5818,7 @@ packages: validator: 13.11.0 optionalDependencies: commander: 9.5.0 - dev: true + dev: false github.com/web3-storage/one-webcrypto/5148cd14d5489a8ac4cd38223870e02db15a2382: resolution: {tarball: https://codeload.github.com/web3-storage/one-webcrypto/tar.gz/5148cd14d5489a8ac4cd38223870e02db15a2382} diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..b411134 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,2 @@ +export const JWT_ISSUER = "carrot-data-uploader"; +export const NONCE_LENGTH_BYTES = 32; diff --git a/src/index.js b/src/index.js index 0d18717..568c560 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,17 @@ import { server as createServer } from "@hapi/hapi"; import { getStoreJsonDataRoute } from "./routes/data/json.js"; -import { getS3Client, getW3UpClient } from "./utils.js"; +import { + getAuthenticationScheme, + getDbClient, + getS3Client, + getW3UpClient, +} from "./utils.js"; +import HapiPinoPlugin from "hapi-pino"; +import HapiInertPlugin from "@hapi/inert"; +import HapiVisionPlugin from "@hapi/vision"; +import HapiSwaggerPlugin from "hapi-swagger"; +import { getLoginMessageRoute } from "./routes/login-message.js"; +import { getTokenRoute } from "./routes/token.js"; const DEV = process.env.NODE_ENV !== "production"; @@ -27,27 +38,9 @@ const start = async () => { cors: true, }, }); - - const W3UP_PRINCIPAL_KEY = requireEnv({ name: "W3UP_PRINCIPAL_KEY" }); - const W3UP_DELEGATION_PROOF = requireEnv({ name: "W3UP_DELEGATION_PROOF" }); - const w3UpClient = await getW3UpClient({ - principalKey: W3UP_PRINCIPAL_KEY, - delegationProof: W3UP_DELEGATION_PROOF, - }); - - const S3_ENDPOINT = requireEnv({ name: "S3_ENDPOINT" }); - const S3_BUCKET = requireEnv({ name: "S3_BUCKET" }); - const S3_ACCESS_KEY_ID = requireEnv({ name: "S3_ACCESS_KEY_ID" }); - const S3_SECRET_ACCESS_KEY = requireEnv({ name: "S3_SECRET_ACCESS_KEY" }); - const s3Client = getS3Client({ - endpoint: S3_ENDPOINT, - accessKeyId: S3_ACCESS_KEY_ID, - secretAccessKey: S3_SECRET_ACCESS_KEY, - }); - - const serverPlugins = [ + await server.register([ { - plugin: (await import("hapi-pino")).default, + plugin: HapiPinoPlugin, options: { formatters: { level(label) { @@ -59,12 +52,10 @@ const start = async () => { }, }, }, - ]; - if (DEV) { - serverPlugins.push((await import("@hapi/inert")).default); - serverPlugins.push((await import("@hapi/vision")).default); - serverPlugins.push({ - plugin: (await import("hapi-swagger")).default, + HapiInertPlugin, + HapiVisionPlugin, + { + plugin: HapiSwaggerPlugin, options: { info: { title: "W3up uploader API", @@ -77,10 +68,42 @@ const start = async () => { }, }, }, - }); - } - await server.register(serverPlugins); + }, + ]); + + const DB_CONNECTION_STRING = requireEnv({ name: "DB_CONNECTION_STRING" }); + const dbClient = await getDbClient({ + connectionString: DB_CONNECTION_STRING, + logger: server.logger, + }); + + const W3UP_PRINCIPAL_KEY = requireEnv({ name: "W3UP_PRINCIPAL_KEY" }); + const W3UP_DELEGATION_PROOF = requireEnv({ name: "W3UP_DELEGATION_PROOF" }); + const w3UpClient = await getW3UpClient({ + principalKey: W3UP_PRINCIPAL_KEY, + delegationProof: W3UP_DELEGATION_PROOF, + }); + + const S3_ENDPOINT = requireEnv({ name: "S3_ENDPOINT" }); + const S3_BUCKET = requireEnv({ name: "S3_BUCKET" }); + const S3_ACCESS_KEY_ID = requireEnv({ name: "S3_ACCESS_KEY_ID" }); + const S3_SECRET_ACCESS_KEY = requireEnv({ name: "S3_SECRET_ACCESS_KEY" }); + const s3Client = getS3Client({ + endpoint: S3_ENDPOINT, + accessKeyId: S3_ACCESS_KEY_ID, + secretAccessKey: S3_SECRET_ACCESS_KEY, + }); + + const JWT_SECRET = requireEnv({ name: "JWT_SECRET" }); + server.auth.scheme( + "jwt", + getAuthenticationScheme({ jwtSecretKey: JWT_SECRET }), + ); + server.auth.strategy("jwt", "jwt"); + server.auth.default("jwt"); + server.route(getLoginMessageRoute({ dbClient })); + server.route(getTokenRoute({ dbClient, jwtSecretKey: JWT_SECRET })); server.route( await getStoreJsonDataRoute({ w3UpClient, diff --git a/src/routes/data/json.js b/src/routes/data/json.js index 0c0ef94..24bed48 100644 --- a/src/routes/data/json.js +++ b/src/routes/data/json.js @@ -48,6 +48,16 @@ export const getStoreJsonDataRoute = async ({ maxBytes: 1024, // 1kb }, validate: { + headers: joi + .object({ + authorization: joi + .string() + .required() + .regex( + /^Bearer [0-9a-zA-Z]*\.[0-9a-zA-Z]*\.[0-9a-zA-Z-_]*$/, + ), + }) + .unknown(), payload: joi.object({ data: joi .object() diff --git a/src/routes/login-message.js b/src/routes/login-message.js new file mode 100644 index 0000000..4b2b16c --- /dev/null +++ b/src/routes/login-message.js @@ -0,0 +1,89 @@ +import { badRequest, internal } from "@hapi/boom"; +import joi from "joi"; +import { isAddress, getAddress } from "viem/utils"; +import { getLoginMessage, updateOrInsertNonce } from "../utils.js"; + +/** + * @param {{ dbClient: import("pg").Client }} params + * @returns {import("@hapi/hapi").ServerRoute} + */ +export const getLoginMessageRoute = ({ dbClient }) => { + return { + method: "GET", + path: "/login-message/{address}", + options: { + plugins: { + "hapi-swagger": { + responses: { + 400: { + description: + "The address parameter was either not given or not valid.", + }, + 200: { + description: + "The nonce was created; The response contains the full login " + + "message to sign in order to authenticate.", + schema: joi + .object({ + message: joi + .string() + .label( + "The login message with the baked in nonce.", + ), + }) + .required(), + }, + }, + }, + }, + description: + "Gets a new login message to sign for a given address.", + notes: + "Updates or creates a new nonce for a given address (user), " + + "and returns the login message that a user needs to sign in order " + + "to authenticate, with the nonce baked in. This is used in order " + + "to avoid signature replay attacks.", + auth: false, + tags: ["api"], + validate: { + params: joi.object({ + address: joi + .string() + .required() + .regex(/0x[a-fA-F0-9]{40}/) + .description( + "The address for which to generate the login message.", + ), + }), + }, + }, + handler: async (request, h) => { + const { address } = request.params; + if (!isAddress(address)) return badRequest("invalid address"); + const checksummedAddress = getAddress(address); + + let nonce; + try { + nonce = await updateOrInsertNonce({ + client: dbClient, + address: checksummedAddress, + }); + } catch (error) { + request.logger.error( + error, + `Could not update or insert nonce for address ${checksummedAddress}`, + ); + return internal("Could not update or create nonce"); + } + + return h + .response({ + message: getLoginMessage({ + address: checksummedAddress, + nonce, + }), + }) + .type("application/json"); + }, + }; +}; diff --git a/src/routes/token.js b/src/routes/token.js new file mode 100644 index 0000000..1e34fad --- /dev/null +++ b/src/routes/token.js @@ -0,0 +1,125 @@ +import { badRequest, forbidden, internal } from "@hapi/boom"; +import joi from "joi"; +import { isAddress, getAddress, recoverMessageAddress } from "viem/utils"; +import { + getLoginMessage, + deleteNonce, + getNonce, + generateJWT, +} from "../utils.js"; + +/** + * @param {{ dbClient: import("pg").Client, jwtSecretKey: string }} params + * @returns {import("@hapi/hapi").ServerRoute} + */ +export const getTokenRoute = ({ dbClient, jwtSecretKey }) => { + return { + method: "POST", + path: "/token", + options: { + plugins: { + "hapi-swagger": { + responses: { + 400: { + description: + "The signature parameter was either not given or not valid.", + }, + 200: { + description: + "The JWT was successfully created, the response contains it.", + schema: joi.object({ + token: joi.string().label("The created JWT."), + }), + }, + }, + }, + }, + description: "Generates a new JWT token for a given user.", + notes: + "Generates a new JWT token for a given user, and returns it. " + + "The token will be valid for 24 hours", + auth: false, + tags: ["api"], + validate: { + payload: joi.object({ + address: joi + .string() + .required() + .regex(/0x[a-fA-F0-9]{40}/) + .description( + "The address of the account which signed the login message.", + ), + signature: joi + .string() + .required() + .regex(/0x[a-fA-F0-9]+/) + .description( + "A signed message that proves the user owns the address being authenticated. " + + "The signed message must be retrieved using the login message API.", + ), + }), + }, + }, + handler: async (request, h) => { + /** @type {{ address: import("viem").Address, signature: import("viem").Hex }} */ + const payload = request.payload; + const { address, signature } = payload; + + if (!isAddress(address)) return badRequest("Invalid address"); + const checksummedAddress = getAddress(address); + + let nonce; + try { + nonce = await getNonce({ + client: dbClient, + address: checksummedAddress, + }); + } catch (error) { + request.logger.error( + error, + `Could not get nonce for address ${checksummedAddress}`, + ); + return badRequest( + `Could not get nonce for address ${checksummedAddress}`, + ); + } + + let recoveredAddress; + try { + recoveredAddress = await recoverMessageAddress({ + message: getLoginMessage({ + address: checksummedAddress, + nonce, + }), + signature, + }); + } catch (error) { + request.logger.error(error, "Error while recovering signer"); + return badRequest("Error while recovering signer"); + } + + if (recoveredAddress !== address) + return forbidden("Address mismatch"); + + let token; + try { + token = generateJWT({ jwtSecretKey }); + } catch (error) { + request.logger.error(error, "Error while generating JWT"); + return internal("Error while generating JWT"); + } + + try { + await deleteNonce({ client: dbClient, address }); + } catch (error) { + request.logger.error( + error, + "Error while deleting nonce from database", + ); + return internal("Error while generating JWT"); + } + + return h.response({ token }).type("application/json"); + }, + }; +}; diff --git a/src/utils.js b/src/utils.js index 57c83b4..8b40c15 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,10 +3,16 @@ import { parse as parsePrincipalKey } from "@ucanto/principal/ed25519"; import { importDAG } from "@ucanto/core/delegation"; import { CarReader } from "@ipld/car"; import { S3Client } from "@aws-sdk/client-s3"; +import pg from "pg"; +import { randomBytes } from "crypto"; +import { isAddress } from "viem"; +import jsonwebtoken from "jsonwebtoken"; +import { JWT_ISSUER, NONCE_LENGTH_BYTES } from "./constants.js"; +import { unauthorized } from "@hapi/boom"; /** * @param {{ principalKey: string, delegationProof: string }} params - * @returns {import("@web3-storage/w3up-client").Client} + * @returns {Promise} */ export const getW3UpClient = async ({ principalKey, delegationProof }) => { const w3UpPrincipal = parsePrincipalKey(principalKey); @@ -42,3 +48,151 @@ export const getS3Client = ({ endpoint, accessKeyId, secretAccessKey }) => { }, }); }; + +/** + * @param {{ connectionString: string, logger: import("pino").Logger }} params + * @returns {Promise} + */ +export const getDbClient = async ({ connectionString, logger }) => { + let client = new pg.Client({ connectionString }); + try { + await client.connect(); + logger.info("Connected to database"); + } catch (error) { + logger.error("Error connecting to database", error); + + const parsedConnectionString = new URL(connectionString); + const { pathname, username } = parsedConnectionString; + const database = pathname.slice(1, pathname.length); + + parsedConnectionString.pathname = "/postgres"; + const postgresDatabaseClient = new pg.Client({ + connectionString: parsedConnectionString.toString(), + }); + await postgresDatabaseClient.connect(); + + logger.info(`Creating database ${database}`); + await postgresDatabaseClient.query(`CREATE DATABASE "${database}";`); + await postgresDatabaseClient.query( + `GRANT ALL PRIVILEGES ON DATABASE "${database}" TO "${username}";`, + ); + logger.info(`Database ${database} created`); + + logger.info(`Connecting to database ${database}`); + client = new pg.Client({ connectionString }); + await client.connect(); + logger.info("Connected to database"); + } + + logger.info("Creating table if they don't already exist"); + await client.query( + `CREATE TABLE IF NOT EXISTS nonces (address VARCHAR(42) PRIMARY KEY, value VARCHAR(${NONCE_LENGTH_BYTES * 2}))`, + ); + logger.info("Tables created"); + + return client; +}; + +/** + * @param {{ address: string, nonce: string; }} params + * @returns {string} + */ +export const getLoginMessage = ({ address, nonce }) => { + return ( + "Welcome to Carrot!\n\n" + + "Sign this message to authenticate.\n\n" + + "This request will not trigger a blockchain transaction or cost you any fees.\n\n" + + "Your authentication status will reset after 24 hours.\n\n" + + "Wallet address:\n" + + `${address}\n\n` + + "Nonce:\n" + + nonce + ); +}; + +/** + * @param {{ client: import("pg").Client, address: import("viem").Address }} params + * @returns {Promise} + */ +export const updateOrInsertNonce = async ({ client, address }) => { + if (!isAddress(address)) + throw new Error(`Invalid address ${address} given`); + const nonce = randomBytes(NONCE_LENGTH_BYTES).toString("hex"); + await client.query( + "INSERT INTO nonces (address, value) VALUES ($1, $2) ON CONFLICT (address) DO UPDATE SET value = EXCLUDED.value", + [address, nonce], + ); + return nonce; +}; + +/** + * @param {{ client: import("pg").Client, address: import("viem").Address }} params + * @returns {Promise} + */ +export const getNonce = async ({ client, address }) => { + if (!isAddress(address)) + throw new Error(`Invalid address ${address} given`); + const result = await client.query( + "SELECT value FROM nonces WHERE address = $1", + [address], + ); + const nonce = result.rows[0]?.value; + if (!nonce) throw new Error(`No nonce value found for address ${address}`); + return nonce; +}; + +/** + * @param {{ client: import("pg").Client, address: import("viem").Address }} params + */ +export const deleteNonce = async ({ client, address }) => { + if (!isAddress(address)) + throw new Error(`Invalid address ${address} given`); + await client.query("DELETE FROM nonces WHERE address = $1", [address]); +}; + +/** + * @param {{ jwtSecretKey: string }} params + * @returns {import("@hapi/hapi").ServerAuthScheme} + */ +export const getAuthenticationScheme = ({ jwtSecretKey }) => { + return () => ({ + authenticate: (request, h) => { + /** + * @type {{ authorization?: string }} params + */ + const headers = request.headers; + const { authorization } = headers; + + if (!authorization) + return unauthorized("Missing Authorization header"); + + if ( + !authorization.match( + /^Bearer [0-9a-zA-Z]*\.[0-9a-zA-Z]*\.[0-9a-zA-Z-_]*$/, + ) + ) + return unauthorized("Malformed Authorization header"); + + const jwt = authorization.split(" ")[1]; + + try { + jsonwebtoken.verify(jwt, jwtSecretKey, { issuer: JWT_ISSUER }); + } catch (error) { + return unauthorized("Invalid JWT"); + } + + return h.authenticated({ credentials: {} }); + }, + }); +}; + +/** + * @param {{ jwtSecretKey: string }} params + * @returns {string} + */ +export const generateJWT = ({ jwtSecretKey }) => { + return jsonwebtoken.sign({}, jwtSecretKey, { + expiresIn: "24 hours", + issuer: JWT_ISSUER, + }); +};