diff --git a/.nostr.local/settings.json b/.nostr.local/settings.json index ee1d976..037a0e7 100644 --- a/.nostr.local/settings.json +++ b/.nostr.local/settings.json @@ -1,19 +1,70 @@ { "info": { - "relay_url": "wss://nostream.localtest.me", - "name": "nostream.localtest.me", - "description": "A nostr relay written in TypeScript.", - "pubkey": "replace-with-your-pubkey", + "relay_url": "wss://nostr-relay-dev.wlvs.space", + "name": "nostr-relay-dev.wlvs.space", + "description": "A nostr relay written in Typescript.", + "pubkey": "00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", "contact": "operator@your-domain.com" }, "network": { - "max_payload_size": 131072, - "remote_ip_header": "x-forwarded-for" + "maxPayloadSize": 131072, + "remoteIpHeader": "x-forwarded-for", + "idleTimeout": 60 + }, + "payments": { + "enabled": true, + "processor": "zebedee", + "feeSchedules": { + "admission": [{ + "enabled": true, + "descripton": "Admission fee in msats (1000 msats = 1 satoshi)", + "amount": 1000000, + "whitelists": { + "pubkeys": ["replace-with-your-pubkey"] + } + }], + "publication": [ + { + "enabled": true, + "description": "Publication fee in msats (1000 msats = 1 satoshi)", + "amount": 100, + "whitelists": { + "pubkeys": ["replace-with-your-pubkey"] + } + } + ] + } + }, + "paymentProcessors": { + "zebedee": { + "baseURL": "https://api.zebedee.io/", + "callbackBaseURL": "https://nostr-relay-dev.wlvs.space/callbacks/zebedee" + } }, "workers": { "count": 0 }, "limits": { + "connection": { + "rateLimits": [ + { + "period": 60000, + "rate": 12 + }, + { + "period": 3600000, + "rate": 360 + }, + { + "period": 86400000, + "rate": 2880 + } + ], + "ipWhitelist": [ + "::1", + "::ffff:10.10.10.1" + ] + }, "event": { "eventId": { "minLeadingZeroBits": 0 @@ -31,45 +82,62 @@ "maxPositiveDelta": 900, "maxNegativeDelta": 0 }, - "content": { - "maxLength": 1048576 - }, + "content": [ + { + "description": "64 KB for event kind ranges 0-10 and 40-49", + "kinds": [[0, 10], [40, 49]], + "maxLength": 65536 + }, + { + "description": "96 KB for event kind ranges 11-39 and 50-max", + "kinds": [[11, 39], [50, 9007199254740991]], + "maxLength": 98304 + } + ], "rateLimits": [ { + "description": "6 events/min for event kinds 0, 3, 40 and 41", "kinds": [0, 3, 40, 41], "period": 60000, "rate": 6 }, { + "description": "12 events/min for event kinds 1, 2, 4 and 42", "kinds": [1, 2, 4, 42], "period": 60000, "rate": 12 }, { + "description": "360 events/hour for event kinds 1, 2, 4 and 42", "kinds": [1, 2, 4, 42], "period": 3600000, "rate": 360 }, { + "description": "30 events/min for event kind ranges 5-7 and 43-49", "kinds": [[5, 7], [43, 49]], "period": 60000, "rate": 30 }, { + "description": "24 events/min for replaceable events and parameterized replaceable events", "kinds": [[10000, 19999], [30000, 39999]], "period": 60000, "rate": 24 }, { + "description": "60 events/min for ephemeral events", "kinds": [[20000, 29999]], "period": 60000, "rate": 60 }, { + "description": "720 events/hour for all events", "period": 3600000, "rate": 720 }, { + "description": "2880 events/day for all events", "period": 86400000, "rate": 2880 } @@ -91,14 +159,29 @@ "message": { "rateLimits": [ { + "description": "60 subscriptions/min", + "types": ["REQ"], + "period": 60000, + "rate": 60 + }, + { + "description": "2880 subscriptions/hour", + "types": ["REQ"], + "period": 3600000, + "rate": 2880 + }, + { + "description": "120 raw messages/min", "period": 60000, "rate": 120 }, { + "description": "3600 raw messages/hour", "period": 3600000, "rate": 3600 }, { + "description": "86400 raw messages/day", "period": 86400000, "rate": 86400 } diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 015de4c..704f1c9 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -47,8 +47,8 @@ Running `nostream` for the first time creates the settings file in ` { + table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()')) + table.binary('pubkey').notNullable().index() + table.text('bolt11').notNullable() + table.bigint('amount_requested').unsigned().notNullable() + table.bigint('amount_paid').unsigned() + table.enum('unit', ['msats', 'sats', 'btc']) + table.enum('status', ['pending', 'completed']) + table.text('description') + table.datetime('confirmed_at', { useTz: false, precision: 3 }) + table.datetime('expires_at', { useTz: false, precision: 3 }) + table.timestamp('updated_at', { useTz: false }) + table.timestamps(true, true, false) + }) +} + +exports.down = function (knex) { + return knex.schema.dropTable('invoices') +} diff --git a/package-lock.json b/package-lock.json index 119a3ee..b494abd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,14 @@ "version": "1.18.0", "license": "MIT", "dependencies": { - "@noble/secp256k1": "^1.7.1", + "@noble/secp256k1": "1.7.1", + "axios": "1.2.2", + "bech32": "2.0.0", + "body-parser": "1.20.1", "debug": "4.3.4", "dotenv": "^16.0.3", + "express": "4.18.2", + "helmet": "6.0.1", "joi": "17.7.0", "knex": "2.4.0", "pg": "8.8.0", @@ -20,7 +25,7 @@ "redis": "4.5.1", "rxjs": "7.8.0", "tor-control-ts": "^1.0.0", - "ws": "^8.12.0" + "ws": "8.12.0" }, "devDependencies": { "@commitlint/cli": "17.2.0", @@ -35,6 +40,7 @@ "@types/chai": "^4.3.1", "@types/chai-as-promised": "^7.1.5", "@types/debug": "4.1.7", + "@types/express": "4.17.15", "@types/mocha": "^9.1.1", "@types/node": "^17.0.24", "@types/pg": "^8.6.5", @@ -1767,6 +1773,16 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", @@ -1782,6 +1798,15 @@ "@types/chai": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -1791,6 +1816,29 @@ "@types/ms": "*" } }, + "node_modules/@types/express": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz", + "integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.32", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz", + "integrity": "sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -1812,6 +1860,12 @@ "@types/unist": "*" } }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -1859,6 +1913,12 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, "node_modules/@types/ramda": { "version": "0.28.13", "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.28.13.tgz", @@ -1868,12 +1928,28 @@ "ts-toolbelt": "^6.15.1" } }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@types/sinon": { "version": "10.0.11", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz", @@ -2122,6 +2198,18 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "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.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", @@ -2367,6 +2455,11 @@ "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", "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/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -2429,8 +2522,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -2442,12 +2534,13 @@ } }, "node_modules/axios": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", - "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz", + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", "dependencies": { - "follow-redirects": "^1.14.7" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/bail": { @@ -2481,6 +2574,15 @@ "node": ">=10" } }, + "node_modules/base-api-client/node_modules/axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.7" + } + }, "node_modules/base-api-client/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2507,6 +2609,11 @@ } ] }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -2547,6 +2654,42 @@ "node": ">= 6" } }, + "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/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -2656,6 +2799,14 @@ "node": ">=4" } }, + "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/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -2680,6 +2831,18 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3021,7 +3184,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3162,6 +3324,25 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "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.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/conventional-changelog-angular": { "version": "5.0.13", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", @@ -3307,6 +3488,19 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "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/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -3713,17 +3907,33 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "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/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, + "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-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -3832,6 +4042,11 @@ "xtend": "^4.0.0" } }, + "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/electron-to-chromium": { "version": "1.4.218", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.218.tgz", @@ -3844,6 +4059,14 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "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/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3929,8 +4152,7 @@ "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==", - "dev": true + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -4162,6 +4384,14 @@ "node": ">=0.10.0" } }, + "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/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4197,6 +4427,65 @@ "node": ">=0.10.0" } }, + "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/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/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/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -4356,6 +4645,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-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -4467,7 +4786,6 @@ "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true, "funding": [ { "type": "individual", @@ -4500,7 +4818,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -4510,6 +4827,22 @@ "node": ">= 6" } }, + "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/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -4632,6 +4965,19 @@ "node": "*" } }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4967,6 +5313,17 @@ "node": ">=8" } }, + "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/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -5001,6 +5358,14 @@ "he": "bin/he" } }, + "node_modules/helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -5040,6 +5405,21 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "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": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5095,7 +5475,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -5191,8 +5570,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/ini": { "version": "2.0.0", @@ -5280,6 +5658,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", @@ -6273,6 +6659,14 @@ "url": "https://opencollective.com/unified" } }, + "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/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -6316,6 +6710,11 @@ "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", "dev": true }, + "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/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6331,6 +6730,14 @@ "node": ">= 8" } }, + "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/micromark": { "version": "2.11.4", "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", @@ -6380,7 +6787,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -6389,7 +6795,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -6720,6 +7125,14 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -9623,6 +10036,25 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "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", @@ -9927,6 +10359,14 @@ "parse-path": "^7.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/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10341,6 +10781,23 @@ "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", "dev": true }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -10360,6 +10817,20 @@ "teleport": ">=0.2.0" } }, + "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/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10407,6 +10878,28 @@ "safe-buffer": "^5.1.0" } }, + "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.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/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -10996,7 +11489,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11015,8 +11507,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==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/seed-random": { "version": "2.2.0", @@ -11158,6 +11649,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/send/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/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -11167,12 +11710,31 @@ "randombytes": "^2.1.0" } }, + "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-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "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", @@ -11194,6 +11756,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": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -11449,6 +12024,14 @@ "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", "dev": true }, + "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/stream-combiner2": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", @@ -11800,6 +12383,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/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -12065,6 +12656,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/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -12158,6 +12761,14 @@ "node": ">= 10.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/update-browserslist-db": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", @@ -12226,6 +12837,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "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", @@ -12266,6 +12885,14 @@ "node": ">= 0.10" } }, + "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/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -13928,6 +14555,16 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/chai": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", @@ -13943,6 +14580,15 @@ "@types/chai": "*" } }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -13952,6 +14598,29 @@ "@types/ms": "*" } }, + "@types/express": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz", + "integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.32", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz", + "integrity": "sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -13973,6 +14642,12 @@ "@types/unist": "*" } }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, "@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -14020,6 +14695,12 @@ "pg-types": "^2.2.0" } }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, "@types/ramda": { "version": "0.28.13", "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.28.13.tgz", @@ -14029,12 +14710,28 @@ "ts-toolbelt": "^6.15.1" } }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, "@types/sinon": { "version": "10.0.11", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz", @@ -14194,6 +14891,15 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "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==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "acorn": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", @@ -14383,6 +15089,11 @@ "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", "dev": true }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -14433,8 +15144,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "at-least-node": { "version": "1.0.0", @@ -14443,12 +15153,13 @@ "dev": true }, "axios": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", - "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz", + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", "requires": { - "follow-redirects": "^1.14.7" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "bail": { @@ -14475,6 +15186,15 @@ "uuid": "8.3.2" }, "dependencies": { + "axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.7" + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -14489,6 +15209,11 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, "before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -14525,6 +15250,40 @@ } } }, + "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==", + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -14595,6 +15354,11 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, "cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -14613,6 +15377,15 @@ "write-file-atomic": "^3.0.0" } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -14859,7 +15632,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -14972,6 +15744,19 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "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==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, "conventional-changelog-angular": { "version": "5.0.13", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", @@ -15093,6 +15878,16 @@ } } }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -15396,8 +16191,12 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "deprecation": { "version": "2.3.1", @@ -15405,6 +16204,11 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -15489,6 +16293,11 @@ "xtend": "^4.0.0" } }, + "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==" + }, "electron-to-chromium": { "version": "1.4.218", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.218.tgz", @@ -15501,6 +16310,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -15576,8 +16390,7 @@ "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==", - "dev": true + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "escape-string-regexp": { "version": "4.0.0", @@ -15747,6 +16560,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -15773,6 +16591,64 @@ "homedir-polyfill": "^1.0.1" } }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==" + } + } + }, "ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -15910,6 +16786,35 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -15993,8 +16898,7 @@ "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, "foreground-child": { "version": "2.0.0", @@ -16010,13 +16914,22 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -16103,6 +17016,16 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -16366,6 +17289,11 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, "hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -16390,6 +17318,11 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==" + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -16420,6 +17353,18 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "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==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -16457,7 +17402,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -16515,8 +17459,7 @@ "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==" }, "ini": { "version": "2.0.0", @@ -16579,6 +17522,11 @@ "p-is-promise": "^3.0.0" } }, + "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==" + }, "is-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", @@ -17326,6 +18274,11 @@ "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", "dev": true }, + "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==" + }, "meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -17359,6 +18312,11 @@ "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", "dev": true }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -17371,6 +18329,11 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, "micromark": { "version": "2.11.4", "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", @@ -17400,14 +18363,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "requires": { "mime-db": "1.52.0" } @@ -17666,6 +18627,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -19725,6 +20691,19 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -19949,6 +20928,11 @@ "parse-path": "^7.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -20262,6 +21246,20 @@ "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", "dev": true }, + "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==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -20274,6 +21272,14 @@ "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", "dev": true }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -20300,6 +21306,22 @@ "safe-buffer": "^5.1.0" } }, + "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==" + }, + "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==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -20765,14 +21787,12 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "seed-random": { "version": "2.2.0", @@ -20887,6 +21907,53 @@ "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", "dev": true }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -20896,12 +21963,28 @@ "randombytes": "^2.1.0" } }, + "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==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -20917,6 +22000,16 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -21130,6 +22223,11 @@ "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", "dev": true }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, "stream-combiner2": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", @@ -21405,6 +22503,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -21584,6 +22687,15 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -21650,6 +22762,11 @@ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, "update-browserslist-db": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", @@ -21704,6 +22821,11 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "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==" + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -21738,6 +22860,11 @@ "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, "verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", diff --git a/package.json b/package.json index 0bd8520..2b19d4e 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@types/chai": "^4.3.1", "@types/chai-as-promised": "^7.1.5", "@types/debug": "4.1.7", + "@types/express": "4.17.15", "@types/mocha": "^9.1.1", "@types/node": "^17.0.24", "@types/pg": "^8.6.5", @@ -109,8 +110,13 @@ }, "dependencies": { "@noble/secp256k1": "1.7.1", + "axios": "1.2.2", + "bech32": "2.0.0", + "body-parser": "1.20.1", "debug": "4.3.4", "dotenv": "^16.0.3", + "express": "4.18.2", + "helmet": "6.0.1", "joi": "17.7.0", "knex": "2.4.0", "pg": "8.8.0", diff --git a/resources/css/style.css b/resources/css/style.css new file mode 100644 index 0000000..df3066e --- /dev/null +++ b/resources/css/style.css @@ -0,0 +1,166 @@ +:root { + --bs-warning: #f7931a; +} +.m-auto { + margin: auto !important; +} +.w-100 { + width: 100% !important; +} +.w-66 { + width: 66% !important; +} +.btn-warning { + background-color: #f7931a; +} +.hidden { + display: none; +} +#invoice img { + width: 100%; +} +.mw-256 { + max-width: 256px; +} +.success-checkmark { + width: 80px; + height: 115px; + margin: 0 auto; +} +.success-checkmark .check-icon { + width: 80px; + height: 80px; + position: relative; + border-radius: 50%; + box-sizing: content-box; + border: 4px solid #4CAF50; +} +.success-checkmark .check-icon::before { + top: 3px; + left: -2px; + width: 30px; + transform-origin: 100% 50%; + border-radius: 100px 0 0 100px; +} +.success-checkmark .check-icon::after { + top: 0; + left: 30px; + width: 60px; + transform-origin: 0 50%; + border-radius: 0 100px 100px 0; + animation: rotate-circle 10.25s ease-in; +} +.success-checkmark .check-icon::before, .success-checkmark .check-icon::after { + content: ""; + height: 100px; + position: absolute; + background: #FFFFFF; + transform: rotate(-45deg); +} +.success-checkmark .check-icon .icon-line { + height: 5px; + background-color: #4CAF50; + display: block; + border-radius: 2px; + position: absolute; + z-index: 10; +} +.success-checkmark .check-icon .icon-line.line-tip { + top: 46px; + left: 14px; + width: 25px; + transform: rotate(45deg); + animation: icon-line-tip 2s; +} +.success-checkmark .check-icon .icon-line.line-long { + top: 38px; + right: 8px; + width: 47px; + transform: rotate(-45deg); + animation: icon-line-long 2s; +} +.success-checkmark .check-icon .icon-circle { + top: -4px; + left: -4px; + z-index: 10; + width: 80px; + height: 80px; + border-radius: 50%; + position: absolute; + box-sizing: content-box; + border: 4px solid rgba(76, 175, 80, 0.5); +} +.success-checkmark .check-icon .icon-fix { + top: 8px; + width: 5px; + left: 26px; + z-index: 1; + height: 85px; + position: absolute; + transform: rotate(-45deg); + background-color: #FFFFFF; +} + +@keyframes rotate-circle { + 0% { + transform: rotate(-45deg); + } + 5% { + transform: rotate(-45deg); + } + 12% { + transform: rotate(-405deg); + } + 100% { + transform: rotate(-405deg); + } +} +@keyframes icon-line-tip { + 0% { + width: 0; + left: 1px; + top: 19px; + } + 54% { + width: 0; + left: 1px; + top: 19px; + } + 70% { + width: 50px; + left: -8px; + top: 37px; + } + 84% { + width: 17px; + left: 21px; + top: 48px; + } + 100% { + width: 25px; + left: 14px; + top: 45px; + } +} +@keyframes icon-line-long { + 0% { + width: 0; + right: 46px; + top: 54px; + } + 65% { + width: 0; + right: 46px; + top: 54px; + } + 84% { + width: 55px; + right: 0px; + top: 35px; + } + 100% { + width: 47px; + right: 8px; + top: 38px; + } +} \ No newline at end of file diff --git a/resources/default-settings.json b/resources/default-settings.json new file mode 100644 index 0000000..bc0af2e --- /dev/null +++ b/resources/default-settings.json @@ -0,0 +1,216 @@ +{ + "info": { + "relay_url": "wss://nostream.your-domain.com", + "name": "nostream.your-domain.com", + "description": "A nostr relay written in Typescript.", + "pubkey": "replace-with-your-pubkey", + "contact": "operator@your-domain.com" + }, + "payments": { + "enabled": false, + "processor": "zebedee", + "feeSchedules": { + "admission": [{ + "enabled": false, + "descripton": "Admission fee charged per public key in msats (1000 msats = 1 satoshi)", + "amount": 1000000, + "whitelists": { + "pubkeys": ["replace-with-your-pubkey"] + } + }], + "publication": [ + { + "enabled": false, + "description": "Publication fee charged per event in msats (1000 msats = 1 satoshi)", + "amount": 10, + "whitelists": { + "pubkeys": ["replace-with-your-pubkey"] + } + } + ] + } + }, + "paymentProcessors": { + "zebedee": { + "baseURL": "https://api.zebedee.io/", + "callbackBaseURL": "https://nostream.your-domain.com/callbacks/zebedee" + } + }, + "network": { + "maxPayloadSize": 131072, + "remoteIpHeader": "x-forwarded-for", + "idleTimeout": 60 + }, + "workers": { + "count": 0 + }, + "limits": { + "invoice": { + "rateLimits": [ + { + "period": 60000, + "rate": 1 + }, + { + "period": 3600000, + "rate": 30 + }, + { + "period": 86400000, + "rate": 360 + } + ], + "ipWhitelist": [ + "::1", + "::ffff:10.10.10.1" + ] + }, + "connection": { + "rateLimits": [ + { + "period": 60000, + "rate": 12 + }, + { + "period": 3600000, + "rate": 360 + }, + { + "period": 86400000, + "rate": 2880 + } + ], + "ipWhitelist": [ + "::1", + "::ffff:10.10.10.1" + ] + }, + "event": { + "eventId": { + "minLeadingZeroBits": 0 + }, + "kind": { + "whitelist": [], + "blacklist": [] + }, + "pubkey": { + "minBalanceMsats": 0, + "minLeadingZeroBits": 0, + "whitelist": [], + "blacklist": [] + }, + "createdAt": { + "maxPositiveDelta": 900, + "maxNegativeDelta": 0 + }, + "content": [ + { + "description": "64 KB for event kind ranges 0-10 and 40-49", + "kinds": [[0, 10], [40, 49]], + "maxLength": 65536 + }, + { + "description": "96 KB for event kind ranges 11-39 and 50-max", + "kinds": [[11, 39], [50, 9007199254740991]], + "maxLength": 98304 + } + ], + "rateLimits": [ + { + "description": "6 events/min for event kinds 0, 3, 40 and 41", + "kinds": [0, 3, 40, 41], + "period": 60000, + "rate": 6 + }, + { + "description": "12 events/min for event kinds 1, 2, 4 and 42", + "kinds": [1, 2, 4, 42], + "period": 60000, + "rate": 12 + }, + { + "description": "360 events/hour for event kinds 1, 2, 4 and 42", + "kinds": [1, 2, 4, 42], + "period": 3600000, + "rate": 360 + }, + { + "description": "30 events/min for event kind ranges 5-7 and 43-49", + "kinds": [[5, 7], [43, 49]], + "period": 60000, + "rate": 30 + }, + { + "description": "24 events/min for replaceable events and parameterized replaceable events", + "kinds": [[10000, 19999], [30000, 39999]], + "period": 60000, + "rate": 24 + }, + { + "description": "60 events/min for ephemeral events", + "kinds": [[20000, 29999]], + "period": 60000, + "rate": 60 + }, + { + "description": "720 events/hour for all events", + "period": 3600000, + "rate": 720 + }, + { + "description": "2880 events/day for all events", + "period": 86400000, + "rate": 2880 + } + ], + "whitelists": { + "pubkeys": [], + "ipAddresses": [ + "::1", + "::ffff:10.10.10.1" + ] + } + }, + "client": { + "subscription": { + "maxSubscriptions": 10, + "maxFilters": 10 + } + }, + "message": { + "rateLimits": [ + { + "description": "60 subscriptions/min", + "types": ["REQ"], + "period": 60000, + "rate": 60 + }, + { + "description": "2880 subscriptions/hour", + "types": ["REQ"], + "period": 3600000, + "rate": 2880 + }, + { + "description": "120 raw messages/min", + "period": 60000, + "rate": 120 + }, + { + "description": "3600 raw messages/hour", + "period": 3600000, + "rate": 3600 + }, + { + "description": "86400 raw messages/day", + "period": 86400000, + "rate": 86400 + } + ], + "ipWhitelist": [ + "::1", + "::ffff:10.10.10.1" + ] + } + } +} diff --git a/resources/favicon.ico b/resources/favicon.ico new file mode 100644 index 0000000..e21d60e Binary files /dev/null and b/resources/favicon.ico differ diff --git a/resources/index.html b/resources/index.html new file mode 100644 index 0000000..a8bb35d --- /dev/null +++ b/resources/index.html @@ -0,0 +1,135 @@ + + + + + + Pay To Relay - {{name}} + + + + + +
+
+
+
+

{{name}}

+
+
+
+
+

+ This Nostr relay requires a one-time admission fee. +

+

+ Provide your public key to generate an invoice. +

+
+
+
+
+
+
+ + +
+ Hex or npub formats accepted. +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+ + + + + diff --git a/resources/invoices.html b/resources/invoices.html new file mode 100644 index 0000000..c33cd2f --- /dev/null +++ b/resources/invoices.html @@ -0,0 +1,151 @@ + + + + + + Pay Invoice - {{name}} + + + + + +
+
+
+

{{name}}

+
+
+
+
+

+ Scan with your bitcoin lightning wallet +

+ +
+
+
+
+
+
+
+
+
+ + copy +
+
+
+
+
+ Waiting for payment +
+ + +
+
+ +
+
+

Invoice expired

+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/resources/terms.html b/resources/terms.html new file mode 100644 index 0000000..587ba55 --- /dev/null +++ b/resources/terms.html @@ -0,0 +1,66 @@ + + + + + + Terms of Service Agreement - {{name}} + + + + +
+

{{name}}

+
+
+

Terms of Service Agreement

+

+ These are the terms of service for {{name}}; please read them before using {{name}}. +

+

+ This service (and supporting services) are provided "as is", without warranty of any kind, express or implied, including but + not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. +

+

+ By connecting to this relay, you agree: +

    +
  • To not engage in spam
  • +
  • To not flood
  • +
  • To not expect content moderation
  • +
  • To not misuse or abuse the relay service and other supporting services
  • +
  • To not disseminate illegal content or material
  • +
  • That requests to delete content you published cannot be guaranteed
  • +
  • That this relay has no control over any content published in other relays
  • +
  • That some services, such as but not limited to the privilege to publish content may require payment(s)
  • +
  • That charge backs from payments may result in the termination of the privilege to use the service
  • +
  • That the service might be revoked to you at the operator's sole discretion if found in violation of these terms
  • +
  • That the terms of service may change at any time in the future without explicit notice
  • +
  • To grant us the necessary rights to your content to provide the service to you and to other users for an unlimited time
  • +
  • To use the service in compliance with all laws, rules, and regulations applicable to you
  • +
  • To use the service in good faith and not seek to get the relay operator(s) in trouble
  • +
  • That the service may throttle, rate limit or revoke your access to any content and/or your privilege to publish content for any reason
  • +
  • That the content you publish to this relay will be further broadcasted to any interested client and/or accepting relay
  • +
  • To not infringe on the right of others to publish content to this relay as allowed by the terms of service
  • +
  • That this service is not targeted, nor intended for use by, anyone under the legal age in their respective jurisdiction
  • +
  • To be of legal age or have sufficient legal constent, permission and capacity to use this service
  • +
  • That the service may be temporarily shutdown or permanently terminated at any time and without notice
  • +
  • That the content published by you and other users may be removed at any time and without notice and for any reason
  • +
  • To have your IP address and/or public key collected for the purpose of detecting abuse, spam or misuse
  • +
  • To have your IP address and/or public key in full, truncated, or as a hash digest shared with interested clients and other accepting relays for the sole purpose of reporting abuse, spam or misuse
  • +
  • To cooperate with the relay and its operators for the purpose of combating abuse, spam or misuse of the service
  • +
+
+ In addition you understand that: +
    +
  • Nostr is a decentralized and distributed network of relays that relays data by users.
  • +
  • Censorship resistance is practiced by posting to multiple relays and running your own private relay.
  • +
  • The responsibility of filtering and moderating is the sole responsibility of the users and not of the relays.
  • +
  • You may be inadvertently exposed to content that you might find triggering, disturbing, distasteful, immoral or against your views.
  • +
  • The relay operator is not liable and has no involvement in the type, quality and legality of the content being produced by users of the relay.
  • +
+

+
+
+
+ + + diff --git a/scripts/stop b/scripts/stop index 38eeb65..7122edf 100755 --- a/scripts/stop +++ b/scripts/stop @@ -2,8 +2,10 @@ PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" DOCKER_COMPOSE_TOR_FILE="${PROJECT_ROOT}/docker-compose.tor.yml" +DOCKER_COMPOSE_LOCAL_FILE="${PROJECT_ROOT}/docker-compose.local.yml" docker compose \ -f $DOCKER_COMPOSE_FILE \ -f $DOCKER_COMPOSE_TOR_FILE \ + -f $DOCKER_COMPOSE_LOCAL_FILE \ down $@ diff --git a/scripts/update b/scripts/update new file mode 100755 index 0000000..0ed3f63 --- /dev/null +++ b/scripts/update @@ -0,0 +1,13 @@ +#!/bin/bash +PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." +SCRIPTS_DIR="${PROJECT_ROOT}/scripts" + +git stash -u + +git pull + +git stash pop + +$SCRIPTS_DIR/stop + +$SCRIPTS_DIR/start diff --git a/settings.sample.json b/settings.sample.json index 87b6594..b0d04f2 100644 --- a/settings.sample.json +++ b/settings.sample.json @@ -7,13 +7,34 @@ "contact": "operator@your-domain.com" }, "network": { - "max_payload_size": 131072, - "remote_ip_header": "x-forwarded-for" + "maxPayloadSize": 131072, + "remoteIpHeader": "x-forwarded-for", + "idleTimeout": 60 }, "workers": { "count": 0 }, "limits": { + "connection": { + "rateLimits": [ + { + "period": 60000, + "rate": 12 + }, + { + "period": 3600000, + "rate": 360 + }, + { + "period": 86400000, + "rate": 2880 + } + ], + "ipWhitelist": [ + "::1", + "::ffff:10.10.10.1" + ] + }, "event": { "eventId": { "minLeadingZeroBits": 0 @@ -23,6 +44,7 @@ "blacklist": [] }, "pubkey": { + "minBalanceMsats": 10000, "minLeadingZeroBits": 0, "whitelist": [], "blacklist": [] @@ -109,4 +131,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/@types/clients.ts b/src/@types/clients.ts new file mode 100644 index 0000000..fb41fb5 --- /dev/null +++ b/src/@types/clients.ts @@ -0,0 +1,19 @@ +export interface InvoiceEnvelope { + bolt11: string +} + +export interface CreateInvoiceResponse { + externalReference: string + amount: number + invoice: InvoiceEnvelope +} + +export interface CreateInvoiceRequest { + amountMsats: number + description?: string + requestId?: string +} + +export interface IPaymentsProcessor { + createInvoice(request: CreateInvoiceRequest): Promise +} diff --git a/src/@types/controllers.ts b/src/@types/controllers.ts new file mode 100644 index 0000000..3820ba9 --- /dev/null +++ b/src/@types/controllers.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express' + +export interface IController { + handleRequest(request: Request, response: Response): Promise +} diff --git a/src/@types/event.ts b/src/@types/event.ts index f3183e8..e87cdb7 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -12,6 +12,10 @@ export interface Event { content: string } +export type UnsignedEvent = Omit + +export type UnidentifiedEvent = Omit + export interface DelegatedEvent extends Event { [EventDelegatorMetadataKey]?: Pubkey } diff --git a/src/@types/invoice.ts b/src/@types/invoice.ts new file mode 100644 index 0000000..2ba4819 --- /dev/null +++ b/src/@types/invoice.ts @@ -0,0 +1,42 @@ +import { Pubkey } from './base' + +export enum InvoiceUnit { + MSATS = 'msats', + SATS = 'sats', + BTC = 'btc' +} + +export enum InvoiceStatus { + PENDING = 'pending', + COMPLETED = 'completed' +} + +export interface Invoice { + id: string + pubkey: Pubkey + bolt11: string + amountRequested: bigint + amountPaid: bigint + unit: InvoiceUnit + status: InvoiceStatus + description: string + confirmedAt: Date + expiresAt: Date + updatedAt: Date + createdAt: Date +} + +export interface DBInvoice { + id: string + pubkey: Buffer + bolt11: string + amount_requested: BigInt + amount_paid: BigInt + unit: InvoiceUnit + status: InvoiceStatus, + description: string + confirmed_at: Date + expires_at: Date + updated_at: Date + created_at: Date +} diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index 6eefc06..4c7bb2c 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -2,6 +2,7 @@ import { PassThrough } from 'stream' import { DBEvent, Event } from './event' import { EventId, Pubkey } from './base' +import { Invoice } from './invoice' import { SubscriptionFilter } from './subscription' export type ExposedPromiseKeys = 'then' | 'catch' | 'finally' @@ -17,3 +18,8 @@ export interface IEventRepository { insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise } + +export interface IInvoiceRepository { + findById(id: string): Promise + upsert(invoice: Invoice): Promise +} diff --git a/src/@types/settings.ts b/src/@types/settings.ts index 7e698c2..b121cc2 100644 --- a/src/@types/settings.ts +++ b/src/@types/settings.ts @@ -1,4 +1,5 @@ import { EventKinds } from '../constants/base' +import { MessageType } from './messages' import { Pubkey } from './base' export interface Info { @@ -10,8 +11,14 @@ export interface Info { } export interface Network { - max_payload_size?: number - remote_ip_header?: string + maxPayloadSize?: number + remoteIpHeader?: string +} + +export interface RateLimit { + description?: string + period: number + rate: number } export interface EventIdLimits { @@ -19,6 +26,7 @@ export interface EventIdLimits { } export interface PubkeyLimits { + minBalanceMsats: number minLeadingZeroBits: number whitelist?: Pubkey[] blacklist?: Pubkey[] @@ -26,10 +34,8 @@ export interface PubkeyLimits { export type EventKindsRange = [EventKinds, EventKinds] -export interface EventRateLimit { +export interface EventRateLimit extends RateLimit { kinds?: (EventKinds | [EventKinds, EventKinds])[] - rate: number - period: number } export interface KindLimits { @@ -49,6 +55,11 @@ export interface CreatedAtLimits { } export interface ContentLimits { + description?: string + kinds?: (EventKinds | EventKindsRange)[] + /** + * Maximum number of characters allowed on events + */ maxLength?: number } @@ -62,7 +73,7 @@ export interface EventLimits { pubkey?: PubkeyLimits kind?: KindLimits createdAt?: CreatedAtLimits - content?: ContentLimits + content?: ContentLimits | ContentLimits[] rateLimits?: EventRateLimit[] whitelists?: EventWhitelists } @@ -76,9 +87,8 @@ export interface ClientLimits { subscription?: ClientSubscriptionLimits } -export interface MessageRateLimit { - rate: number - period: number +export interface MessageRateLimit extends RateLimit { + type?: MessageType } export interface MessageLimits { @@ -86,7 +96,19 @@ export interface MessageLimits { ipWhitelist?: string[] } +export interface ConnectionLimits { + rateLimits: RateLimit[] + ipWhitelist?: string[] +} + +export interface InvoiceLimits { + rateLimits: RateLimit[] + ipWhitelist?: string[] +} + export interface Limits { + invoice?: InvoiceLimits + connection?: ConnectionLimits client?: ClientLimits event?: EventLimits message?: MessageLimits @@ -96,9 +118,42 @@ export interface Worker { count: number } +export interface FeeScheduleWhitelists { + pubkeys?: Pubkey[] +} + +export interface FeeSchedule { + enabled: boolean + description?: string + amount: number + whitelists?: FeeScheduleWhitelists +} + +export interface FeeSchedules { + admission: FeeSchedule[] + publication: FeeSchedule[] +} + +export interface Payments { + enabled: boolean + processor: keyof PaymentProcessors + feeSchedules: FeeSchedules +} + +export interface ZebedeePaymentProcessor { + baseURL: string + callbackBaseURL: string +} + +export interface PaymentProcessors { + zebedee?: ZebedeePaymentProcessor +} + export interface ISettings { info: Info - network?: Network + payments?: Payments + paymentProcessors?: PaymentProcessors + network: Network workers?: Worker limits?: Limits } diff --git a/src/adapters/web-server-adapter.ts b/src/adapters/web-server-adapter.ts index befceed..ee13e78 100644 --- a/src/adapters/web-server-adapter.ts +++ b/src/adapters/web-server-adapter.ts @@ -1,9 +1,12 @@ import { Duplex, EventEmitter } from 'stream' import { IncomingMessage, Server, ServerResponse } from 'http' -import packageJson from '../../package.json' +// import packageJson from '../../package.json' import { createLogger } from '../factories/logger-factory' +import { Factory } from '../@types/base' +import { getRemoteAddress } from '../utils/http' +import { IRateLimiter } from '../@types/utils' import { ISettings } from '../@types/settings' import { IWebServerAdapter } from '../@types/adapters' @@ -12,12 +15,13 @@ const debug = createLogger('web-server-adapter') export class WebServerAdapter extends EventEmitter implements IWebServerAdapter { public constructor( protected readonly webServer: Server, + private readonly slidingWindowRateLimiter: Factory, private readonly settings: () => ISettings, ) { debug('web server starting') super() this.webServer - .on('request', this.onRequest.bind(this)) + //.on('request', this.onRequest.bind(this)) .on('error', this.onError.bind(this)) .on('clientError', this.onClientError.bind(this)) .once('close', this.onClose.bind(this)) @@ -33,31 +37,108 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter debug('listening for incoming connections') } - private onRequest(request: IncomingMessage, response: ServerResponse) { + private async onRequest(request: IncomingMessage, response: ServerResponse) { debug('request received: %O', request.headers) - if (request.method === 'GET' && request.headers['accept'] === 'application/nostr+json') { - const { - info: { name, description, pubkey, contact }, - } = this.settings() - const relayInformationDocument = { - name, - description, - pubkey, - contact, - supported_nips: packageJson.supportedNips, - software: packageJson.repository.url, - version: packageJson.version, - } + const clientAddress = getRemoteAddress(request, this.settings()) - response.setHeader('content-type', 'application/nostr+json') - response.setHeader('access-control-allow-origin', '*') - const body = JSON.stringify(relayInformationDocument) - response.end(body) - } else if (request.headers['upgrade'] !== 'connection') { - response.setHeader('content-type', 'text/plain') - response.end('Please use a Nostr client to connect.') + if (await this.isRateLimited(clientAddress)) { + response.end() } + + // const { + // info: { name, description, pubkey, contact }, + // } = this.settings() + + // try { + // if (request.method === 'GET' && request.headers['accept'] === 'application/nostr+json') { + // const relayInformationDocument = { + // name, + // description, + // pubkey, + // contact, + // supported_nips: packageJson.supportedNips, + // software: packageJson.repository.url, + // version: packageJson.version, + // } + + // response.setHeader('content-type', 'application/nostr+json') + // response.setHeader('access-control-allow-origin', '*') + // const body = JSON.stringify(relayInformationDocument) + // response.end(body) + // } else if (request.headers['upgrade'] !== 'connection') { + // const url = new URL(request.url, `https://${request.headers.host}`) + // if (request.method === 'GET' && url.pathname === '/') { + // response.setHeader('content-type', 'text/html; charset=utf-8') + // response.write('') + // response.write('') + // response.write(`${name}`) + // response.write('') + // response.write('') + // response.write('
') + // response.write('Public key (HEX): ') + // response.write('') + // response.write('') + // response.write('
') + // response.write('') + // response.write('') + // response.end() + // } else if (request.method === 'GET' && url.pathname === '/generate-invoice') { + // response.setHeader('content-type', 'text/html; charset=utf-8') + // response.write('') + // response.write('') + // response.write(`${name}`) + // response.write('') + // response.write('') + // response.write('Invoice ') + // response.write(JSON.stringify(url.searchParams)) + // response.write('') + // response.write('') + // response.end() + // } else { + // response.setHeader('content-type', 'text/plain') + // response.end('Please use a Nostr client to connect.') + // } + // } + // } catch (error) { + // debug('error: %o', error) + // response.statusCode = 500 + // response.end('Internal server error') + // } + } + + private async isRateLimited(client: string): Promise { + const { + rateLimits, + ipWhitelist = [], + } = this.settings().limits?.connection ?? {} + + if (ipWhitelist.includes(client)) { + return false + } + + const rateLimiter = this.slidingWindowRateLimiter() + + const hit = (period: number, rate: number) => + rateLimiter.hit( + `${client}:connection:${period}`, + 1, + { period: period, rate: rate }, + ) + + let limited = false + for (const { rate, period } of rateLimits) { + const isRateLimited = await hit(period, rate) + + + if (isRateLimited) { + debug('rate limited %s: %d messages / %d ms exceeded', client, rate, period) + + limited = true + } + } + + return limited } private onError(error: Error) { diff --git a/src/adapters/web-socket-adapter.ts b/src/adapters/web-socket-adapter.ts index 371b940..184ad5d 100644 --- a/src/adapters/web-socket-adapter.ts +++ b/src/adapters/web-socket-adapter.ts @@ -13,6 +13,7 @@ import { attemptValidation } from '../utils/validation' import { createLogger } from '../factories/logger-factory' import { Event } from '../@types/event' import { Factory } from '../@types/base' +import { getRemoteAddress } from '../utils/http' import { IRateLimiter } from '../@types/utils' import { ISettings } from '../@types/settings' import { isEventMatchingFilter } from '../utils/event' @@ -42,8 +43,8 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter this.subscriptions = new Map() this.clientId = Buffer.from(this.request.headers['sec-websocket-key'] as string, 'base64').toString('hex') - const remoteIpHeader = this.settings().network?.remote_ip_header ?? 'x-forwarded-for' - this.clientAddress = (this.request.headers[remoteIpHeader] ?? this.request.socket.remoteAddress) as string + + this.clientAddress = getRemoteAddress(this.request, this.settings()) this.client .on('message', this.onClientMessage.bind(this)) @@ -203,10 +204,10 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter rateLimiter.hit( `${client}:message:${period}`, 1, - { period: period, rate: rate }, + { period, rate }, ) - + let limited = false for (const { rate, period } of rateLimits) { const isRateLimited = await hit(period, rate) @@ -214,11 +215,11 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter if (isRateLimited) { debug('rate limited %s: %d messages / %d ms exceeded', client, rate, period) - return true + limited = true } } - return false + return limited } private onClientPong() { diff --git a/src/adapters/web-socket-server-adapter.ts b/src/adapters/web-socket-server-adapter.ts index 977dc02..5c416c2 100644 --- a/src/adapters/web-socket-server-adapter.ts +++ b/src/adapters/web-socket-server-adapter.ts @@ -6,6 +6,7 @@ import { WebSocketAdapterEvent, WebSocketServerAdapterEvent } from '../constants import { createLogger } from '../factories/logger-factory' import { Event } from '../@types/event' import { Factory } from '../@types/base' +import { IRateLimiter } from '../@types/utils' import { ISettings } from '../@types/settings' import { propEq } from 'ramda' import { WebServerAdapter } from './web-server-adapter' @@ -26,9 +27,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock IWebSocketAdapter, [WebSocket, IncomingMessage, IWebSocketServerAdapter] >, + slidingWindowRateLimiter: Factory, settings: () => ISettings, ) { - super(webServer, settings) + super(webServer, slidingWindowRateLimiter, settings) this.webSocketsAdapters = new WeakMap() diff --git a/src/constants/base.ts b/src/constants/base.ts index d36e847..7a4582e 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -14,6 +14,9 @@ export enum EventKinds { CHANNEL_MUTE_USER = 44, CHANNEL_RESERVED_FIRST = 45, CHANNEL_RESERVED_LAST = 49, + // Relay-only + RELAY_INVITE = 50, + INVOICE_UPDATE = 402, // Replaceable events REPLACEABLE_FIRST = 10000, REPLACEABLE_LAST = 19999, @@ -35,5 +38,9 @@ export enum EventTags { Deduplication = 'd', } +export enum PaymentsProcessors { + ZEBEDEE = 'zebedee', +} + export const EventDelegatorMetadataKey = Symbol('Delegator') export const EventDeduplicationMetadataKey = Symbol('Deduplication') diff --git a/src/controllers/callbacks/zebedee-callback-controller.ts b/src/controllers/callbacks/zebedee-callback-controller.ts new file mode 100644 index 0000000..c8f47f0 --- /dev/null +++ b/src/controllers/callbacks/zebedee-callback-controller.ts @@ -0,0 +1,95 @@ +import { andThen, pipe } from 'ramda' +import { Request, Response } from 'express' +import cluster from 'cluster' + +import { Event, UnidentifiedEvent } from '../../@types/event' +import { EventKinds, PaymentsProcessors } from '../../constants/base' +import { getPrivateKeyFromSecret, getPublicKey, identifyEvent, signEvent } from '../../utils/event' +import { IEventRepository, IInvoiceRepository } from '../../@types/repositories' +import { InvoiceStatus, InvoiceUnit } from '../../@types/invoice' +import { createLogger } from '../../factories/logger-factory' +import { fromZebedeeInvoice } from '../../utils/transform' +import { IController } from '../../@types/controllers' +import { WebSocketServerAdapterEvent } from '../../constants/adapter' + +const debug = createLogger('zebedee-callback-controller') + +export class ZebedeeCallbackController implements IController { + public constructor( + private readonly invoiceRepository: IInvoiceRepository, + private readonly eventRepository: IEventRepository, + ) {} + + // TODO: Validate + public async handleRequest( + request: Request, + response: Response, + ) { + debug('request body: %o', request.body) + + const invoice = fromZebedeeInvoice(request.body) + + try { + await this.invoiceRepository.upsert(invoice) + } catch (error) { + console.error('Unable to persist invoice:', invoice.bolt11) + + throw error + } + + if (invoice.status !== InvoiceStatus.COMPLETED) { + response + .status(200) + .send() + + return + } + + // Generate deterministic private key for given pubkey + const privkey = getPrivateKeyFromSecret(process.env.SECRET)(invoice.pubkey) + const pubkey = getPublicKey(privkey) + + const amountPaid = (invoice.unit === InvoiceUnit.MSATS) ? invoice.amountPaid / 1000n : invoice.amountPaid + + const newEvent: UnidentifiedEvent = { + pubkey, + kind: EventKinds.INVOICE_UPDATE, + created_at: Math.floor(invoice.confirmedAt.getTime() / 1000), + content: `✅ ${amountPaid.toString()} ${(invoice.unit === InvoiceUnit.BTC) ? 'BTC' : 'sats'} received`, + tags: [ + ['p', invoice.pubkey], + ['status', invoice.status], + ['payments-processor', PaymentsProcessors.ZEBEDEE], + ['r', `lightning:${invoice.bolt11}`], + ], + } + + const event: Event = await pipe( + identifyEvent, + andThen(signEvent(privkey)), + )(newEvent) + + try { + await this.eventRepository.create(event) + } catch (error) { + response.status(500).send(`Unable to save event for invoice: ${invoice.bolt11}`) + return + } finally { + this.broadcastEvent(event) + } + + response + .status(200) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('OK') + } + + private broadcastEvent(event: Event) { + if (cluster.isWorker) { + process.send({ + eventName: WebSocketServerAdapterEvent.Broadcast, + event, + }) + } + } +} diff --git a/src/factories/payments-processor-factory.ts b/src/factories/payments-processor-factory.ts new file mode 100644 index 0000000..2a48dba --- /dev/null +++ b/src/factories/payments-processor-factory.ts @@ -0,0 +1,35 @@ +import axios from 'axios' + +import { createSettings } from './settings-factory' +import { ISettings } from '../@types/settings' +import { NullPaymentsProcessor } from '../payments-processors/null-payments-processor' +import { PaymentsProcessor } from '../payments-processors/payments-procesor' +import { ZebedeePaymentsProcesor } from '../payments-processors/zebedee-payments-processor' + +const createZebedeePaymentsProcessor = (settings: ISettings) => { + const client = axios.create({ + headers: { + 'content-type': 'application/json', + 'apikey': process.env.ZEBEDEE_API_KEY, + }, + baseURL: settings.paymentProcessors.zebedee.baseURL, + maxRedirects: 1, + }) + + const zpp = new ZebedeePaymentsProcesor(client, createSettings) + return new PaymentsProcessor(zpp) +} + +export const createPaymentsProcessor = () => { + const settings = createSettings() + if (!settings.payments.enabled) { + throw new Error('Payments disabled') + } + + switch (settings.payments.processor) { + case 'zebedee': + return createZebedeePaymentsProcessor(settings) + default: + return new NullPaymentsProcessor() + } +} \ No newline at end of file diff --git a/src/factories/worker-factory.ts b/src/factories/worker-factory.ts index 8b7d54f..cc3ba11 100644 --- a/src/factories/worker-factory.ts +++ b/src/factories/worker-factory.ts @@ -1,3 +1,5 @@ +import express from 'express' +import helmet from 'helmet' import http from 'http' import process from 'process' import { WebSocketServer } from 'ws' @@ -6,6 +8,8 @@ import { getMasterDbClient, getReadReplicaDbClient } from '../database/client' import { AppWorker } from '../app/worker' import { createSettings } from '../factories/settings-factory' import { EventRepository } from '../repositories/event-repository' +import router from '../routes' +import { slidingWindowRateLimiterFactory } from './rate-limiter-factory' import { webSocketAdapterFactory } from './websocket-adapter-factory' import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter' @@ -14,16 +18,47 @@ export const workerFactory = (): AppWorker => { const readReplicaDbClient = getReadReplicaDbClient() const eventRepository = new EventRepository(dbClient, readReplicaDbClient) + const app = express() + app + .disable('x-powered-by') + .use( helmet.contentSecurityPolicy({ + directives: { + /** + * TODO: Remove 'unsafe-inline' + */ + 'script-src-attr': ["'unsafe-inline'"], + 'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net/npm/', 'https://unpkg.com/', 'https://cdnjs.cloudflare.com/ajax/libs/'], + 'style-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'], + 'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'], + }, + })) + .use('/favicon.ico', express.static('./resources/favicon.ico')) + .use('/css', express.static('./resources/css')) + .use(router) + // deepcode ignore HttpToHttps: we use proxies - const server = http.createServer() + const server = http.createServer(app) + + const settings = createSettings() + + let maxPayloadSize: number | undefined + if (settings.network['max_payload_size']) { + console.warn(`WARNING: Setting network.max_payload_size is deprecated and will be removed in a future version. + Use network.maxPayloadSize instead.`) + maxPayloadSize = settings.network['max_payload_size'] + } else { + maxPayloadSize = settings.network.maxPayloadSize + } + const webSocketServer = new WebSocketServer({ server, - maxPayload: createSettings().network?.max_payload_size ?? 131072, // 128 kB + maxPayload: maxPayloadSize ?? 131072, // 128 kB }) const adapter = new WebSocketServerAdapter( server, webSocketServer, webSocketAdapterFactory(eventRepository), + slidingWindowRateLimiterFactory, createSettings, ) diff --git a/src/factories/zebedee-callback-controller-factory.ts b/src/factories/zebedee-callback-controller-factory.ts new file mode 100644 index 0000000..a499a3d --- /dev/null +++ b/src/factories/zebedee-callback-controller-factory.ts @@ -0,0 +1,16 @@ +import { EventRepository } from '../repositories/event-repository' +import { getDbClient } from '../database/client' +import { IController } from '../@types/controllers' +import { InvoiceRepository } from '../repositories/invoice-repository' +import { ZebedeeCallbackController } from '../controllers/callbacks/zebedee-callback-controller' + +export const createZebedeeCallbackController = (): IController => { + const dbClient = getDbClient() + const eventRepository = new EventRepository(dbClient) + const invoiceRepotistory = new InvoiceRepository(dbClient) + + return new ZebedeeCallbackController( + invoiceRepotistory, + eventRepository, + ) +} diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index 61cf0f2..1f46a96 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -1,10 +1,9 @@ -import { EventKindsRange, EventRateLimit, ISettings } from '../@types/settings' -import { getEventProofOfWork, getPubkeyProofOfWork, isEventIdValid, isEventSignatureValid } from '../utils/event' +import { EventRateLimit, ISettings } from '../@types/settings' +import { getEventProofOfWork, getPubkeyProofOfWork, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid } from '../utils/event' import { IEventStrategy, IMessageHandler } from '../@types/message-handlers' import { createCommandResult } from '../utils/messages' import { createLogger } from '../factories/logger-factory' import { Event } from '../@types/event' -import { EventKinds } from '../constants/base' import { Factory } from '../@types/base' import { IncomingEventMessage } from '../@types/messages' import { IRateLimiter } from '../@types/utils' @@ -61,27 +60,63 @@ export class EventMessageHandler implements IMessageHandler { protected canAcceptEvent(event: Event): string | undefined { const now = Math.floor(Date.now()/1000) - const limits = this.settings().limits.event - if (limits.content.maxLength > 0 && event.content.length > limits.content.maxLength) { + + const limits = this.settings().limits?.event ?? {} + + if (Array.isArray(limits.content)) { + for (const limit of limits.content) { + if ( + typeof limit.maxLength !== 'undefined' + && limit.maxLength > 0 + && event.content.length > limit.maxLength + && ( + !Array.isArray(limit.kinds) + || limit.kinds.some(isEventKindOrRangeMatch(event)) + ) + ) { + return `rejected: content is longer than ${limit.maxLength} bytes` + } + } + } else if ( + typeof limits.content?.maxLength !== 'undefined' + && limits.content?.maxLength > 0 + && event.content.length > limits.content.maxLength + && ( + !Array.isArray(limits.content.kinds) + || limits.content.kinds.some(isEventKindOrRangeMatch(event)) + ) + ) { return `rejected: content is longer than ${limits.content.maxLength} bytes` } - if (limits.createdAt.maxPositiveDelta > 0 && event.created_at > now + limits.createdAt.maxPositiveDelta) { + if ( + typeof limits.createdAt?.maxPositiveDelta !== 'undefined' + && limits.createdAt.maxPositiveDelta > 0 + && event.created_at > now + limits.createdAt.maxPositiveDelta) { return `rejected: created_at is more than ${limits.createdAt.maxPositiveDelta} seconds in the future` } - if (limits.createdAt.maxNegativeDelta > 0 && event.created_at < now - limits.createdAt.maxNegativeDelta) { + if ( + typeof limits.createdAt?.maxNegativeDelta !== 'undefined' + && limits.createdAt.maxNegativeDelta > 0 + && event.created_at < now - limits.createdAt.maxNegativeDelta) { return `rejected: created_at is more than ${limits.createdAt.maxNegativeDelta} seconds in the past` } - if (limits.eventId.minLeadingZeroBits > 0) { + if ( + typeof limits.eventId?.minLeadingZeroBits !== 'undefined' + && limits.eventId.minLeadingZeroBits > 0 + ) { const pow = getEventProofOfWork(event.id) if (pow < limits.eventId.minLeadingZeroBits) { return `pow: difficulty ${pow}<${limits.eventId.minLeadingZeroBits}` } } - if (limits.pubkey.minLeadingZeroBits > 0) { + if ( + typeof limits.pubkey?.minLeadingZeroBits !== 'undefined' + && limits.pubkey.minLeadingZeroBits > 0 + ) { const pow = getPubkeyProofOfWork(event.pubkey) if (pow < limits.pubkey.minLeadingZeroBits) { return `pow: pubkey difficulty ${pow}<${limits.pubkey.minLeadingZeroBits}` @@ -89,29 +124,32 @@ export class EventMessageHandler implements IMessageHandler { } if ( - limits.pubkey.whitelist.length > 0 + typeof limits.pubkey?.whitelist !== 'undefined' + && limits.pubkey.whitelist.length > 0 && !limits.pubkey.whitelist.some((prefix) => event.pubkey.startsWith(prefix)) ) { return 'blocked: pubkey not allowed' } if ( - limits.pubkey.blacklist.length > 0 + typeof limits.pubkey?.blacklist !== 'undefined' + && limits.pubkey.blacklist.length > 0 && limits.pubkey.blacklist.some((prefix) => event.pubkey.startsWith(prefix)) ) { return 'blocked: pubkey not allowed' } - const isEventKindMatch = (item: EventKinds | EventKindsRange) => - typeof item === 'number' - ? item === event.kind - : event.kind >= item[0] && event.kind <= item[1] - - if (limits.kind.whitelist.length > 0 && !limits.kind.whitelist.some(isEventKindMatch)) { + if ( + typeof limits.kind?.whitelist !== 'undefined' + && limits.kind.whitelist.length > 0 + && !limits.kind.whitelist.some(isEventKindOrRangeMatch(event))) { return `blocked: event kind ${event.kind} not allowed` } - if (limits.kind.blacklist.length > 0 && limits.kind.blacklist.some(isEventKindMatch)) { + if ( + typeof limits.kind?.blacklist !== 'undefined' + && limits.kind.blacklist.length > 0 + && limits.kind.blacklist.some(isEventKindOrRangeMatch(event))) { return `blocked: event kind ${event.kind} not allowed` } } @@ -132,13 +170,16 @@ export class EventMessageHandler implements IMessageHandler { } if ( - Array.isArray(whitelists?.pubkeys) + typeof whitelists?.pubkeys !== 'undefined' + && Array.isArray(whitelists?.pubkeys) && whitelists.pubkeys.includes(event.pubkey) ) { return false } - if (Array.isArray(whitelists?.ipAddresses) + if ( + typeof whitelists?.ipAddresses !== 'undefined' + && Array.isArray(whitelists?.ipAddresses) && whitelists.ipAddresses.includes(this.webSocket.getClientAddress()) ) { return false @@ -162,10 +203,22 @@ export class EventMessageHandler implements IMessageHandler { ) } - const hits = await Promise.all(rateLimits.map(hit)) + let limited = false + for (const { rate, period, kinds } of rateLimits) { + // skip if event kind does not apply + if (Array.isArray(kinds) && !kinds.some(isEventKindOrRangeMatch(event))) { + continue + } - debug('rate limit check %s: %o', event.pubkey, hits) + const isRateLimited = await hit({ period, rate, kinds }) - return hits.some((active) => active) + if (isRateLimited) { + debug('rate limited %s: %d events / %d ms exceeded', event.pubkey, rate, period) + + limited = true + } + } + + return limited } } diff --git a/src/handlers/request-handlers/get-invoice-request-handler.ts b/src/handlers/request-handlers/get-invoice-request-handler.ts new file mode 100644 index 0000000..14a6f4c --- /dev/null +++ b/src/handlers/request-handlers/get-invoice-request-handler.ts @@ -0,0 +1,17 @@ +import { NextFunction, Request, Response } from 'express' +import { readFileSync } from 'fs' + +import { createSettings as settings } from '../../factories/settings-factory' + +let pageCache: string + +export const getInvoiceRequestHandler = (_req: Request, res: Response, next: NextFunction) => { + const { info: { name } } = settings() + + if (!pageCache) { + pageCache = readFileSync('./resources/index.html', 'utf8').replaceAll('{{name}}', name) + } + + res.status(200).setHeader('content-type', 'text/html; charset=utf8').send(pageCache) + next() +} diff --git a/src/handlers/request-handlers/get-terms-request-handler.ts b/src/handlers/request-handlers/get-terms-request-handler.ts new file mode 100644 index 0000000..c7c59cd --- /dev/null +++ b/src/handlers/request-handlers/get-terms-request-handler.ts @@ -0,0 +1,17 @@ +import { NextFunction, Request, Response } from 'express' +import { readFileSync } from 'fs' + +import { createSettings as settings } from '../../factories/settings-factory' + +let pageCache: string + +export const getTermsRequestHandler = (_req: Request, res: Response, next: NextFunction) => { + const { info: { name } } = settings() + + if (!pageCache) { + pageCache = readFileSync('./resources/terms.html', 'utf8').replaceAll('{{name}}', name) + } + + res.status(200).setHeader('content-type', 'text/html; charset=utf8').send(pageCache) + next() +} diff --git a/src/handlers/request-handlers/post-invoice-request-handler.ts b/src/handlers/request-handlers/post-invoice-request-handler.ts new file mode 100644 index 0000000..fce1e50 --- /dev/null +++ b/src/handlers/request-handlers/post-invoice-request-handler.ts @@ -0,0 +1,147 @@ +import { NextFunction, Request, Response } from 'express' +import { readFileSync } from 'fs' + +import { getPrivateKeyFromSecret, getPublicKey } from '../../utils/event' +import { createLogger } from '../../factories/logger-factory' +import { fromNpub } from '../../utils/transform' +import { getRemoteAddress } from '../../utils/http' +import { IPaymentsProcessor } from '../../@types/clients' +import { createSettings as settings } from '../../factories/settings-factory' +import { slidingWindowRateLimiterFactory } from '../../factories/rate-limiter-factory' + +let pageCache: string + +const debug = createLogger('post-invoice-request-handler') + +// deepcode ignore NoRateLimitingForExpensiveWebOperation: only read once +export const postInvoiceRequestHandler = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + if (!pageCache) { + pageCache = readFileSync('./resources/invoices.html', 'utf8') + } + + debug('params: %o', req.params) + debug('body: %o', req.body) + + const currentSettings = settings() + + const { + info: { name, relay_url }, + limits: { invoice: { ipWhitelist, rateLimits } }, + } = currentSettings + + const remoteAddress = getRemoteAddress(req, currentSettings) + + const isRateLimited = async (remoteAddress: string) => { + let limited = false + if (!ipWhitelist.includes(remoteAddress)) { + const rateLimiter = slidingWindowRateLimiterFactory() + for (const { rate, period } of rateLimits) { + if (await rateLimiter.hit(`${remoteAddress}:invoice:${period}`, 1, { period, rate })) { + debug('rate limited %s: %d in %d milliseconds', remoteAddress, rate, period) + limited = true + } + } + } + return limited + } + + const limited = await isRateLimited(remoteAddress) + if (limited) { + res + .status(429) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('Too many requests') + return next() + } + + const tosAccepted = req.body?.tosAccepted === 'yes' + + if (!tosAccepted) { + res + .status(400) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('ToS agreement: not accepted') + + return next() + } + + const pubkeyRaw = typeof req.body?.pubkey === 'string' + ? req.body?.pubkey?.trim() + : undefined + + let pubkey: string + if (typeof pubkeyRaw !== 'string') { + res + .status(400) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('Invalid pubkey: missing') + + return next() + } else if (/^[0-9a-f]{64}$/.test(pubkeyRaw)) { + pubkey = pubkeyRaw + } else if (/^npub/.test(pubkeyRaw)) { + try { + pubkey = fromNpub(pubkeyRaw) + } catch (error) { + res + .status(400) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('Invalid pubkey: npub not valid') + + return next() + } + } else { + res + .status(400) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('Invalid pubkey: unknown format') + + return next() + } + + const admissionFee = currentSettings.payments?.feeSchedules.admission + .filter((feeSchedule) => feeSchedule.enabled && !feeSchedule.whitelists.pubkeys.includes(pubkey)) + + if (!Array.isArray(admissionFee) || !admissionFee.length) { + res + .status(400) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('No admission fee required') + + return next() + } + + const paymentsProcessor = req['paymentsProcessor'] as IPaymentsProcessor + + const invoiceResponse = await paymentsProcessor.createInvoice({ + amountMsats: admissionFee.reduce((sum, fee) => sum + fee.amount, 0), + description: `Admission Fee for ${pubkey}`, + requestId: pubkey, + }) + + const privkey = getPrivateKeyFromSecret(process.env.SECRET)(pubkey) + const relayPubkey = getPublicKey(privkey) + + const replacements = { + name, + pubkey, + relay_url, + relay_pubkey: relayPubkey, + invoice: invoiceResponse.invoice.bolt11, + } + + const body = Object + .entries(replacements) + .reduce((body, [key, value]) => body.replaceAll(`{{${key}}}`, value), pageCache) + + res + .status(200) + .setHeader('Content-Type', 'text/html; charset=utf8') + .send(body) + + return next() +} diff --git a/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts b/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts new file mode 100644 index 0000000..62ede18 --- /dev/null +++ b/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts @@ -0,0 +1,11 @@ +import { Request, Response } from 'express' +import { createZebedeeCallbackController } from '../../factories/zebedee-callback-controller-factory' + +export const postZebedeeCallbackRequestHandler = async ( + req: Request, + res: Response, +) => { + const controller = createZebedeeCallbackController() + + return controller.handleRequest(req, res) +} diff --git a/src/handlers/request-handlers/root-request-handler.ts b/src/handlers/request-handlers/root-request-handler.ts new file mode 100644 index 0000000..1bbbabb --- /dev/null +++ b/src/handlers/request-handlers/root-request-handler.ts @@ -0,0 +1,6 @@ +import { NextFunction, Request, Response } from 'express' + +export const rootRequestHandler = (_req: Request, res: Response, next: NextFunction) => { + res.redirect(301, '/invoices') + next() +} diff --git a/src/payments-processors/null-payments-processor.ts b/src/payments-processors/null-payments-processor.ts new file mode 100644 index 0000000..1256ee1 --- /dev/null +++ b/src/payments-processors/null-payments-processor.ts @@ -0,0 +1,13 @@ +import { CreateInvoiceRequest, CreateInvoiceResponse, IPaymentsProcessor } from '../@types/clients' + +export class NullPaymentsProcessor implements IPaymentsProcessor { + public async createInvoice(_request: CreateInvoiceRequest): Promise { + return { + amount: 0, + externalReference: '', + invoice: { + bolt11: '', + }, + } + } +} diff --git a/src/payments-processors/payments-procesor.ts b/src/payments-processors/payments-procesor.ts new file mode 100644 index 0000000..391832a --- /dev/null +++ b/src/payments-processors/payments-procesor.ts @@ -0,0 +1,11 @@ +import { CreateInvoiceRequest, CreateInvoiceResponse, IPaymentsProcessor } from '../@types/clients' + +export class PaymentsProcessor implements IPaymentsProcessor { + public constructor( + private readonly processor: IPaymentsProcessor + ) {} + + public async createInvoice(request: CreateInvoiceRequest): Promise { + return this.processor.createInvoice(request) + } +} diff --git a/src/payments-processors/zebedee-payments-processor.ts b/src/payments-processors/zebedee-payments-processor.ts new file mode 100644 index 0000000..298509c --- /dev/null +++ b/src/payments-processors/zebedee-payments-processor.ts @@ -0,0 +1,59 @@ +import { applySpec, path, pipe } from 'ramda' +import { AxiosInstance } from 'axios' +import { Factory } from '../@types/base' + +import { CreateInvoiceRequest, CreateInvoiceResponse, IPaymentsProcessor } from '../@types/clients' +import { createLogger } from '../factories/logger-factory' +import { ISettings } from '../@types/settings' +import { toJSON } from '../utils/transform' + +const debug = createLogger('zebedee-payments-processor') + +export class ZebedeePaymentsProcesor implements IPaymentsProcessor { + public constructor( + private httpClient: AxiosInstance, + private settings: Factory + ) {} + + public async createInvoice(request: CreateInvoiceRequest): Promise { + debug('create invoice: %o', request) + const { + amountMsats, + description, + requestId, + } = request + + const body = { + amount: amountMsats.toString(), + description, + internalId: requestId, + callbackUrl: this.settings().paymentProcessors?.zebedee?.callbackBaseURL, + } + + try { + debug('request body: %o', body) + const response = await this.httpClient.post('/v0/charges', body, { + maxRedirects: 1, + }) + + const result = pipe( + applySpec({ + externalReference: path(['data', 'id']), + amount: pipe(path(['data', 'amount']), Number), + invoice: applySpec({ + bolt11: path(['data', 'invoice', 'request']), + }), + rawResponse: toJSON, + }) + )(response.data) + + debug('result: %o', result) + + return result + } catch (error) { + console.error('Unable to request invoice. Reason:', error.message) + + throw error + } + } +} diff --git a/src/repositories/invoice-repository.ts b/src/repositories/invoice-repository.ts new file mode 100644 index 0000000..faba9aa --- /dev/null +++ b/src/repositories/invoice-repository.ts @@ -0,0 +1,70 @@ +import { + applySpec, + is, + omit, + pipe, + prop, + propSatisfies, + toString, + when, +} from 'ramda' + +import { DBInvoice, Invoice } from '../@types/invoice' +import { fromDBInvoice, toBuffer } from '../utils/transform' +import { createLogger } from '../factories/logger-factory' +import { DatabaseClient } from '../@types/base' +import { IInvoiceRepository } from '../@types/repositories' + +const debug = createLogger('invoice-repository') + +export class InvoiceRepository implements IInvoiceRepository { + public constructor(private readonly dbClient: DatabaseClient) { } + + public async findById(id: string): Promise { + const [dbInvoice] = await this.dbClient('invoices').where('id', id).select() + + if (!dbInvoice) { + return + } + + return fromDBInvoice(dbInvoice) + } + + public upsert(invoice: Invoice): Promise { + debug('upserting invoice: %o', invoice) + + const row = applySpec({ + id: when(propSatisfies(is(String), 'id'), prop('id')), + pubkey: pipe(prop('pubkey'), toBuffer), + amount_requested: pipe(prop('amountRequested'), toString), + amount_paid: when(propSatisfies(is(BigInt), 'amountPaid'), pipe(prop('amountPaid'), toString)), + unit: prop('unit'), + status: prop('status'), + description: prop('description'), + confirmed_at: prop('confirmedAt'), + expires_at: prop('expiresAt'), + })(invoice) + + const query = this.dbClient('invoices') + .insert(row) + .onConflict('id') + .merge( + omit([ + 'id', + 'pubkey', + 'bolt11', + 'amount_requested', + 'unit', + 'description', + 'expires_at', + 'created_at', + ])(row) + ) + + return { + then: (onfulfilled: (value: number) => T1 | PromiseLike, onrejected: (reason: any) => T2 | PromiseLike) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), + catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), + toString: (): string => query.toString(), + } as Promise + } +} diff --git a/src/routes/callbacks/index.ts b/src/routes/callbacks/index.ts new file mode 100644 index 0000000..5397856 --- /dev/null +++ b/src/routes/callbacks/index.ts @@ -0,0 +1,10 @@ +import { json, Router } from 'express' + +import { postZebedeeCallbackRequestHandler } from '../../handlers/request-handlers/post-zebedee-callback-request-handler' + +const router = Router() + +router.post('/zebedee', json(), postZebedeeCallbackRequestHandler) + +export default router + diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..8b86484 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,17 @@ +import express from 'express' + +import callbacksRouter from './callbacks' +import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler' +import invoiceRouter from './invoices' +import { rootRequestHandler } from '../handlers/request-handlers/root-request-handler' + +const router = express.Router() + +router.get('/', rootRequestHandler) +router.get('/terms', getTermsRequestHandler) + +router.use('/invoices', invoiceRouter) +router.use('/callbacks', callbacksRouter) + + +export default router diff --git a/src/routes/invoices/index.ts b/src/routes/invoices/index.ts new file mode 100644 index 0000000..56c65d3 --- /dev/null +++ b/src/routes/invoices/index.ts @@ -0,0 +1,11 @@ +import { Router, urlencoded } from 'express' + +import { getInvoiceRequestHandler } from '../../handlers/request-handlers/get-invoice-request-handler' +import { postInvoiceRequestHandler } from '../../handlers/request-handlers/post-invoice-request-handler' + +const invoiceRouter = Router() + +invoiceRouter.get('/', getInvoiceRequestHandler) +invoiceRouter.post('/', urlencoded({ extended: true }), postInvoiceRequestHandler) + +export default invoiceRouter diff --git a/src/schemas/http-request-schemas.ts b/src/schemas/http-request-schemas.ts new file mode 100644 index 0000000..f2bd502 --- /dev/null +++ b/src/schemas/http-request-schemas.ts @@ -0,0 +1,9 @@ +import Schema from 'joi' + +import { pubkeySchema } from './base-schema' + + +export const generateInvoiceSchema = Schema.object({ + pubkey: pubkeySchema.required(), + tosAccepted: Schema.valid('yes').required(), +}).unknown(false) diff --git a/src/utils/event.ts b/src/utils/event.ts index c504b3b..a8a0e4f 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -1,16 +1,18 @@ import * as secp256k1 from '@noble/secp256k1' import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda' +import { createHmac } from 'crypto' -import { CanonicalEvent, DBEvent, Event } from '../@types/event' +import { CanonicalEvent, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event' import { EventId, Pubkey, Tag } from '../@types/base' import { EventKinds, EventTags } from '../constants/base' +import { EventKindsRange } from '../@types/settings' import { fromBuffer } from './transform' import { getLeadingZeroBits } from './proof-of-work' import { isGenericTagQuery } from './filter' import { RuneLike } from './runes/rune-like' import { SubscriptionFilter } from '../@types/subscription' -export const serializeEvent = (event: Event): CanonicalEvent => [ +export const serializeEvent = (event: UnidentifiedEvent): CanonicalEvent => [ 0, event.pubkey, event.created_at, @@ -29,6 +31,12 @@ export const toNostrEvent: (event: DBEvent) => Event = applySpec({ sig: pipe(prop('event_signature') as () => Buffer, fromBuffer), }) +export const isEventKindOrRangeMatch = ({ kind }: Event) => + (item: EventKinds | EventKindsRange) => + typeof item === 'number' + ? item === kind + : kind >= item[0] && kind <= item[1] + export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Event): boolean => { const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix) @@ -149,16 +157,42 @@ export const isDelegatedEventValid = async (event: Event): Promise => { return secp256k1.schnorr.verify(delegation[3], token, delegation[1]) } -export const isEventIdValid = async (event: Event): Promise => { +export const getEventHash = async (event: Event | UnidentifiedEvent | UnsignedEvent): Promise => { const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event)))) - return Buffer.from(id).toString('hex') === event.id + return Buffer.from( + id + ).toString('hex') +} + +export const isEventIdValid = async (event: Event): Promise => { + return event.id === await getEventHash(event) } export const isEventSignatureValid = async (event: Event): Promise => { return secp256k1.schnorr.verify(event.sig, event.id, event.pubkey) } +export const identifyEvent = async (event: UnidentifiedEvent): Promise => { + const id = await getEventHash(event) + + return { ...event, id } +} + +export const getPrivateKeyFromSecret = + (secret: string) => (publicKey: Pubkey | Buffer): string => { + const hmac = createHmac('sha256', secret) + hmac.update(typeof publicKey === 'string' ? Buffer.from(publicKey, 'hex') : publicKey) + return hmac.digest().toString('hex') +} + +export const getPublicKey = (privkey: string | Buffer) => Buffer.from(secp256k1.getPublicKey(privkey, true)).toString('hex').substring(2) + +export const signEvent = (privkey: string | Buffer | undefined) => async (event: UnsignedEvent): Promise => { + const sig = await secp256k1.schnorr.sign(event.id, privkey) + return { ...event, sig: Buffer.from(sig).toString('hex') } +} + export const isReplaceableEvent = (event: Event): boolean => { return event.kind === EventKinds.SET_METADATA || event.kind === EventKinds.CONTACT_LIST diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 0000000..a9bb97a --- /dev/null +++ b/src/utils/http.ts @@ -0,0 +1,17 @@ +import { IncomingMessage } from 'http' + +import { ISettings } from '../@types/settings' + +export const getRemoteAddress = (request: IncomingMessage, settings: ISettings): string => { + let header: string | undefined + // TODO: Remove deprecation warning + if ('network' in settings && 'remote_ip_header' in settings.network) { + console.warn(`WARNING: Setting network.remote_ip_header is deprecated and will be removed in a future version. + Use network.remoteIpHeader instead.`) + header = settings.network['remote_ip_header'] as string + } else { + header = settings.network.remoteIpHeader as string + } + + return (request.headers[header] ?? request.socket.remoteAddress) as string +} diff --git a/src/utils/settings.ts b/src/utils/settings.ts index ebfd3ba..88d00b1 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -4,8 +4,8 @@ import { join } from 'path' import { mergeDeepRight } from 'ramda' import { createLogger } from '../factories/logger-factory' +import defaultSettingsJson from '../../resources/default-settings.json' import { ISettings } from '../@types/settings' -import settingsSampleJson from '../../settings.sample.json' const debug = createLogger('settings') @@ -35,7 +35,7 @@ export class SettingsStatic { } debug('creating settings') const path = SettingsStatic.getSettingsFilePath() - const defaults = settingsSampleJson as ISettings + const defaults = defaultSettingsJson as ISettings try { if (fs.existsSync(path)) { diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 71aed3b..057ecc4 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -1,5 +1,67 @@ +import { applySpec, is, path, pathEq, pipe, prop, propSatisfies, when } from 'ramda' +import { bech32 } from 'bech32' + +import { DBInvoice, Invoice } from '../@types/invoice' +import { Pubkey } from '../@types/base' + export const toJSON = (input: any) => JSON.stringify(input) export const toBuffer = (input: any) => Buffer.from(input, 'hex') export const fromBuffer = (input: Buffer) => input.toString('hex') + +export const toBigInt = (input: string): bigint => BigInt(input) + +export const fromBigInt = (input: bigint) => input.toString() + +export const fromDBInvoice = (input: DBInvoice): Invoice => applySpec({ + id: prop('id') as () => Pubkey, + pubkey: pipe(prop('pubkey') as () => Buffer, fromBuffer), + bolt11: prop('bolt11'), + amountRequested: pipe(prop('amount_requested'), toBigInt), + amountPaid: pipe(prop('amount_paid'), toBigInt), + unit: prop('unit'), + status: prop('status'), + description: prop('description'), + confirmedAt: prop('confirmed_at'), + expiresAt: prop('expires_at'), + updatedAt: prop('updated_at'), + createdAt: prop('created_at'), +})(input) + +export const fromNpub = (npub: string) => { + const { prefix, words } = bech32.decode(npub) + if (prefix !== 'npub') { + throw new Error('not an npub key') + } + + return Buffer.from( + bech32.fromWords(words).slice(0, 32) + ).toString('hex') +} + +export const toDate = (input: string) => new Date(input) + + +export const fromZebedeeInvoice = applySpec({ + id: prop('id'), + pubkey: prop('internalId'), + bolt11: path(['invoice', 'request']), + amountRequested: pipe(prop('amount'), toBigInt), + amountPaid: when( + pathEq(['status'], 'completed'), + pipe(prop('amount'), toBigInt), + ), + unit: prop('unit'), + status: prop('status'), + description: prop('description'), + confirmedAt: when( + propSatisfies(is(String), 'confirmed_at'), + pipe(prop('confirmed_at'), toDate), + ), + expiresAt: when( + propSatisfies(is(String), 'confirmed_at'), + pipe(prop('expires_at'), toDate), + ), + createdAt: pipe(prop('created_at'), toDate), +}) diff --git a/test/integration/docker-compose.yml b/test/integration/docker-compose.yml index c9cca09..279e264 100644 --- a/test/integration/docker-compose.yml +++ b/test/integration/docker-compose.yml @@ -20,7 +20,7 @@ services: DEBUG: "" volumes: - ../../package.json:/code/package.json - - ../../settings.sample.json:/code/settings.sample.json + - ../../resources/default-settings.json:/code/resources/default-settings.json - ../../src:/code/src - ../../test/integration:/code/test/integration - ../../cucumber.js:/code/cucumber.js diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index fe626d3..38df95f 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -9,6 +9,7 @@ chai.use(chaiAsPromised) import { EventLimits, ISettings } from '../../../src/@types/settings' import { IncomingEventMessage, MessageType } from '../../../src/@types/messages' import { Event } from '../../../src/@types/event' +import { EventKinds } from '../../../src/constants/base' import { EventMessageHandler } from '../../../src/handlers/event-message-handler' import { IWebSocketAdapter } from '../../../src/@types/adapters' import { WebSocketAdapterEvent } from '../../../src/constants/adapter' @@ -174,6 +175,7 @@ describe('EventMessageHandler', () => { whitelist: [], }, pubkey: { + minBalanceMsats: 0, minLeadingZeroBits: 0, blacklist: [], whitelist: [], @@ -241,8 +243,8 @@ describe('EventMessageHandler', () => { describe('content', () => { describe('maxLength', () => { - it('returns undefined if maxLength is zero', () => { - eventLimits.content.maxLength = 0 + it('returns undefined if maxLength is disabled', () => { + eventLimits.content = [{ maxLength: 0 }] expect( (handler as any).canAcceptEvent(event) @@ -250,7 +252,62 @@ describe('EventMessageHandler', () => { }) it('returns undefned if content is not too long', () => { - eventLimits.content.maxLength = 100 + eventLimits.content = [{ maxLength: 1 }] + event.content = 'x'.repeat(1) + + expect( + (handler as any).canAcceptEvent(event) + ).to.be.undefined + }) + + it('returns undefined if kind does not match', () => { + eventLimits.content = [{ kinds: [EventKinds.SET_METADATA], maxLength: 1 }] + event.content = 'x' + + expect( + (handler as any).canAcceptEvent(event) + ).to.be.undefined + }) + + it('returns undefined if kind matches but content is short', () => { + eventLimits.content = [{ kinds: [EventKinds.TEXT_NOTE], maxLength: 1 }] + event.content = 'x' + + expect( + (handler as any).canAcceptEvent(event) + ).to.be.undefined + }) + + it('returns reason if kind matches but content is too long', () => { + eventLimits.content = [{ kinds: [EventKinds.TEXT_NOTE], maxLength: 1 }] + event.content = 'xx' + + expect( + (handler as any).canAcceptEvent(event) + ).to.equal('rejected: content is longer than 1 bytes') + }) + + it('returns reason if content is too long', () => { + eventLimits.content = [{ maxLength: 1 }] + event.content = 'x'.repeat(2) + + expect( + (handler as any).canAcceptEvent(event) + ).to.equal('rejected: content is longer than 1 bytes') + }) + }) + + describe('maxLength (deprecated)', () => { + it('returns undefined if maxLength is zero', () => { + eventLimits.content = { maxLength: 0 } + + expect( + (handler as any).canAcceptEvent(event) + ).to.be.undefined + }) + + it('returns undefined if content is short', () => { + eventLimits.content = { maxLength: 100 } event.content = 'x'.repeat(100) expect( @@ -259,12 +316,48 @@ describe('EventMessageHandler', () => { }) it('returns reason if content is too long', () => { - eventLimits.content.maxLength = 100 - event.content = 'x'.repeat(101) + eventLimits.content = { maxLength: 1 } + event.content = 'xx' expect( (handler as any).canAcceptEvent(event) - ).to.equal('rejected: content is longer than 100 bytes') + ).to.equal('rejected: content is longer than 1 bytes') + }) + + it('returns undefined if kind matches and content is short', () => { + eventLimits.content = { kinds: [EventKinds.TEXT_NOTE], maxLength: 1 } + event.content = 'x' + + expect( + (handler as any).canAcceptEvent(event) + ).to.be.undefined + }) + + it('returns undefined if kind does not match and content is too long', () => { + eventLimits.content = { kinds: [EventKinds.SET_METADATA], maxLength: 1 } + event.content = 'xx' + + expect( + (handler as any).canAcceptEvent(event) + ).to.be.undefined + }) + + it('returns reason if content is too long', () => { + eventLimits.content = { maxLength: 1 } + event.content = 'xx' + + expect( + (handler as any).canAcceptEvent(event) + ).to.equal('rejected: content is longer than 1 bytes') + }) + + it('returns undefined if content is not set', () => { + eventLimits.content = undefined + event.content = 'xx' + + expect( + (handler as any).canAcceptEvent(event) + ).to.be.undefined }) }) @@ -666,12 +759,12 @@ describe('EventMessageHandler', () => { rate: 1, }, { - kinds: [0], + kinds: [1], period: 60000, rate: 2, }, { - kinds: [[10, 20]], + kinds: [[0, 3]], period: 86400000, rate: 3, }, @@ -689,7 +782,7 @@ describe('EventMessageHandler', () => { } ) expect(rateLimiterHitStub.secondCall).to.have.been.calledWithExactly( - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000:[0]', + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000:[1]', 1, { period: 60000, @@ -697,7 +790,7 @@ describe('EventMessageHandler', () => { } ) expect(rateLimiterHitStub.thirdCall).to.have.been.calledWithExactly( - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:86400000:[[10,20]]', + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:86400000:[[0,3]]', 1, { period: 86400000, @@ -729,7 +822,14 @@ describe('EventMessageHandler', () => { const actualResult = await (handler as any).isRateLimited(event) - expect(rateLimiterHitStub).to.have.been.calledThrice + expect(rateLimiterHitStub).to.have.been.calledOnceWithExactly( + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000', + 1, + { + period: 60000, + rate: 1, + }, + ) expect(actualResult).to.be.false }) @@ -745,19 +845,33 @@ describe('EventMessageHandler', () => { rate: 2, }, { - kinds: [[10, 20]], - period: 86400000, + kinds: [[0, 5]], + period: 180, rate: 3, }, ] rateLimiterHitStub.onFirstCall().resolves(false) rateLimiterHitStub.onSecondCall().resolves(true) - rateLimiterHitStub.onThirdCall().resolves(false) const actualResult = await (handler as any).isRateLimited(event) - expect(rateLimiterHitStub).to.have.been.calledThrice + expect(rateLimiterHitStub.firstCall).to.have.been.calledWithExactly( + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000', + 1, + { + period: 60000, + rate: 1, + }, + ) + expect(rateLimiterHitStub.secondCall).to.have.been.calledWithExactly( + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:180:[[0,5]]', + 1, + { + period: 180, + rate: 3, + }, + ) expect(actualResult).to.be.true }) }) diff --git a/test/unit/utils/http.spec.ts b/test/unit/utils/http.spec.ts new file mode 100644 index 0000000..3e5517f --- /dev/null +++ b/test/unit/utils/http.spec.ts @@ -0,0 +1,68 @@ +import { expect } from 'chai' +import { IncomingMessage } from 'http' + +import { getRemoteAddress } from '../../../src/utils/http' + +describe('getRemoteAddress', () => { + const header = 'x-forwarded-for' + const socketAddress = 'socket-address' + const address = 'address' + + let request: IncomingMessage + + beforeEach(() => { + request = { + headers: { + [header]: address, + }, + socket: { + remoteAddress: socketAddress, + }, + } as any + }) + + it('returns address using network.remote_ip_address when set', () => { + expect( + getRemoteAddress( + request, + { network: { 'remote_ip_header': header } } as any, + ) + ).to.equal(address) + }) + + it('returns address using network.remoteIpAddress when set', () => { + expect( + getRemoteAddress( + request, + { network: { remoteIpHeader: header } } as any, + ) + ).to.equal(address) + }) + + it('returns address from socket when header is unset', () => { + expect( + getRemoteAddress( + request, + { network: { } } as any, + ) + ).to.equal(socketAddress) + }) + + it('returns undefined if unable to find header', () => { + expect( + getRemoteAddress( + { ...request, socket: {} } as any, + { network: {} } as any, + ) + ).to.be.undefined + }) + + it('returns undefined if header setting is unset', () => { + expect( + getRemoteAddress( + { ...request, socket: {} } as any, + {} as any, + ) + ).to.be.undefined + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 2839ea8..932e636 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es6", + "target": "es2020", "outDir": "./dist", "moduleResolution": "Node", "types": ["node", "mocha", "@cucumber/cucumber"],