From 11739c955dad87559649f03f45ed79a3b862c30a Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Tue, 14 Jan 2025 17:13:09 -0600 Subject: [PATCH] fork and copy @satellite/personal-node --- .changeset/README.md | 8 + .changeset/config.json | 11 + .dockerignore | 5 + .env.example | 18 + .gitignore | 5 + .gitmodules | 3 + .nvmrc | 1 + .prettierrc | 5 + .vscode/settings.json | 5 + LICENSE | 21 + README.md | 2 + dockerfile | 29 + nostrudel | 1 + package.json | 80 + pnpm-lock.yaml | 3915 +++++++++++++++++ public/.gitkeep | 0 src/app/database.ts | 88 + src/app/index.ts | 410 ++ src/const.ts | 7 + src/env.ts | 33 + src/helpers/fs.ts | 9 + src/helpers/ip.ts | 19 + src/helpers/json.ts | 27 + src/helpers/network.ts | 23 + src/helpers/nip19.ts | 12 + src/index.ts | 87 + src/logger.ts | 25 + src/modules/address-book.ts | 66 + src/modules/cautious-pool.ts | 95 + src/modules/community-multiplexer.ts | 89 + src/modules/community-proxy.ts | 175 + src/modules/config-manager.ts | 61 + src/modules/contact-book.ts | 54 + src/modules/control/config-actions.ts | 49 + src/modules/control/control-api.ts | 130 + src/modules/control/database-actions.ts | 66 + src/modules/control/decryption-cache.ts | 50 + src/modules/control/dm-actions.ts | 31 + src/modules/control/logs-actions.ts | 27 + src/modules/control/notification-actions.ts | 43 + src/modules/control/receiver-actions.ts | 29 + src/modules/control/remote-auth-actions.ts | 69 + src/modules/control/report-actions.ts | 93 + src/modules/control/scrapper-actions.ts | 37 + .../decryption-cache/decryption-cache.ts | 153 + src/modules/direct-message-manager.ts | 168 + src/modules/gossip.ts | 132 + src/modules/graph/index.ts | 105 + src/modules/hyper-connection-manager.ts | 65 + src/modules/labeled-event-store.ts | 79 + src/modules/log-store/log-store.ts | 163 + src/modules/network/inbound/hyper.ts | 71 + src/modules/network/inbound/i2p.ts | 76 + src/modules/network/inbound/index.ts | 79 + src/modules/network/inbound/tor.ts | 29 + src/modules/network/interfaces.ts | 22 + src/modules/network/outbound/hyper.ts | 57 + src/modules/network/outbound/i2p.ts | 31 + src/modules/network/outbound/index.ts | 134 + src/modules/network/outbound/tor.ts | 31 + src/modules/network/outbound/websocket.ts | 11 + .../notifications/notifications-manager.ts | 155 + src/modules/profile-book.ts | 40 + src/modules/pubkey-batch-loader.ts | 175 + src/modules/receiver/index.ts | 259 ++ src/modules/reports/report.ts | 77 + src/modules/reports/reports/conversations.ts | 115 + src/modules/reports/reports/dm-search.ts | 11 + src/modules/reports/reports/events-summary.ts | 69 + src/modules/reports/reports/index.ts | 30 + src/modules/reports/reports/logs.ts | 23 + src/modules/reports/reports/network-status.ts | 69 + .../reports/reports/notification-channels.ts | 29 + src/modules/reports/reports/overview.ts | 40 + .../reports/reports/receiver-status.ts | 38 + .../reports/reports/scrapper-status.ts | 55 + src/modules/reports/reports/services.ts | 12 + src/modules/scrapper/index.ts | 145 + src/modules/scrapper/pubkey-relay-scrapper.ts | 115 + src/modules/scrapper/pubkey-scrapper.ts | 83 + src/modules/secrets-manager.ts | 125 + .../state/application-state-manager.ts | 49 + src/modules/state/mutable-state.ts | 91 + src/modules/switchboard/switchboard.ts | 92 + src/sidecars/hyperdht.ts | 19 + src/types/holesail-server.d.ts | 24 + src/types/hyperdht.d.ts | 38 + src/types/streamx.d.ts | 5 + tsconfig.json | 15 + 89 files changed, 9417 insertions(+) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .nvmrc create mode 100644 .prettierrc create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 dockerfile create mode 160000 nostrudel create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 public/.gitkeep create mode 100644 src/app/database.ts create mode 100644 src/app/index.ts create mode 100644 src/const.ts create mode 100644 src/env.ts create mode 100644 src/helpers/fs.ts create mode 100644 src/helpers/ip.ts create mode 100644 src/helpers/json.ts create mode 100644 src/helpers/network.ts create mode 100644 src/helpers/nip19.ts create mode 100644 src/index.ts create mode 100644 src/logger.ts create mode 100644 src/modules/address-book.ts create mode 100644 src/modules/cautious-pool.ts create mode 100644 src/modules/community-multiplexer.ts create mode 100644 src/modules/community-proxy.ts create mode 100644 src/modules/config-manager.ts create mode 100644 src/modules/contact-book.ts create mode 100644 src/modules/control/config-actions.ts create mode 100644 src/modules/control/control-api.ts create mode 100644 src/modules/control/database-actions.ts create mode 100644 src/modules/control/decryption-cache.ts create mode 100644 src/modules/control/dm-actions.ts create mode 100644 src/modules/control/logs-actions.ts create mode 100644 src/modules/control/notification-actions.ts create mode 100644 src/modules/control/receiver-actions.ts create mode 100644 src/modules/control/remote-auth-actions.ts create mode 100644 src/modules/control/report-actions.ts create mode 100644 src/modules/control/scrapper-actions.ts create mode 100644 src/modules/decryption-cache/decryption-cache.ts create mode 100644 src/modules/direct-message-manager.ts create mode 100644 src/modules/gossip.ts create mode 100644 src/modules/graph/index.ts create mode 100644 src/modules/hyper-connection-manager.ts create mode 100644 src/modules/labeled-event-store.ts create mode 100644 src/modules/log-store/log-store.ts create mode 100644 src/modules/network/inbound/hyper.ts create mode 100644 src/modules/network/inbound/i2p.ts create mode 100644 src/modules/network/inbound/index.ts create mode 100644 src/modules/network/inbound/tor.ts create mode 100644 src/modules/network/interfaces.ts create mode 100644 src/modules/network/outbound/hyper.ts create mode 100644 src/modules/network/outbound/i2p.ts create mode 100644 src/modules/network/outbound/index.ts create mode 100644 src/modules/network/outbound/tor.ts create mode 100644 src/modules/network/outbound/websocket.ts create mode 100644 src/modules/notifications/notifications-manager.ts create mode 100644 src/modules/profile-book.ts create mode 100644 src/modules/pubkey-batch-loader.ts create mode 100644 src/modules/receiver/index.ts create mode 100644 src/modules/reports/report.ts create mode 100644 src/modules/reports/reports/conversations.ts create mode 100644 src/modules/reports/reports/dm-search.ts create mode 100644 src/modules/reports/reports/events-summary.ts create mode 100644 src/modules/reports/reports/index.ts create mode 100644 src/modules/reports/reports/logs.ts create mode 100644 src/modules/reports/reports/network-status.ts create mode 100644 src/modules/reports/reports/notification-channels.ts create mode 100644 src/modules/reports/reports/overview.ts create mode 100644 src/modules/reports/reports/receiver-status.ts create mode 100644 src/modules/reports/reports/scrapper-status.ts create mode 100644 src/modules/reports/reports/services.ts create mode 100644 src/modules/scrapper/index.ts create mode 100644 src/modules/scrapper/pubkey-relay-scrapper.ts create mode 100644 src/modules/scrapper/pubkey-scrapper.ts create mode 100644 src/modules/secrets-manager.ts create mode 100644 src/modules/state/application-state-manager.ts create mode 100644 src/modules/state/mutable-state.ts create mode 100644 src/modules/switchboard/switchboard.ts create mode 100644 src/sidecars/hyperdht.ts create mode 100644 src/types/holesail-server.d.ts create mode 100644 src/types/hyperdht.d.ts create mode 100644 src/types/streamx.d.ts create mode 100644 tsconfig.json diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..a590128 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "master", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d0b1c11 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +/dist +/nostrudel/dist +/data +node_modules +/nostrudel/node_modules diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a40457b --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# where to store the relays data +DATA_PATH=./data + +# the port to use +PORT=3000 + +# the address to the tor SOCKS5 proxy to enable connections to .onion addresses +# TOR_PROXY="127.0.0.1:9050" +# tor proxy type, SOCKS5 or HTTP +# TOR_PROXY_TYPE="SOCKS5" + +# the address to the i2p SOCKS5 proxy to enable connections to .i2p addresses +# I2P_PROXY="127.0.0.1:4447" +# I@P proxy type, SOCKS5 or HTTP +# I2P_PROXY_TYPE="SOCKS5" + +# sets a hardcoded tor address +# TOR_ADDRESS="http://xxxxxxxxxxxx.onion" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdc4748 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +.env +/data +dist diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..731f4cc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "nostrudel"] + path = nostrudel + url = https://github.com/hzrd149/nostrudel.git diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..054d599 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 120 +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..08a334a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "search.exclude": { + "**/nostrudel": true + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c30a8f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 hzrd149 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS 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. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index db77a45..666cda4 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # bakery + +A relay backend for noStrudel diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..d9d096b --- /dev/null +++ b/dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 +FROM node:22-slim AS base + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +WORKDIR /app +COPY . /app + +FROM base AS web +RUN --mount=type=cache,id=pnpm,target=/pnpm/store cd nostrudel && pnpm install +RUN cd nostrudel && pnpm build + +FROM base AS build +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install +RUN pnpm run build + +FROM base +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod +COPY --from=build /app/dist /app/dist +COPY --from=web /app/nostrudel/dist /app/public + +VOLUME [ "/app/data" ] +EXPOSE 3000 + +ENV PORT="3000" + +CMD [ "node", "." ] diff --git a/nostrudel b/nostrudel new file mode 160000 index 0000000..831712b --- /dev/null +++ b/nostrudel @@ -0,0 +1 @@ +Subproject commit 831712bf348f5c19e3a52536da209e3042cb18fd diff --git a/package.json b/package.json new file mode 100644 index 0000000..fe426cc --- /dev/null +++ b/package.json @@ -0,0 +1,80 @@ +{ + "name": "bakery", + "version": "0.1.0", + "description": "A relay backend for noStrudel", + "type": "module", + "bin": "dist/index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "prepack": "pnpm build", + "start": "node .", + "dev": "nodemon --loader @swc-node/register/esm src/index.ts", + "build": "tsc", + "format": "prettier -w . --ignore-path .gitignore" + }, + "files": [ + "dist", + "nostrudel/dist" + ], + "keywords": [ + "nostr", + "relay" + ], + "author": "hzrd149", + "license": "MIT", + "dependencies": { + "@diva.exchange/i2p-sam": "^5.4.1", + "@noble/hashes": "^1.7.0", + "@satellite-earth/core": "^0.5.0", + "applesauce-core": "^0.10.0", + "applesauce-signer": "^0.10.0", + "better-sqlite3": "^11.7.2", + "blossom-client-sdk": "^2.1.1", + "cors": "^2.8.5", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "get-port": "^7.1.0", + "holesail-server": "^1.4.4", + "hyper-address": "^0.1.3", + "hyper-socks5-proxy": "^0.1.2", + "hyperdht": "^6.20.1", + "import-meta-resolve": "^4.1.0", + "lodash.throttle": "^4.1.1", + "lowdb": "^7.0.1", + "mkdirp": "^3.0.1", + "nanoid": "^5.0.9", + "nostr-tools": "^2.10.4", + "pac-proxy-agent": "^7.1.0", + "process-streams": "^1.0.3", + "streamx": "^2.21.1", + "unique-names-generator": "^4.7.1", + "web-push": "^3.6.7", + "ws": "^8.18.0" + }, + "devDependencies": { + "@changesets/cli": "^2.27.11", + "@swc-node/register": "^1.10.9", + "@swc/core": "^1.10.7", + "@types/better-sqlite3": "^7.6.12", + "@types/cors": "^2.8.17", + "@types/debug": "^4.1.12", + "@types/express": "^4.17.21", + "@types/lodash.throttle": "^4.1.9", + "@types/node": "^22.10.6", + "@types/web-push": "^3.6.4", + "@types/ws": "^8.5.13", + "nodemon": "^3.1.9", + "prettier": "^3.4.2", + "typescript": "^5.7.3" + }, + "nodemonConfig": { + "ignore": [ + "data/**" + ], + "exec": "node", + "signal": "SIGTERM" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..39f8fda --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3915 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@diva.exchange/i2p-sam': + specifier: ^5.4.1 + version: 5.4.1 + '@noble/hashes': + specifier: ^1.7.0 + version: 1.7.0 + '@satellite-earth/core': + specifier: ^0.5.0 + version: 0.5.0(typescript@5.7.3) + applesauce-core: + specifier: ^0.10.0 + version: 0.10.0(typescript@5.7.3) + applesauce-signer: + specifier: ^0.10.0 + version: 0.10.0(typescript@5.7.3) + better-sqlite3: + specifier: ^11.7.2 + version: 11.7.2 + blossom-client-sdk: + specifier: ^2.1.1 + version: 2.1.1 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + debug: + specifier: ^4.4.0 + version: 4.4.0(supports-color@5.5.0) + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + express: + specifier: ^4.21.2 + version: 4.21.2 + get-port: + specifier: ^7.1.0 + version: 7.1.0 + holesail-server: + specifier: ^1.4.4 + version: 1.4.4 + hyper-address: + specifier: ^0.1.3 + version: 0.1.3 + hyper-socks5-proxy: + specifier: ^0.1.2 + version: 0.1.2 + hyperdht: + specifier: ^6.20.1 + version: 6.20.1 + import-meta-resolve: + specifier: ^4.1.0 + version: 4.1.0 + lodash.throttle: + specifier: ^4.1.1 + version: 4.1.1 + lowdb: + specifier: ^7.0.1 + version: 7.0.1 + mkdirp: + specifier: ^3.0.1 + version: 3.0.1 + nanoid: + specifier: ^5.0.9 + version: 5.0.9 + nostr-tools: + specifier: ^2.10.4 + version: 2.10.4(typescript@5.7.3) + pac-proxy-agent: + specifier: ^7.1.0 + version: 7.1.0 + process-streams: + specifier: ^1.0.3 + version: 1.0.3 + streamx: + specifier: ^2.21.1 + version: 2.21.1 + unique-names-generator: + specifier: ^4.7.1 + version: 4.7.1 + web-push: + specifier: ^3.6.7 + version: 3.6.7 + ws: + specifier: ^8.18.0 + version: 8.18.0 + devDependencies: + '@changesets/cli': + specifier: ^2.27.11 + version: 2.27.11 + '@swc-node/register': + specifier: ^1.10.9 + version: 1.10.9(@swc/core@1.10.7)(@swc/types@0.1.17)(typescript@5.7.3) + '@swc/core': + specifier: ^1.10.7 + version: 1.10.7 + '@types/better-sqlite3': + specifier: ^7.6.12 + version: 7.6.12 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/lodash.throttle': + specifier: ^4.1.9 + version: 4.1.9 + '@types/node': + specifier: ^22.10.6 + version: 22.10.6 + '@types/web-push': + specifier: ^3.6.4 + version: 3.6.4 + '@types/ws': + specifier: ^8.5.13 + version: 8.5.13 + nodemon: + specifier: ^3.1.9 + version: 3.1.9 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + typescript: + specifier: ^5.7.3 + version: 5.7.3 + +packages: + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@cashu/cashu-ts@2.1.0': + resolution: {integrity: sha512-qFfFz1dx9keJxumjk5FyTvI1j0Yp/P5LXDy0cGO4Xlp3WYKOI1nykNOTPd+bTY9vSkvIM+xuXRer9BtQxqHtwA==} + + '@cashu/crypto@0.3.4': + resolution: {integrity: sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==} + + '@changesets/apply-release-plan@7.0.7': + resolution: {integrity: sha512-qnPOcmmmnD0MfMg9DjU1/onORFyRpDXkMMl2IJg9mECY6RnxL3wN0TCCc92b2sXt1jt8DgjAUUsZYGUGTdYIXA==} + + '@changesets/assemble-release-plan@6.0.5': + resolution: {integrity: sha512-IgvBWLNKZd6k4t72MBTBK3nkygi0j3t3zdC1zrfusYo0KpdsvnDjrMM9vPnTCLCMlfNs55jRL4gIMybxa64FCQ==} + + '@changesets/changelog-git@0.2.0': + resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} + + '@changesets/cli@2.27.11': + resolution: {integrity: sha512-1QislpE+nvJgSZZo9+Lj3Lno5pKBgN46dAV8IVxKJy9wX8AOrs9nn5pYVZuDpoxWJJCALmbfOsHkyxujgetQSg==} + hasBin: true + + '@changesets/config@3.0.5': + resolution: {integrity: sha512-QyXLSSd10GquX7hY0Mt4yQFMEeqnO5z/XLpbIr4PAkNNoQNKwDyiSrx4yd749WddusH1v3OSiA0NRAYmH/APpQ==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.2': + resolution: {integrity: sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ==} + + '@changesets/get-release-plan@4.0.6': + resolution: {integrity: sha512-FHRwBkY7Eili04Y5YMOZb0ezQzKikTka4wL753vfUA5COSebt7KThqiuCN9BewE4/qFGgF/5t3AuzXx1/UAY4w==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.2': + resolution: {integrity: sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.0': + resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==} + + '@changesets/pre@2.0.1': + resolution: {integrity: sha512-vvBJ/If4jKM4tPz9JdY2kGOgWmCowUYOi5Ycv8dyLnEE8FgpYYUo1mgJZxcdtGGP3aG8rAQulGLyyXGSLkIMTQ==} + + '@changesets/read@0.6.2': + resolution: {integrity: sha512-wjfQpJvryY3zD61p8jR87mJdyx2FIhEcdXhKUqkja87toMrP/3jtg/Yg29upN+N4Ckf525/uvV7a4tzBlpk6gg==} + + '@changesets/should-skip-package@0.1.1': + resolution: {integrity: sha512-H9LjLbF6mMHLtJIc/eHR9Na+MifJ3VxtgP/Y+XLn4BF7tDTEN1HNYtH6QMcjP1uxp9sjaFYmW8xqloaCi/ckTg==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.0.0': + resolution: {integrity: sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==} + + '@changesets/write@0.3.2': + resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==} + + '@diva.exchange/i2p-sam@5.4.1': + resolution: {integrity: sha512-/X2ezrRSxAxrEmk+z9c1fIurb1XjUyEQkvdgUdQezdOhC48XPj4Ed4LOpODGacFvOhwSwX1RAy7xV39PA2ki0w==} + engines: {node: '>=16.0.0'} + + '@emnapi/core@1.3.1': + resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@emnapi/wasi-threads@1.0.1': + resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + + '@holesail/hyper-cmd-lib-net@0.2.1': + resolution: {integrity: sha512-I7ZZj4ZplLwbhBCDZiWpGrcsemkssLjY4ZSH9SbsmJcM4b49zlz6ZQOzDsAI1pruOyPdatjc/wAkKQzTRK6w7Q==} + + '@hyperswarm/secret-stream@6.7.1': + resolution: {integrity: sha512-isb18Pt6lXBpOQMRmpqItw+kYynXilOFyOhto/RMP15WQtTWC0rR5jfZPYXU7ZYV6Kxd2lyQ4ZBevoIcvEJHEQ==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@napi-rs/wasm-runtime@0.2.6': + resolution: {integrity: sha512-z8YVS3XszxFTO73iwvFDNpQIzdMmSDTP/mB3E/ucR37V3Sx57hSExcXyMoNwaucWxnsWf4xfbZv0iZ30jr0M4Q==} + + '@noble/ciphers@0.5.3': + resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} + + '@noble/curves@1.1.0': + resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} + + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + + '@noble/curves@1.8.0': + resolution: {integrity: sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.3.1': + resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} + engines: {node: '>= 16'} + + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + + '@noble/hashes@1.7.0': + resolution: {integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==} + engines: {node: ^14.21.3 || >=16} + + '@noble/secp256k1@1.7.1': + resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-resolver/binding-darwin-arm64@1.12.0': + resolution: {integrity: sha512-wYe+dlF8npM7cwopOOxbdNjtmJp17e/xF5c0K2WooQXy5VOh74icydM33+Uh/SZDgwyum09/U1FVCX5GdeQk+A==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@1.12.0': + resolution: {integrity: sha512-FZxxp99om+SlvBr1cjzF8A3TjYcS0BInCqjUlM+2f9m9bPTR2Bng9Zq5Q09ZQyrKJjfGKqlOEHs3akuVOnrx3Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@1.12.0': + resolution: {integrity: sha512-BZi0iU6IEOnXGSkqt1OjTTkN9wfyaK6kTpQwL/axl8eCcNDc7wbv1vloHgILf7ozAY1TP75nsLYlASYI4B5kGA==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@1.12.0': + resolution: {integrity: sha512-L2qnMEnZAqxbG9b1J3di/w/THIm+1fMVfbbTMWIQNMMXdMeqqDN6ojnOLDtuP564rAh4TBFPdLyEfGhMz6ipNA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@1.12.0': + resolution: {integrity: sha512-otVbS4zeo3n71zgGLBYRTriDzc0zpruC0WI3ICwjpIk454cLwGV0yzh4jlGYWQJYJk0BRAmXFd3ooKIF+bKBHw==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@1.12.0': + resolution: {integrity: sha512-IStQDjIT7Lzmqg1i9wXvPL/NsYsxF24WqaQFS8b8rxra+z0VG7saBOsEnOaa4jcEY8MVpLYabFhTV+fSsA2vnA==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@1.12.0': + resolution: {integrity: sha512-SipT7EVORz8pOQSFwemOm91TpSiBAGmOjG830/o+aLEsvQ4pEy223+SAnCfITh7+AahldYsJnVoIs519jmIlKQ==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@1.12.0': + resolution: {integrity: sha512-mGh0XfUzKdn+WFaqPacziNraCWL5znkHRfQVxG9avGS9zb2KC/N1EBbPzFqutDwixGDP54r2gx4q54YCJEZ4iQ==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-wasm32-wasi@1.12.0': + resolution: {integrity: sha512-SZN6v7apKmQf/Vwiqb6e/s3Y2Oacw8uW8V2i1AlxtyaEFvnFE0UBn89zq6swEwE3OCajNWs0yPvgAXUMddYc7Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@1.12.0': + resolution: {integrity: sha512-GRe4bqCfFsyghruEn5bv47s9w3EWBdO2q72xCz5kpQ0LWbw+enPHtTjw3qX5PUcFYpKykM55FaO0hFDs1yzatw==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@1.12.0': + resolution: {integrity: sha512-Z3llHH0jfJP4mlWq3DT7bK6qV+/vYe0+xzCgfc67+Tc/U3eYndujl880bexeGdGNPh87JeYznpZAOJ44N7QVVQ==} + cpu: [x64] + os: [win32] + + '@pondwader/socks5-server@1.0.10': + resolution: {integrity: sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==} + + '@satellite-earth/core@0.5.0': + resolution: {integrity: sha512-YbD3YW6+pNjWReP+BwXc/dDO8aQen/zdw+IJTKryujGeYn4z0fRePB3foxSKcM6y1/BRNclBccYbzdvj0GagzA==} + + '@scure/base@1.1.1': + resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} + + '@scure/base@1.2.1': + resolution: {integrity: sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ==} + + '@scure/bip32@1.3.1': + resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} + + '@scure/bip32@1.6.1': + resolution: {integrity: sha512-jSO+5Ud1E588Y+LFo8TaB8JVPNAZw/lGGao+1SepHDeTs2dFLurdNIAgUuDlwezqEjRjElkCJajVrtrZaBxvaQ==} + + '@scure/bip39@1.2.1': + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + + '@scure/bip39@1.5.1': + resolution: {integrity: sha512-GnlufVSP9UdAo/H2Patfv22VTtpNTyfi+I3qCKpvuB5l1KWzEYx+l2TNpBy9Ksh4xTs3Rn06tBlpWCi/1Vz8gw==} + + '@swc-node/core@1.13.3': + resolution: {integrity: sha512-OGsvXIid2Go21kiNqeTIn79jcaX4l0G93X2rAnas4LFoDyA9wAwVK7xZdm+QsKoMn5Mus2yFLCc4OtX2dD/PWA==} + engines: {node: '>= 10'} + peerDependencies: + '@swc/core': '>= 1.4.13' + '@swc/types': '>= 0.1' + + '@swc-node/register@1.10.9': + resolution: {integrity: sha512-iXy2sjP0phPEpK2yivjRC3PAgoLaT4sjSk0LDWCTdcTBJmR4waEog0E6eJbvoOkLkOtWw37SB8vCkl/bbh4+8A==} + peerDependencies: + '@swc/core': '>= 1.4.13' + typescript: '>= 4.3' + + '@swc-node/sourcemap-support@0.5.1': + resolution: {integrity: sha512-JxIvIo/Hrpv0JCHSyRpetAdQ6lB27oFYhv0PKCNf1g2gUXOjpeR1exrXccRxLMuAV5WAmGFBwRnNOJqN38+qtg==} + + '@swc/core-darwin-arm64@1.10.7': + resolution: {integrity: sha512-SI0OFg987P6hcyT0Dbng3YRISPS9uhLX1dzW4qRrfqQdb0i75lPJ2YWe9CN47HBazrIA5COuTzrD2Dc0TcVsSQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.10.7': + resolution: {integrity: sha512-RFIAmWVicD/l3RzxgHW0R/G1ya/6nyMspE2cAeDcTbjHi0I5qgdhBWd6ieXOaqwEwiCd0Mot1g2VZrLGoBLsjQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.10.7': + resolution: {integrity: sha512-QP8vz7yELWfop5mM5foN6KkLylVO7ZUgWSF2cA0owwIaziactB2hCPZY5QU690coJouk9KmdFsPWDnaCFUP8tg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.10.7': + resolution: {integrity: sha512-NgUDBGQcOeLNR+EOpmUvSDIP/F7i/OVOKxst4wOvT5FTxhnkWrW+StJGKj+DcUVSK5eWOYboSXr1y+Hlywwokw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.10.7': + resolution: {integrity: sha512-gp5Un3EbeSThBIh6oac5ZArV/CsSmTKj5jNuuUAuEsML3VF9vqPO+25VuxCvsRf/z3py+xOWRaN2HY/rjMeZog==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.10.7': + resolution: {integrity: sha512-k/OxLLMl/edYqbZyUNg6/bqEHTXJT15l9WGqsl/2QaIGwWGvles8YjruQYQ9d4h/thSXLT9gd8bExU2D0N+bUA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.10.7': + resolution: {integrity: sha512-XeDoURdWt/ybYmXLCEE8aSiTOzEn0o3Dx5l9hgt0IZEmTts7HgHHVeRgzGXbR4yDo0MfRuX5nE1dYpTmCz0uyA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.10.7': + resolution: {integrity: sha512-nYAbi/uLS+CU0wFtBx8TquJw2uIMKBnl04LBmiVoFrsIhqSl+0MklaA9FVMGA35NcxSJfcm92Prl2W2LfSnTqQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.10.7': + resolution: {integrity: sha512-+aGAbsDsIxeLxw0IzyQLtvtAcI1ctlXVvVcXZMNXIXtTURM876yNrufRo4ngoXB3jnb1MLjIIjgXfFs/eZTUSw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.10.7': + resolution: {integrity: sha512-TBf4clpDBjF/UUnkKrT0/th76/zwvudk5wwobiTFqDywMApHip5O0VpBgZ+4raY2TM8k5+ujoy7bfHb22zu17Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.10.7': + resolution: {integrity: sha512-py91kjI1jV5D5W/Q+PurBdGsdU5TFbrzamP7zSCqLdMcHkKi3rQEM5jkQcZr0MXXSJTaayLxS3MWYTBIkzPDrg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.17': + resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/better-sqlite3@7.6.12': + resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/dom-serial@1.0.6': + resolution: {integrity: sha512-eUHKbc6mdMgMm75/oBLocs3wkOQkPQ/oNCT+b5OgUT6mLgIvDTp3wCCE9tYZNvDPPh6Cj9lVg2IguWfS/mDrrQ==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/lodash.throttle@4.1.9': + resolution: {integrity: sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==} + + '@types/lodash@4.17.14': + resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@22.10.6': + resolution: {integrity: sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==} + + '@types/qs@6.9.18': + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + '@types/web-push@3.6.4': + resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} + + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + applesauce-core@0.10.0: + resolution: {integrity: sha512-QMhUh4FIARcqY5soCB4Z8DIu+py0rYb28IgWT4gP9DLBGpDrY8lStXk7W1/46TLjEH97y0hbiXFK7kMCZ31oOQ==} + + applesauce-net@0.10.0: + resolution: {integrity: sha512-ZsAs/MkeGHiPZ2/a8lwP8lx/Eh+5Dot0qG4BLTAqjg4emP/RsiqW+hyc6v6QcVbdvuR0+hP1gka3+wWtiy/cTA==} + + applesauce-signer@0.10.0: + resolution: {integrity: sha512-2Cn2ZUxk47cBJBFoUl9DB37mjgg8/8GwTAG7csPqtooJ4nUh0ylO4Gh1Mr/401Lc8tjZJvpVBySWpCeXrnG1rQ==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-dgram@1.0.1: + resolution: {integrity: sha512-EdsyRErrkWgN8fENdrDdXFEE9HAuJ/m6ehXz13fVj9JhdCaLWIA+L8o5aYNRLt66x08RlyG2vbrRAZoxGfcdlg==} + + bare-dns@1.0.5: + resolution: {integrity: sha512-Hqb9dpUe4M7y9N6oRV0XhHzUkUliyUE7qw/q3cmn0PF06fdBXooy45fvKGeYUUv6OCvXyLKfi7b7QyZS7r/YoQ==} + + bare-dns@2.0.2: + resolution: {integrity: sha512-xEry4yLFAetelqgYGRMC9/82b/jq9Q9l2a+mX+VouWlZeORIoLWV4OXMUilRy4WfdgGTd8QMMqPlmbE5rX3hnw==} + engines: {bare: '>=1.7.0'} + + bare-events@2.5.4: + resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} + + bare-net@1.0.1: + resolution: {integrity: sha512-eTMpS2fxXiuz5eMvMGEkJBsgIfanPFoniiepCmgbCRQVdAIjg1wutaRzHTqRJpuG4yCzrcar581AIPf1XzyH8g==} + + bare-net@2.0.1: + resolution: {integrity: sha512-klzDKbZFzfguS6054Yv+8/e9rhggYVAg8I8svCWCtYkMwdlh3hz1SGCut6Q7d7iqV60YtuG3Y8957DqLcKuEJQ==} + + bare-pipe@3.3.8: + resolution: {integrity: sha512-X8Ulz/os6LR/cF5HBljq/tJ0NP7eNGjiripZF53EMtgkhJ9FVl1/WLZxwvUvcqbR82Ywaq6KYM+A6zS7DGPPUw==} + + bare-pipe@4.0.2: + resolution: {integrity: sha512-ccjh68NHL3uf+mG/+QMsvEpJhYPCYSRsEgfRCTPeslXciRTImpencrHCroeUD1Cl4f9m4M7u+S4GRLKbyfIDoA==} + engines: {bare: '>=1.7.0'} + + bare-stream@2.6.4: + resolution: {integrity: sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-tcp@1.9.1: + resolution: {integrity: sha512-eaiKHR0ECRrIrsVNVIjoJxA6J08YqR/kSQ5Ikd0nRmgp5qtKxt49PS0z+USfcN5ZsfDcVFpUFo108EAeXtCshA==} + + bare-tcp@2.0.2: + resolution: {integrity: sha512-6qsg+Mg26/UfIEMUWHg1GtK4NlUV2VVCJKD5+yOkxgfGfVhY6u8wNBwuNG6wqRDlz0T7Mc/i6iWFEa4YgsZ/lg==} + engines: {bare: '>=1.7.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + better-sqlite3@11.7.2: + resolution: {integrity: sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bits-to-bytes@1.3.0: + resolution: {integrity: sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + blind-relay@1.3.3: + resolution: {integrity: sha512-J49HZaRLA+jgmIA+J01x0NzDtNMQVzuFfz7CnLLijIUMJkxTbaexIGI/tfk6LONcr/+INmgbS8gMLeuaRvfreA==} + + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + + blossom-client-sdk@0.9.1: + resolution: {integrity: sha512-lEZ4uNzM09rhp7mjzmgLDC3OEgFd76GkmR90fye/IT5HVCofIT6ldBfyqBY9DcuM1S+XNa1Cu14wFg95CyH8Ag==} + + blossom-client-sdk@2.1.1: + resolution: {integrity: sha512-a95eZV7W5/QPN30p0s2K8ZxX0vwMXsnl2JIJDXaOu0nDVxikfKINa/7mhTRtA2i3dzjQ378FPET8vwn0GjtFzg==} + engines: {node: '>=18'} + + blossom-server-sdk@0.4.0: + resolution: {integrity: sha512-sZDosyS2OVa+fM2ifdCOckziAcFwMpzlcxGG1JawwR8dWcQkWmsTJVETZKuZG8EUeUrl25oSyGBWHx7itA2ZOA==} + + bn.js@4.12.1: + resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bogon@1.1.0: + resolution: {integrity: sha512-a6SnToksXHuUlgeMvI/txWmTcKz7c7iBa8f0HbXL4toN1Uza/CTQ4F7n9jSDX49TCpxv3KUP100q4sZfwLyLiw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + compact-encoding-bitfield@1.0.0: + resolution: {integrity: sha512-3nMVKUg+PF72UHfainmCL8uKvyWfxsjqOtUY+HiMPGLPCTjnwzoKfFAMo1Ad7nwTPdjBqtGK5b3BOFTFW4EBTg==} + + compact-encoding-net@1.2.0: + resolution: {integrity: sha512-LVXpNpF7PGQeHRVVLGgYWzuVoYAaDZvKUsUxRioGfkotzvOh4AzoQF1HBH3zMNaSnx7gJXuUr3hkjnijaH/Eng==} + + compact-encoding@2.16.0: + resolution: {integrity: sha512-zG2ul4Egc8ktfmj2vYiC6s/U3u1OkNyUGj32jrJq6qcJ4atGcEpVr+AI+VDJ3oteNfGIw/uol4oBQyB74OeGvw==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + dht-rpc@6.16.0: + resolution: {integrity: sha512-2Tf1E8eWQp+fHje9/n6UdVVky1C80LoGa2VPUorJ8qL5fu8xhvQQ9YiujjgbVIdwSka0oP3VFv5daAx8u6YRPg==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplex-maker@1.0.0: + resolution: {integrity: sha512-KoHuzggxg7f+vvjqOHfXxaQYI1POzBm+ah0eec7YDssZmbt6QFBI8d1nl5GQwAgR2f+VQCPvyvZtmWWqWuFtlA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.0: + resolution: {integrity: sha512-Ujz8Al/KfOVR7fkaghAB1WvnLsdYxHDWmfoi2vlA2jZWRg31XhIC1a4B+/I24muD8iSbHxJ1JkrfqmWb65P/Mw==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + express-async-handler@1.2.0: + resolution: {integrity: sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-xml-parser@4.5.1: + resolution: {integrity: sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==} + hasBin: true + + fastq@1.18.0: + resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-uri@6.0.4: + resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==} + engines: {node: '>= 14'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + holesail-server@1.4.4: + resolution: {integrity: sha512-NoLgx3TQZHyU2KMzKs6b+AMYG8TQz2zkcEPbVTYhB/CG9siPnZaKXhcgBg9vkTaRYMGpTpRQfggaE6jg3ZlBlg==} + + http-error@0.0.6: + resolution: {integrity: sha512-1okadUMOfkA8o9mvatq5dopVrPdIsKw3K9JL2izosTqFJVa+ID8Siw4ichfW7AvXRVHVvpfQpEekPvrjZ3bqSg==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http_ece@1.2.0: + resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} + engines: {node: '>=16'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-id@1.0.2: + resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} + + hyper-address@0.1.3: + resolution: {integrity: sha512-8pq6Sf7y2kK94wt7Qqc8KyGhncc32T1KybKqrEQcNlW9kkznTA85iP4OG8STRjLdjl6aXYAr/98M8P39jCS51w==} + hasBin: true + + hyper-cmd-lib-keys@0.0.2: + resolution: {integrity: sha512-/KUs6yDoBzYFSlx2PnFoCDZaiNwMqc+iDsMqaXM0Wd+Ntu2nkxJxFSlxKCZUThHOmjJF3DH1SPD/5kgENpbwug==} + + hyper-socks5-proxy@0.1.2: + resolution: {integrity: sha512-PkzrB4I61g04qgwu7tyKH2ULh/+QCQqftZkk6VDeo5hUEV7dichmAtx+LW31x8z8MpkBZF+gLBNewlyz1ypXsA==} + hasBin: true + + hypercore-crypto@3.4.2: + resolution: {integrity: sha512-16ii4M6T1dFfRa41Szv3IR0wXfImJMYJ8ysZEGwHEDH7sMeWVEBck6tg1GCNutYl39E+H7wMY2p3ndCRfj+XdQ==} + + hypercore-id-encoding@1.3.0: + resolution: {integrity: sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==} + + hyperdht@6.20.1: + resolution: {integrity: sha512-0UO5rrxOwZLmpDApX49GmhdbZOpVIG9VvudlReH8GS4Upz2HNR75GIPChRClkfpD3Lo80yWJHfitnKiqo7tC6w==} + hasBin: true + + hypertrace@1.4.2: + resolution: {integrity: sha512-sa6iq1FaJ03Db3eUl5ZodyOL3fheyrum9xzeHasXOQ/AprTT6vS1WjpbXfYkHhmzVmyn0jBW/VsCb1QaBkGyow==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + json-stream@1.0.0: + resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + kademlia-routing-table@1.0.3: + resolution: {integrity: sha512-ag1nLPaCSxbLnG6zeTBoGkdm+JSUi2JHMiNynHBoRG8se2w9yfyzepF3oX1B52HOEldtbuxTde5NxbNUtnwGqA==} + + light-bolt11-decoder@3.2.0: + resolution: {integrity: sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lowdb@7.0.1: + resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} + engines: {node: '>=18'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@4.0.6: + resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==} + engines: {node: '>=16'} + hasBin: true + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minio@7.1.3: + resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} + engines: {node: ^16 || ^18 || >=20} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoassert@2.0.0: + resolution: {integrity: sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==} + + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} + hasBin: true + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + nat-sampler@1.0.1: + resolution: {integrity: sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + node-abi@3.72.0: + resolution: {integrity: sha512-a28z9xAQXvDh40lVCknWCP5zYTZt6Av8HZqZ63U5OWxTcP20e3oOIy8yHkYfctQM2adR8ru1GxWCkS0gS+WYKA==} + engines: {node: '>=10'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + nodemon@3.1.9: + resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==} + engines: {node: '>=10'} + hasBin: true + + noise-curve-ed@2.0.1: + resolution: {integrity: sha512-8HMZ40Wmarg8RQjVemLrjB49JSL6eGeOD+tlzaQW5/p+hNPfHFEMC3UZZ57zUqUprMuz6GN+gsPExpz2DWL+iA==} + + noise-handshake@3.1.0: + resolution: {integrity: sha512-0S1qkUvMbTvZCfgr/vSkVT84YyvI4Q0OLwSc5BFxVmjaePrxAwVeXeJDY3A7N/7+qj95gZ15LaNoP9ZnBXH5Lw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nostr-tools@2.10.4: + resolution: {integrity: sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + nostr-wasm@0.1.0: + resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + oxc-resolver@1.12.0: + resolution: {integrity: sha512-YlaCIArvWNKCWZFRrMjhh2l5jK80eXnpYP+bhRc1J/7cW3TiyEY0ngJo73o/5n8hA3+4yLdTmXLNTQ3Ncz50LQ==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + pac-proxy-agent@7.1.0: + resolution: {integrity: sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-manager-detector@0.2.8: + resolution: {integrity: sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + + process-streams@1.0.3: + resolution: {integrity: sha512-xkIaM5vYnyekB88WyET78YEqXiaJRy0xcvIdE22n+myhvBT7LlLmX6iAtq7jDvVH8CUx2rqQsd32JdRyJMV3NA==} + + protomux@3.10.1: + resolution: {integrity: sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + record-cache@1.2.0: + resolution: {integrity: sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + safety-catch@1.0.2: + resolution: {integrity: sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + signal-promise@1.0.3: + resolution: {integrity: sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sodium-native@4.3.1: + resolution: {integrity: sha512-YdP64gAdpIKHfL4ttuX4aIfjeunh9f+hNeQJpE9C8UMndB3zkgZ7YmmGT4J2+v6Ibyp6Wem8D1TcSrtdW0bqtg==} + + sodium-secretstream@1.1.1: + resolution: {integrity: sha512-9lRQtNdQYmANo+sgNjEQafKrd/N4ojqv17E8wOzx3yOCaOJ5Gb4MuXoYq2Nv4Xo9Kt2fOROYcmV24bamu86c8A==} + + sodium-universal@4.0.1: + resolution: {integrity: sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==} + peerDependencies: + sodium-javascript: ~0.8.0 + peerDependenciesMeta: + sodium-javascript: + optional: true + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + steno@4.0.2: + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} + engines: {node: '>=18'} + + streamx@2.21.1: + resolution: {integrity: sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + tar-fs@2.1.2: + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + time-ordered-set@2.0.1: + resolution: {integrity: sha512-VJEKmgSN2UiOLB8BpN8Sh2b9LGMHTP5OPrQRpnKjvOheOyzk0mufbjzjKTIG2gO4A+Y+vDJ+0TcLbpUmMLsg8A==} + + timeout-refresh@2.0.1: + resolution: {integrity: sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + udx-native@1.17.2: + resolution: {integrity: sha512-QAcwyS2ORfZIpdzHQqCmUMTm47ZXNA3bero4WWGP69fjzD6NwBnG4Bolsi3nF1vzrlH6dEP7SkqkGVb7E2nu8A==} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + unique-names-generator@4.7.1: + resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==} + engines: {node: '>=8'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unslab@1.3.0: + resolution: {integrity: sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + + web-push@3.6.7: + resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} + engines: {node: '>= 16'} + hasBin: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-typed-array@1.1.18: + resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xache@1.2.1: + resolution: {integrity: sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + z32@1.1.0: + resolution: {integrity: sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==} + +snapshots: + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@cashu/cashu-ts@2.1.0': + dependencies: + '@cashu/crypto': 0.3.4 + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@scure/bip32': 1.6.1 + buffer: 6.0.3 + + '@cashu/crypto@0.3.4': + dependencies: + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@scure/bip32': 1.6.1 + '@scure/bip39': 1.5.1 + buffer: 6.0.3 + + '@changesets/apply-release-plan@7.0.7': + dependencies: + '@changesets/config': 3.0.5 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.2 + '@changesets/should-skip-package': 0.1.1 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.6.3 + + '@changesets/assemble-release-plan@6.0.5': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.2 + '@changesets/should-skip-package': 0.1.1 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.6.3 + + '@changesets/changelog-git@0.2.0': + dependencies: + '@changesets/types': 6.0.0 + + '@changesets/cli@2.27.11': + dependencies: + '@changesets/apply-release-plan': 7.0.7 + '@changesets/assemble-release-plan': 6.0.5 + '@changesets/changelog-git': 0.2.0 + '@changesets/config': 3.0.5 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.2 + '@changesets/get-release-plan': 4.0.6 + '@changesets/git': 3.0.2 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.1 + '@changesets/read': 0.6.2 + '@changesets/should-skip-package': 0.1.1 + '@changesets/types': 6.0.0 + '@changesets/write': 0.3.2 + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + external-editor: 3.1.0 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.8 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.6.3 + spawndamnit: 3.0.1 + term-size: 2.2.1 + + '@changesets/config@3.0.5': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.2 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.2': + dependencies: + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.6.3 + + '@changesets/get-release-plan@4.0.6': + dependencies: + '@changesets/assemble-release-plan': 6.0.5 + '@changesets/config': 3.0.5 + '@changesets/pre': 2.0.1 + '@changesets/read': 0.6.2 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.0': + dependencies: + '@changesets/types': 6.0.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.2': + dependencies: + '@changesets/git': 3.0.2 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.0 + '@changesets/types': 6.0.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.1': + dependencies: + '@changesets/types': 6.0.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.0.0': {} + + '@changesets/write@0.3.2': + dependencies: + '@changesets/types': 6.0.0 + fs-extra: 7.0.1 + human-id: 1.0.2 + prettier: 2.8.8 + + '@diva.exchange/i2p-sam@5.4.1': + dependencies: + nanoid: 5.0.9 + rfc4648: 1.5.4 + + '@emnapi/core@1.3.1': + dependencies: + '@emnapi/wasi-threads': 1.0.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@holesail/hyper-cmd-lib-net@0.2.1': + dependencies: + bare-dgram: 1.0.1 + + '@hyperswarm/secret-stream@6.7.1': + dependencies: + b4a: 1.6.7 + hypercore-crypto: 3.4.2 + noise-curve-ed: 2.0.1 + noise-handshake: 3.1.0 + sodium-secretstream: 1.1.1 + sodium-universal: 4.0.1 + streamx: 2.21.1 + timeout-refresh: 2.0.1 + unslab: 1.3.0 + transitivePeerDependencies: + - sodium-javascript + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.26.0 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.26.0 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@napi-rs/wasm-runtime@0.2.6': + dependencies: + '@emnapi/core': 1.3.1 + '@emnapi/runtime': 1.3.1 + '@tybys/wasm-util': 0.9.0 + optional: true + + '@noble/ciphers@0.5.3': {} + + '@noble/curves@1.1.0': + dependencies: + '@noble/hashes': 1.3.1 + + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + + '@noble/curves@1.8.0': + dependencies: + '@noble/hashes': 1.7.0 + + '@noble/hashes@1.3.1': {} + + '@noble/hashes@1.3.2': {} + + '@noble/hashes@1.7.0': {} + + '@noble/secp256k1@1.7.1': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.18.0 + + '@oxc-resolver/binding-darwin-arm64@1.12.0': + optional: true + + '@oxc-resolver/binding-darwin-x64@1.12.0': + optional: true + + '@oxc-resolver/binding-freebsd-x64@1.12.0': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@1.12.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@1.12.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@1.12.0': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@1.12.0': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@1.12.0': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@1.12.0': + dependencies: + '@napi-rs/wasm-runtime': 0.2.6 + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@1.12.0': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@1.12.0': + optional: true + + '@pondwader/socks5-server@1.0.10': {} + + '@satellite-earth/core@0.5.0(typescript@5.7.3)': + dependencies: + better-sqlite3: 11.7.2 + blossom-client-sdk: 0.9.1 + blossom-server-sdk: 0.4.0 + cors: 2.8.5 + debug: 4.4.0(supports-color@5.5.0) + express: 4.21.2 + express-async-handler: 1.2.0 + follow-redirects: 1.15.9(debug@4.4.0) + http-error: 0.0.6 + http-errors: 2.0.0 + lowdb: 7.0.1 + mime: 4.0.6 + nostr-tools: 2.10.4(typescript@5.7.3) + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - typescript + - utf-8-validate + + '@scure/base@1.1.1': {} + + '@scure/base@1.2.1': {} + + '@scure/bip32@1.3.1': + dependencies: + '@noble/curves': 1.1.0 + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + + '@scure/bip32@1.6.1': + dependencies: + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@scure/base': 1.2.1 + + '@scure/bip39@1.2.1': + dependencies: + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + + '@scure/bip39@1.5.1': + dependencies: + '@noble/hashes': 1.7.0 + '@scure/base': 1.2.1 + + '@swc-node/core@1.13.3(@swc/core@1.10.7)(@swc/types@0.1.17)': + dependencies: + '@swc/core': 1.10.7 + '@swc/types': 0.1.17 + + '@swc-node/register@1.10.9(@swc/core@1.10.7)(@swc/types@0.1.17)(typescript@5.7.3)': + dependencies: + '@swc-node/core': 1.13.3(@swc/core@1.10.7)(@swc/types@0.1.17) + '@swc-node/sourcemap-support': 0.5.1 + '@swc/core': 1.10.7 + colorette: 2.0.20 + debug: 4.4.0(supports-color@5.5.0) + oxc-resolver: 1.12.0 + pirates: 4.0.6 + tslib: 2.8.1 + typescript: 5.7.3 + transitivePeerDependencies: + - '@swc/types' + - supports-color + + '@swc-node/sourcemap-support@0.5.1': + dependencies: + source-map-support: 0.5.21 + tslib: 2.8.1 + + '@swc/core-darwin-arm64@1.10.7': + optional: true + + '@swc/core-darwin-x64@1.10.7': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.10.7': + optional: true + + '@swc/core-linux-arm64-gnu@1.10.7': + optional: true + + '@swc/core-linux-arm64-musl@1.10.7': + optional: true + + '@swc/core-linux-x64-gnu@1.10.7': + optional: true + + '@swc/core-linux-x64-musl@1.10.7': + optional: true + + '@swc/core-win32-arm64-msvc@1.10.7': + optional: true + + '@swc/core-win32-ia32-msvc@1.10.7': + optional: true + + '@swc/core-win32-x64-msvc@1.10.7': + optional: true + + '@swc/core@1.10.7': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.17 + optionalDependencies: + '@swc/core-darwin-arm64': 1.10.7 + '@swc/core-darwin-x64': 1.10.7 + '@swc/core-linux-arm-gnueabihf': 1.10.7 + '@swc/core-linux-arm64-gnu': 1.10.7 + '@swc/core-linux-arm64-musl': 1.10.7 + '@swc/core-linux-x64-gnu': 1.10.7 + '@swc/core-linux-x64-musl': 1.10.7 + '@swc/core-win32-arm64-msvc': 1.10.7 + '@swc/core-win32-ia32-msvc': 1.10.7 + '@swc/core-win32-x64-msvc': 1.10.7 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.17': + dependencies: + '@swc/counter': 0.1.3 + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/better-sqlite3@7.6.12': + dependencies: + '@types/node': 22.10.6 + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.10.6 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.10.6 + + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.10.6 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/dom-serial@1.0.6': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 22.10.6 + '@types/qs': 6.9.18 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.9.18 + '@types/serve-static': 1.15.7 + + '@types/http-errors@2.0.4': {} + + '@types/lodash.throttle@4.1.9': + dependencies: + '@types/lodash': 4.17.14 + + '@types/lodash@4.17.14': {} + + '@types/mime@1.3.5': {} + + '@types/ms@0.7.34': {} + + '@types/node@12.20.55': {} + + '@types/node@22.10.6': + dependencies: + undici-types: 6.20.0 + + '@types/qs@6.9.18': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.10.6 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.10.6 + '@types/send': 0.17.4 + + '@types/web-push@3.6.4': + dependencies: + '@types/node': 22.10.6 + + '@types/ws@8.5.13': + dependencies: + '@types/node': 22.10.6 + + '@zxing/text-encoding@0.9.0': + optional: true + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + agent-base@7.1.3: {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + applesauce-core@0.10.0(typescript@5.7.3): + dependencies: + '@scure/base': 1.2.1 + debug: 4.4.0(supports-color@5.5.0) + fast-deep-equal: 3.1.3 + hash-sum: 2.0.0 + light-bolt11-decoder: 3.2.0 + nanoid: 5.0.9 + nostr-tools: 2.10.4(typescript@5.7.3) + rxjs: 7.8.1 + transitivePeerDependencies: + - supports-color + - typescript + + applesauce-net@0.10.0(typescript@5.7.3): + dependencies: + applesauce-core: 0.10.0(typescript@5.7.3) + nanoid: 5.0.9 + nostr-tools: 2.10.4(typescript@5.7.3) + rxjs: 7.8.1 + transitivePeerDependencies: + - supports-color + - typescript + + applesauce-signer@0.10.0(typescript@5.7.3): + dependencies: + '@noble/hashes': 1.7.0 + '@noble/secp256k1': 1.7.1 + '@scure/base': 1.2.1 + '@types/dom-serial': 1.0.6 + applesauce-core: 0.10.0(typescript@5.7.3) + applesauce-net: 0.10.0(typescript@5.7.3) + debug: 4.4.0(supports-color@5.5.0) + nanoid: 5.0.9 + nostr-tools: 2.10.4(typescript@5.7.3) + transitivePeerDependencies: + - supports-color + - typescript + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async@3.2.6: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + b4a@1.6.7: {} + + balanced-match@1.0.2: {} + + bare-dgram@1.0.1: + dependencies: + bare-events: 2.5.4 + udx-native: 1.17.2 + + bare-dns@1.0.5: {} + + bare-dns@2.0.2: {} + + bare-events@2.5.4: {} + + bare-net@1.0.1: + dependencies: + bare-events: 2.5.4 + bare-pipe: 3.3.8 + bare-stream: 2.6.4(bare-events@2.5.4) + bare-tcp: 1.9.1 + transitivePeerDependencies: + - bare-buffer + + bare-net@2.0.1: + dependencies: + bare-events: 2.5.4 + bare-pipe: 4.0.2 + bare-stream: 2.6.4(bare-events@2.5.4) + bare-tcp: 2.0.2 + transitivePeerDependencies: + - bare-buffer + + bare-pipe@3.3.8: + dependencies: + bare-events: 2.5.4 + bare-stream: 2.6.4(bare-events@2.5.4) + transitivePeerDependencies: + - bare-buffer + + bare-pipe@4.0.2: + dependencies: + bare-events: 2.5.4 + bare-stream: 2.6.4(bare-events@2.5.4) + transitivePeerDependencies: + - bare-buffer + + bare-stream@2.6.4(bare-events@2.5.4): + dependencies: + streamx: 2.21.1 + optionalDependencies: + bare-events: 2.5.4 + + bare-tcp@1.9.1: + dependencies: + bare-dns: 1.0.5 + bare-events: 2.5.4 + bare-stream: 2.6.4(bare-events@2.5.4) + transitivePeerDependencies: + - bare-buffer + + bare-tcp@2.0.2: + dependencies: + bare-dns: 2.0.2 + bare-events: 2.5.4 + bare-stream: 2.6.4(bare-events@2.5.4) + transitivePeerDependencies: + - bare-buffer + + base64-js@1.5.1: {} + + basic-ftp@5.0.5: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + better-sqlite3@11.7.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.2 + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bits-to-bytes@1.3.0: + dependencies: + b4a: 1.6.7 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blind-relay@1.3.3: + dependencies: + b4a: 1.6.7 + bare-events: 2.5.4 + bits-to-bytes: 1.3.0 + compact-encoding: 2.16.0 + compact-encoding-bitfield: 1.0.0 + hypertrace: 1.4.2 + protomux: 3.10.1 + sodium-universal: 4.0.1 + streamx: 2.21.1 + transitivePeerDependencies: + - sodium-javascript + + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + + blossom-client-sdk@0.9.1: + dependencies: + '@noble/hashes': 1.7.0 + cross-fetch: 4.1.0 + transitivePeerDependencies: + - encoding + + blossom-client-sdk@2.1.1: + dependencies: + '@cashu/cashu-ts': 2.1.0 + '@noble/hashes': 1.7.0 + + blossom-server-sdk@0.4.0: + dependencies: + better-sqlite3: 11.7.2 + debug: 4.4.0(supports-color@5.5.0) + mime: 4.0.6 + minio: 7.1.3 + transitivePeerDependencies: + - supports-color + + bn.js@4.12.1: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bogon@1.1.0: + dependencies: + compact-encoding: 2.16.0 + compact-encoding-net: 1.2.0 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-or-node@2.1.1: {} + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.7 + set-function-length: 1.2.2 + + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.7 + + chardet@0.7.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + ci-info@3.9.0: {} + + colorette@2.0.20: {} + + commander@12.1.0: {} + + compact-encoding-bitfield@1.0.0: + dependencies: + compact-encoding: 2.16.0 + + compact-encoding-net@1.2.0: + dependencies: + compact-encoding: 2.16.0 + + compact-encoding@2.16.0: + dependencies: + b4a: 1.6.7 + + concat-map@0.0.1: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@6.0.2: {} + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.0(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + decode-uri-component@0.2.2: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-indent@6.1.0: {} + + detect-libc@2.0.3: {} + + dht-rpc@6.16.0: + dependencies: + b4a: 1.6.7 + bare-events: 2.5.4 + compact-encoding: 2.16.0 + compact-encoding-net: 1.2.0 + fast-fifo: 1.3.2 + kademlia-routing-table: 1.0.3 + nat-sampler: 1.0.1 + sodium-universal: 4.0.1 + streamx: 2.21.1 + time-ordered-set: 2.0.1 + udx-native: 1.17.2 + transitivePeerDependencies: + - sodium-javascript + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dotenv@16.4.7: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplex-maker@1.0.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.0: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + expand-template@2.0.3: {} + + express-async-handler@1.2.0: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extendable-error@0.1.7: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-xml-parser@4.5.1: + dependencies: + strnum: 1.0.5 + + fastq@1.18.0: + dependencies: + reusify: 1.0.4 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@1.1.0: {} + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + follow-redirects@1.15.9(debug@4.4.0): + optionalDependencies: + debug: 4.4.0(supports-color@5.5.0) + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fs-constants@1.0.0: {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.2.7: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.0 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-port@7.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.0 + + get-uri@6.0.4: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@3.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hash-sum@2.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + holesail-server@1.4.4: + dependencies: + '@holesail/hyper-cmd-lib-net': 0.2.1 + b4a: 1.6.7 + bare-net: 1.0.1 + hyper-cmd-lib-keys: 0.0.2 + hyperdht: 6.20.1 + net: bare-net@2.0.1 + transitivePeerDependencies: + - bare-buffer + - sodium-javascript + + http-error@0.0.6: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + http_ece@1.2.0: {} + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + human-id@1.0.2: {} + + hyper-address@0.1.3: + dependencies: + '@scure/base': 1.2.1 + commander: 12.1.0 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + hyper-cmd-lib-keys@0.0.2: {} + + hyper-socks5-proxy@0.1.2: + dependencies: + '@pondwader/socks5-server': 1.0.10 + '@scure/base': 1.2.1 + commander: 12.1.0 + debug: 4.4.0(supports-color@5.5.0) + hyper-address: 0.1.3 + hyperdht: 6.20.1 + transitivePeerDependencies: + - sodium-javascript + - supports-color + + hypercore-crypto@3.4.2: + dependencies: + b4a: 1.6.7 + compact-encoding: 2.16.0 + sodium-universal: 4.0.1 + transitivePeerDependencies: + - sodium-javascript + + hypercore-id-encoding@1.3.0: + dependencies: + b4a: 1.6.7 + z32: 1.1.0 + + hyperdht@6.20.1: + dependencies: + '@hyperswarm/secret-stream': 6.7.1 + b4a: 1.6.7 + bare-events: 2.5.4 + blind-relay: 1.3.3 + bogon: 1.1.0 + compact-encoding: 2.16.0 + compact-encoding-net: 1.2.0 + dht-rpc: 6.16.0 + hypercore-crypto: 3.4.2 + hypercore-id-encoding: 1.3.0 + noise-curve-ed: 2.0.1 + noise-handshake: 3.1.0 + record-cache: 1.2.0 + safety-catch: 1.0.2 + signal-promise: 1.0.3 + sodium-universal: 4.0.1 + streamx: 2.21.1 + unslab: 1.3.0 + xache: 1.2.1 + transitivePeerDependencies: + - sodium-javascript + + hypertrace@1.4.2: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + ignore@5.3.2: {} + + import-meta-resolve@4.1.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.2.0: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-callable@1.2.7: {} + + is-extglob@2.1.1: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.18 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + jsbn@1.1.0: {} + + json-stream@1.0.0: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + + kademlia-routing-table@1.0.3: + dependencies: + bare-events: 2.5.4 + + light-bolt11-decoder@3.2.0: + dependencies: + '@scure/base': 1.1.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + lodash.throttle@4.1.1: {} + + lodash@4.17.21: {} + + lowdb@7.0.1: + dependencies: + steno: 4.0.2 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mime@4.0.6: {} + + mimic-response@3.1.0: {} + + minimalistic-assert@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimist@1.2.8: {} + + minio@7.1.3: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 0.2.13 + fast-xml-parser: 4.5.1 + ipaddr.js: 2.2.0 + json-stream: 1.0.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + through2: 4.0.2 + web-encoding: 1.1.5 + xml: 1.0.1 + xml2js: 0.5.0 + + mkdirp-classic@0.5.3: {} + + mkdirp@3.0.1: {} + + mri@1.2.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + nanoassert@2.0.0: {} + + nanoid@5.0.9: {} + + napi-build-utils@1.0.2: {} + + nat-sampler@1.0.1: {} + + negotiator@0.6.3: {} + + netmask@2.0.2: {} + + node-abi@3.72.0: + dependencies: + semver: 7.6.3 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + + nodemon@3.1.9: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.6.3 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + noise-curve-ed@2.0.1: + dependencies: + b4a: 1.6.7 + nanoassert: 2.0.0 + sodium-universal: 4.0.1 + transitivePeerDependencies: + - sodium-javascript + + noise-handshake@3.1.0: + dependencies: + b4a: 1.6.7 + nanoassert: 2.0.0 + sodium-universal: 4.0.1 + transitivePeerDependencies: + - sodium-javascript + + normalize-path@3.0.0: {} + + nostr-tools@2.10.4(typescript@5.7.3): + dependencies: + '@noble/ciphers': 0.5.3 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + '@scure/bip32': 1.3.1 + '@scure/bip39': 1.2.1 + optionalDependencies: + nostr-wasm: 0.1.0 + typescript: 5.7.3 + + nostr-wasm@0.1.0: + optional: true + + object-assign@4.1.1: {} + + object-inspect@1.13.3: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + os-tmpdir@1.0.2: {} + + outdent@0.5.0: {} + + oxc-resolver@1.12.0: + optionalDependencies: + '@oxc-resolver/binding-darwin-arm64': 1.12.0 + '@oxc-resolver/binding-darwin-x64': 1.12.0 + '@oxc-resolver/binding-freebsd-x64': 1.12.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 1.12.0 + '@oxc-resolver/binding-linux-arm64-gnu': 1.12.0 + '@oxc-resolver/binding-linux-arm64-musl': 1.12.0 + '@oxc-resolver/binding-linux-x64-gnu': 1.12.0 + '@oxc-resolver/binding-linux-x64-musl': 1.12.0 + '@oxc-resolver/binding-wasm32-wasi': 1.12.0 + '@oxc-resolver/binding-win32-arm64-msvc': 1.12.0 + '@oxc-resolver/binding-win32-x64-msvc': 1.12.0 + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + pac-proxy-agent@7.1.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + get-uri: 6.0.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + package-manager-detector@0.2.8: {} + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-to-regexp@0.1.12: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pify@4.0.1: {} + + pirates@4.0.6: {} + + possible-typed-array-names@1.0.0: {} + + prebuild-install@7.1.2: + dependencies: + detect-libc: 2.0.3 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.72.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.2 + tunnel-agent: 0.6.0 + + prettier@2.8.8: {} + + prettier@3.4.2: {} + + process-streams@1.0.3: + dependencies: + duplex-maker: 1.0.0 + + protomux@3.10.1: + dependencies: + b4a: 1.6.7 + compact-encoding: 2.16.0 + queue-tick: 1.0.1 + safety-catch: 1.0.2 + unslab: 1.3.0 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pstree.remy@1.1.8: {} + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + queue-microtask@1.2.3: {} + + queue-tick@1.0.1: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + record-cache@1.2.0: + dependencies: + b4a: 1.6.7 + + regenerator-runtime@0.14.1: {} + + resolve-from@5.0.0: {} + + reusify@1.0.4: {} + + rfc4648@1.5.4: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + safety-catch@1.0.2: {} + + sax@1.4.1: {} + + semver@7.6.3: {} + + send@0.19.0: + 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 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + signal-promise@1.0.3: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.6.3 + + slash@3.0.0: {} + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + + socks@2.8.3: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + + sodium-native@4.3.1: + dependencies: + node-gyp-build: 4.8.4 + + sodium-secretstream@1.1.1: + dependencies: + b4a: 1.6.7 + sodium-universal: 4.0.1 + transitivePeerDependencies: + - sodium-javascript + + sodium-universal@4.0.1: + dependencies: + sodium-native: 4.3.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + split-on-first@1.1.0: {} + + sprintf-js@1.0.3: {} + + sprintf-js@1.1.3: {} + + statuses@2.0.1: {} + + steno@4.0.2: {} + + streamx@2.21.1: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.5.4 + + strict-uri-encode@2.0.0: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@2.0.1: {} + + strnum@1.0.5: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + tar-fs@2.1.2: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + term-size@2.2.1: {} + + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + time-ordered-set@2.0.1: {} + + timeout-refresh@2.0.1: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@5.7.3: {} + + udx-native@1.17.2: + dependencies: + b4a: 1.6.7 + bare-events: 2.5.4 + node-gyp-build: 4.8.4 + streamx: 2.21.1 + + undefsafe@2.0.5: {} + + undici-types@6.20.0: {} + + unique-names-generator@4.7.1: {} + + universalify@0.1.2: {} + + unpipe@1.0.0: {} + + unslab@1.3.0: + dependencies: + b4a: 1.6.7 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.18 + + utils-merge@1.0.1: {} + + vary@1.1.2: {} + + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + + web-push@3.6.7: + dependencies: + asn1.js: 5.4.1 + http_ece: 1.2.0 + https-proxy-agent: 7.0.6 + jws: 4.0.0 + minimist: 1.2.8 + transitivePeerDependencies: + - supports-color + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-typed-array@1.1.18: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + for-each: 0.3.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + ws@8.18.0: {} + + xache@1.2.1: {} + + xml2js@0.5.0: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xml@1.0.1: {} + + xmlbuilder@11.0.1: {} + + z32@1.1.0: + dependencies: + b4a: 1.6.7 diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/database.ts b/src/app/database.ts new file mode 100644 index 0000000..ba2a6d7 --- /dev/null +++ b/src/app/database.ts @@ -0,0 +1,88 @@ +import EventEmitter from "events"; +import Database, { type Database as SQLDatabase } from "better-sqlite3"; +import { fileURLToPath } from "url"; +import path from "path"; +import fs from "fs"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export type LocalDatabaseConfig = { + directory: string; + name: string; + wal: boolean; +}; + +export default class LocalDatabase extends EventEmitter { + config: LocalDatabaseConfig; + path: { main: string; shm: string; wal: string }; + + db: SQLDatabase; + + constructor(config: Partial) { + super(); + + this.config = { + directory: "data", + name: "events", + wal: true, + ...config, + }; + + this.path = { + main: path.join(this.config.directory, `${this.config.name}.db`), + shm: path.join(this.config.directory, `${this.config.name}.db-shm`), + wal: path.join(this.config.directory, `${this.config.name}.db-wal`), + }; + + // Detect architecture to pass the correct native sqlite module + this.db = new Database(this.path.main); + + if (this.config.wal) this.db.pragma("journal_mode = WAL"); + } + + hasTable(table: string) { + const result = this.db + .prepare(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name=?`) + .get([table]) as { count: number }; + return result.count > 0; + } + + // Delete all events in the database + /** @deprecated this should not be used */ + clear() { + this.db.transaction(() => { + this.db.prepare(`DELETE FROM tags`).run(); + if (this.hasTable("event_labels")) this.db.prepare(`DELETE FROM event_labels`).run(); + this.db.prepare(`DELETE FROM events`).run(); + })(); + } + + // Get number of events in the database + /** @deprecated this should be moved to a report */ + count() { + const result = this.db.prepare(`SELECT COUNT(*) AS events FROM events`).get() as { events: number }; + + return result.events; + } + + // Get total size of the database on disk + size() { + let sum; + + try { + const statMain = fs.statSync(this.path.main).size; + const statShm = this.config.wal ? fs.statSync(this.path.shm).size : 0; + const statWal = this.config.wal ? fs.statSync(this.path.wal).size : 0; + + sum = statMain + statShm + statWal; + } catch (err) { + console.log(err); + } + + return sum; + } + + destroy() { + this.removeAllListeners(); + } +} diff --git a/src/app/index.ts b/src/app/index.ts new file mode 100644 index 0000000..d73a6d3 --- /dev/null +++ b/src/app/index.ts @@ -0,0 +1,410 @@ +import path from "path"; +import { WebSocketServer } from "ws"; +import { createServer, Server } from "http"; +import { IEventStore, NostrRelay, SQLiteEventStore } from "@satellite-earth/core"; +import { getDMRecipient } from "@satellite-earth/core/helpers/nostr"; +import { kinds } from "nostr-tools"; +import { AbstractRelay } from "nostr-tools/abstract-relay"; +import express, { Express } from "express"; +import { EventEmitter } from "events"; +import { SimpleSigner } from "applesauce-signer/signers/simple-signer"; +import cors from "cors"; + +import { logger } from "../logger.js"; +import Database from "./database.js"; + +import { NIP_11_SOFTWARE_URL, SENSITIVE_KINDS } from "../const.js"; +import { OWNER_PUBKEY, PORT } from "../env.js"; + +import ConfigManager from "../modules/config-manager.js"; +import ControlApi from "../modules/control/control-api.js"; +import ConfigActions from "../modules/control/config-actions.js"; +import ReceiverActions from "../modules/control/receiver-actions.js"; +import Receiver from "../modules/receiver/index.js"; +import DatabaseActions from "../modules/control/database-actions.js"; +import DirectMessageManager from "../modules/direct-message-manager.js"; +import DirectMessageActions from "../modules/control/dm-actions.js"; +import AddressBook from "../modules/address-book.js"; +import NotificationsManager from "../modules/notifications/notifications-manager.js"; +import NotificationActions from "../modules/control/notification-actions.js"; +import ProfileBook from "../modules/profile-book.js"; +import ContactBook from "../modules/contact-book.js"; +import CautiousPool from "../modules/cautious-pool.js"; +import RemoteAuthActions from "../modules/control/remote-auth-actions.js"; +import ReportActions from "../modules/control/report-actions.js"; +import LogStore from "../modules/log-store/log-store.js"; +import DecryptionCache from "../modules/decryption-cache/decryption-cache.js"; +import DecryptionCacheActions from "../modules/control/decryption-cache.js"; +import Scrapper from "../modules/scrapper/index.js"; +import LogsActions from "../modules/control/logs-actions.js"; +import ApplicationStateManager from "../modules/state/application-state-manager.js"; +import ScrapperActions from "../modules/control/scrapper-actions.js"; +import InboundNetworkManager from "../modules/network/inbound/index.js"; +import SecretsManager from "../modules/secrets-manager.js"; +import outboundNetwork, { OutboundNetworkManager } from "../modules/network/outbound/index.js"; +import Switchboard from "../modules/switchboard/switchboard.js"; +import Gossip from "../modules/gossip.js"; + +type EventMap = { + listening: []; +}; + +export default class App extends EventEmitter { + running = false; + config: ConfigManager; + secrets: SecretsManager; + state: ApplicationStateManager; + signer: SimpleSigner; + + server: Server; + wss: WebSocketServer; + express: Express; + + inboundNetwork: InboundNetworkManager; + outboundNetwork: OutboundNetworkManager; + + database: Database; + eventStore: IEventStore; + logStore: LogStore; + relay: NostrRelay; + receiver: Receiver; + scrapper: Scrapper; + control: ControlApi; + reports: ReportActions; + pool: CautiousPool; + addressBook: AddressBook; + profileBook: ProfileBook; + contactBook: ContactBook; + directMessageManager: DirectMessageManager; + notifications: NotificationsManager; + decryptionCache: DecryptionCache; + switchboard: Switchboard; + gossip: Gossip; + + constructor(dataPath: string) { + super(); + + this.config = new ConfigManager(path.join(dataPath, "node.json")); + this.config.read(); + + this.secrets = new SecretsManager(path.join(dataPath, "secrets.json")); + this.secrets.read(); + + this.signer = new SimpleSigner(this.secrets.get("nostrKey")); + + // copy the vapid public key over to config so the web ui can access it + // TODO: this should be moved to another place + this.secrets.on("updated", () => { + this.config.data.vapidPublicKey = this.secrets.get("vapidPublicKey"); + + if (this.signer.key !== this.secrets.get("nostrKey")) { + this.signer = new SimpleSigner(this.secrets.get("nostrKey")); + } + }); + + // set owner pubkey from env variable + if (!this.config.data.owner && OWNER_PUBKEY) { + this.config.setField("owner", OWNER_PUBKEY); + } + + // create http and ws server interface + this.server = createServer(); + this.inboundNetwork = new InboundNetworkManager(this); + this.outboundNetwork = outboundNetwork; + + /** make the outbound network reflect the app config */ + this.outboundNetwork.listenToAppConfig(this.config); + + // setup express + this.express = express(); + this.express.use(cors()); + this.setupExpress(); + + // pass requests to express server + this.server.on("request", this.express); + + // create websocket server + this.wss = new WebSocketServer({ server: this.server }); + + // Fix CORS for websocket + this.wss.on("headers", (headers, request) => { + headers.push("Access-Control-Allow-Origin: *"); + }); + + // Init embedded sqlite database + this.database = new Database({ directory: dataPath }); + + // create log managers + this.logStore = new LogStore(this.database.db); + this.logStore.setup(); + + this.state = new ApplicationStateManager(this.database.db); + this.state.setup(); + + // Recognize local relay by matching auth string + this.pool = new CautiousPool((relay: AbstractRelay, challenge: string) => { + for (const [socket, auth] of this.relay.auth) { + if (auth.challenge === challenge) return true; + } + return false; + }); + + // Initialize the event store + this.eventStore = new SQLiteEventStore(this.database.db); + this.eventStore.setup(); + + // setup decryption cache + this.decryptionCache = new DecryptionCache(this.database.db); + this.decryptionCache.setup(); + + // Setup managers user contacts and profiles + this.addressBook = new AddressBook(this); + this.profileBook = new ProfileBook(this); + this.contactBook = new ContactBook(this); + + // Setup the notifications manager + this.notifications = new NotificationsManager(this /*this.eventStore, this.state*/); + this.notifications.webPushKeys = { + publicKey: this.secrets.get("vapidPublicKey"), + privateKey: this.secrets.get("vapidPrivateKey"), + }; + this.notifications.setup(); + + this.eventStore.on("event:inserted", (event) => { + if (this.notifications.shouldNotify(event)) this.notifications.notify(event); + }); + + // Initializes receiver and scrapper for pulling data from remote relays + this.receiver = new Receiver(this); + this.receiver.on("event", (event) => this.eventStore.addEvent(event)); + + this.scrapper = new Scrapper(this); + this.scrapper.setup(); + + // pass events from the scrapper to the event store + this.scrapper.on("event", (event) => this.eventStore.addEvent(event)); + + // Initializes direct message manager for subscribing to DMs + this.directMessageManager = new DirectMessageManager(this); + + // set watchInbox for owner when config is loaded or changed + this.config.on("updated", (config) => { + if (config.owner) this.directMessageManager.watchInbox(config.owner); + }); + + // API for controlling the node + this.control = new ControlApi(this); + this.control.registerHandler(new ConfigActions(this)); + this.control.registerHandler(new ReceiverActions(this)); + this.control.registerHandler(new ScrapperActions(this)); + this.control.registerHandler(new DatabaseActions(this)); + this.control.registerHandler(new DirectMessageActions(this)); + this.control.registerHandler(new NotificationActions(this)); + this.control.registerHandler(new RemoteAuthActions(this)); + this.control.registerHandler(new DecryptionCacheActions(this)); + this.control.registerHandler(new LogsActions(this)); + + // reports + this.reports = new ReportActions(this); + this.control.registerHandler(this.reports); + + // connect control api to websocket server + this.control.attachToServer(this.wss); + + // if process has an RPC interface, attach control api to it + if (process.send) this.control.attachToProcess(process); + + this.relay = new NostrRelay(this.eventStore); + this.relay.sendChallenge = true; + this.relay.requireRelayInAuth = false; + + // NIP-66 gossip + this.gossip = new Gossip(this.inboundNetwork, this.signer, this.pool, this.relay, this.eventStore); + + this.config.on("updated", (config) => { + this.gossip.interval = config.gossipInterval; + this.gossip.broadcastRelays = config.gossipBroadcastRelays; + + if (config.gossipEnabled) this.gossip.start(); + else this.gossip.stop(); + }); + + // setup PROXY switchboard + this.switchboard = new Switchboard(this); + + // attach switchboard to websocket server + this.wss.on("connection", (ws, request) => { + this.switchboard.handleConnection(ws, request); + }); + + // update profiles when conversations are opened + this.directMessageManager.on("open", (a, b) => { + this.profileBook.loadProfile(a, this.addressBook.getOutboxes(a)); + this.profileBook.loadProfile(b, this.addressBook.getOutboxes(b)); + }); + + // only allow the owner to NIP-42 authenticate with the relay + this.relay.checkAuth = (ws, auth) => { + // If owner is not set, update it to match the pubkey + // that signed the auth message. This allows the user + // to set the owner pubkey from the initial login when + // setting up their personal node (the owner pubkey may + // otherwise be set using the env var `OWNER_PUBKEY`) + if (!this.config.data.owner) { + this.config.update((config) => { + logger(`Owner is unset, setting owner to first NIP-42 auth: ${auth.pubkey}`); + config.owner = auth.pubkey; + }); + return true; + } + if (auth.pubkey !== this.config.data.owner) return "Pubkey dose not match owner"; + return true; + }; + + // when the owner NIP-42 authenticates with the relay pass it along to the control + this.relay.on("socket:auth", (ws, auth) => { + if (auth.pubkey === this.config.data.owner) { + this.control.authenticatedConnections.add(ws); + } + }); + + // if socket is unauthenticated only allow owner's events and incoming DMs + this.relay.registerEventHandler((ctx, next) => { + const auth = ctx.relay.getSocketAuth(ctx.socket); + + if (!auth) { + // is it an incoming DM for the owner? + if (ctx.event.kind === kinds.EncryptedDirectMessage && getDMRecipient(ctx.event) === this.config.data.owner) + return next(); + + if (ctx.event.pubkey === this.config.data.owner) return next(); + + throw new Error(ctx.relay.makeAuthRequiredReason("This relay only accepts events from its owner")); + } + + return next(); + }); + + // handle forwarding direct messages by owner + this.relay.registerEventHandler(async (ctx, next) => { + if (ctx.event.kind === kinds.EncryptedDirectMessage && ctx.event.pubkey === this.config.data.owner) { + // send direct message + const results = await this.directMessageManager.forwardMessage(ctx.event); + + if (!results || !results.some((p) => p.status === "fulfilled")) throw new Error("Failed to forward message"); + return `Forwarded message to ${results.filter((p) => p.status === "fulfilled").length}/${results.length} relays`; + } else return next(); + }); + + // block subscriptions for sensitive kinds unless NIP-42 auth or Auth Code + this.relay.registerSubscriptionFilter((ctx, next) => { + // always allow if authenticated with auth code + const isAuthenticatedWithAuthCode = this.control.authenticatedConnections.has(ctx.socket); + if (isAuthenticatedWithAuthCode) return next(); + + const hasSensitiveKinds = ctx.filters.some( + (filter) => filter.kinds && SENSITIVE_KINDS.some((k) => filter.kinds?.includes(k)), + ); + + if (hasSensitiveKinds) { + const auth = ctx.relay.getSocketAuth(ctx.socket); + if (!auth) throw new Error(ctx.relay.makeAuthRequiredReason("Cant view sensitive events without auth")); + } + + return next(); + }); + + // Handle possible additional actions when the event store receives a new message + this.eventStore.on("event:inserted", (event) => { + const loadProfile = (pubkey: string) => { + const profile = this.profileBook.getProfile(pubkey); + if (!profile) { + this.profileBook.loadProfile(pubkey, this.addressBook.getOutboxes(pubkey)); + this.addressBook.loadOutboxes(pubkey).then((outboxes) => { + this.profileBook.loadProfile(pubkey, outboxes ?? undefined); + }); + } + }; + + // Fetch profiles for all incoming DMs + switch (event.kind) { + case kinds.EncryptedDirectMessage: + loadProfile(event.pubkey); + break; + default: + loadProfile(event.pubkey); + break; + } + }); + + // Read the config again, this fires the "loaded" and "updated" events to synchronize all the other services + // NOTE: its important this is called last. otherwise any this.config.on("update") listeners above will note fire + this.config.read(); + } + + setupExpress() { + this.express.get("/health", (req, res) => { + res.status(200).send("Healthy"); + }); + + // NIP-11 + this.express.get("/", (req, res, next) => { + if (req.headers.accept === "application/nostr+json") { + res.send({ + name: this.config.data.name, + description: this.config.data.description, + software: NIP_11_SOFTWARE_URL, + supported_nips: NostrRelay.SUPPORTED_NIPS, + pubkey: this.config.data.owner, + }); + } else return next(); + }); + } + + async start() { + this.running = true; + await this.config.read(); + + if (this.config.data.runReceiverOnBoot) this.receiver.start(); + if (this.config.data.runScrapperOnBoot) this.scrapper.start(); + + this.tick(); + + // start http server listening + await new Promise((res) => this.server.listen(PORT, () => res())); + + logger(`Listening on`, PORT); + + if (process.send) process.send({ type: "RELAY_READY" }); + + this.emit("listening"); + + await this.inboundNetwork.start(); + } + + tick() { + if (!this.running) return; + + setTimeout(this.tick.bind(this), 100); + } + + async stop() { + this.running = false; + this.config.write(); + this.scrapper.stop(); + this.receiver.stop(); + await this.state.saveAll(); + this.reports.cleanup(); + this.relay.stop(); + this.database.destroy(); + this.receiver.destroy(); + + await this.inboundNetwork.stop(); + await this.outboundNetwork.stop(); + + this.gossip.stop(); + + // wait for server to close + await new Promise((res) => this.server.close(() => res())); + } +} diff --git a/src/const.ts b/src/const.ts new file mode 100644 index 0000000..7998374 --- /dev/null +++ b/src/const.ts @@ -0,0 +1,7 @@ +import { EncryptedDirectMessage } from "nostr-tools/kinds"; + +export const SENSITIVE_KINDS = [EncryptedDirectMessage]; + +export const NIP_11_SOFTWARE_URL = "git+https://github.com/hzrd149/bakery.git"; + +export const OUTBOUND_PROXY_TYPES = ["SOCKS5", "HTTP"]; diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..784cf67 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,33 @@ +import "dotenv/config.js"; +import { OUTBOUND_PROXY_TYPES } from "./const.js"; + +import { safeRelayUrls } from "applesauce-core/helpers"; +import { normalizeToHexPubkey } from "./helpers/nip19.js"; + +export const OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined; +export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS; +export const DATA_PATH = process.env.DATA_PATH || "./data"; +export const PORT = parseInt(process.env.PORT ?? "") || 3000; + +// I2P config +export const I2P_PROXY = process.env.I2P_PROXY; +export const I2P_PROXY_TYPE = (process.env.I2P_PROXY_TYPE || "SOCKS5") as "SOCKS5" | "HTTP"; +export const I2P_SAM_ADDRESS = process.env.I2P_SAM_ADDRESS; + +if (!OUTBOUND_PROXY_TYPES.includes(I2P_PROXY_TYPE)) throw new Error("Invalid I2P_PROXY_TYPE, must be SOCKS5 or HTTP"); + +// Tor config +export const TOR_PROXY = process.env.TOR_PROXY; +export const TOR_PROXY_TYPE = (process.env.TOR_PROXY_TYPE || "SOCKS5") as "SOCKS5" | "HTTP"; +export const TOR_ADDRESS = process.env.TOR_ADDRESS; + +if (!OUTBOUND_PROXY_TYPES.includes(TOR_PROXY_TYPE)) throw new Error("Invalid TOR_PROXY_TYPE, must be SOCKS5 or HTTP"); + +// Default relay config +export const BOOTSTRAP_RELAYS = process.env.BOOTSTRAP_RELAYS + ? safeRelayUrls(process.env.BOOTSTRAP_RELAYS.split(",")) + : safeRelayUrls(["wss://nos.lol", "wss://relay.damus.io", "wss://relay.nostr.band"]); + +export const COMMON_CONTACT_RELAYS = process.env.COMMON_CONTACT_RELAYS + ? safeRelayUrls(process.env.COMMON_CONTACT_RELAYS.split(",")) + : safeRelayUrls(["wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"]); diff --git a/src/helpers/fs.ts b/src/helpers/fs.ts new file mode 100644 index 0000000..d69f656 --- /dev/null +++ b/src/helpers/fs.ts @@ -0,0 +1,9 @@ +import pfs from "fs/promises"; + +export async function pathExists(path: string) { + try { + await pfs.stat(path); + return true; + } catch (error) {} + return false; +} diff --git a/src/helpers/ip.ts b/src/helpers/ip.ts new file mode 100644 index 0000000..e968969 --- /dev/null +++ b/src/helpers/ip.ts @@ -0,0 +1,19 @@ +import os from 'node:os'; + +export function getIPAddresses() { + var ifaces = os.networkInterfaces(); + var addresses: string[] = []; + + for (const [name, info] of Object.entries(ifaces)) { + if (!info) continue; + + for (const interfaceInfo of info) { + // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses + if (interfaceInfo.internal) continue; + + addresses.push(interfaceInfo.address); + } + } + + return addresses; +} diff --git a/src/helpers/json.ts b/src/helpers/json.ts new file mode 100644 index 0000000..5dc2e9a --- /dev/null +++ b/src/helpers/json.ts @@ -0,0 +1,27 @@ +import fs from "fs"; + +const loadJson = (params: { path: string }) => { + let object; + + try { + const data = fs.readFileSync(params.path); + + object = JSON.parse(data.toString("utf8")); + } catch (err) { + console.log(err); + } + + if (object) { + return object; + } +}; + +const saveJson = (data: any, params: { path: string }) => { + try { + fs.writeFileSync(params.path, Buffer.from(JSON.stringify(data))); + } catch (err) { + console.log(err); + } +}; + +export { loadJson, saveJson }; diff --git a/src/helpers/network.ts b/src/helpers/network.ts new file mode 100644 index 0000000..6ebeea6 --- /dev/null +++ b/src/helpers/network.ts @@ -0,0 +1,23 @@ +import net from 'net'; + +export function testTCPConnection(host: string, port: number, timeout = 5000) { + return new Promise((resolve, reject) => { + const socket = new net.Socket(); + + const timer = setTimeout(() => { + socket.destroy(); + reject(new Error('Connection timed out')); + }, timeout); + + socket.connect(port, host, () => { + clearTimeout(timer); + socket.destroy(); + resolve(true); + }); + + socket.on('error', (err) => { + clearTimeout(timer); + reject(err); + }); + }); +} diff --git a/src/helpers/nip19.ts b/src/helpers/nip19.ts new file mode 100644 index 0000000..750ce26 --- /dev/null +++ b/src/helpers/nip19.ts @@ -0,0 +1,12 @@ +import { getPubkeyFromDecodeResult, isHexKey } from "applesauce-core/helpers"; +import { nip19 } from "nostr-tools"; + +export function normalizeToHexPubkey(hex: string) { + if (isHexKey(hex)) return hex; + try { + const decode = nip19.decode(hex); + return getPubkeyFromDecodeResult(decode) ?? null; + } catch (error) { + return null; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a2f1e84 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,87 @@ +#!/usr/bin/env node +import process from "node:process"; +import path from "node:path"; +import pfs from "fs/promises"; + +import express, { Request } from "express"; +import { mkdirp } from "mkdirp"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration.js"; +import localizedFormat from "dayjs/plugin/localizedFormat.js"; +import { useWebSocketImplementation } from "nostr-tools/relay"; + +import OutboundProxyWebSocket from "./modules/network/outbound/websocket.js"; +import App from "./app/index.js"; +import { DATA_PATH, PUBLIC_ADDRESS } from "./env.js"; +import { addListener, logger } from "./logger.js"; +import { pathExists } from "./helpers/fs.js"; + +// add durations plugin +dayjs.extend(duration); +dayjs.extend(localizedFormat); + +// @ts-expect-error +global.WebSocket = OutboundProxyWebSocket; +useWebSocketImplementation(OutboundProxyWebSocket); + +// create app +await mkdirp(DATA_PATH); +const app = new App(DATA_PATH); + +// connect logger to app LogStore +addListener(({ namespace }, ...args) => { + app.logStore.addEntry(namespace, Date.now(), args.join(" ")); +}); + +function getPublicRelayAddressFromRequest(req: Request) { + let url: URL; + if (PUBLIC_ADDRESS) { + url = new URL(PUBLIC_ADDRESS); + } else { + url = new URL("/", req.protocol + "://" + req.hostname); + } + url.protocol = req.protocol === "https:" ? "wss:" : "ws:"; + + return url; +} + +// if the app isn't setup redirect to the setup view +app.express.get("/", (req, res, next) => { + if (!app.config.data.owner) { + logger("Redirecting to setup view"); + + const url = new URL("/setup", req.protocol + "://" + req.headers["host"]); + const relay = getPublicRelayAddressFromRequest(req); + url.searchParams.set("relay", relay.toString()); + res.redirect(url.toString()); + } else return next(); +}); + +// serve the web ui or redirect to another hosted version +const appDir = (await pathExists("./nostrudel/dist")) ? "./nostrudel/dist" : "./public"; +app.express.use(express.static(appDir)); +app.express.get("*", (req, res) => { + res.sendFile(path.resolve(appDir, "index.html")); +}); + +// log uncaught errors +process.on("unhandledRejection", (reason, promise) => { + if (reason instanceof Error) { + console.log("Unhandled Rejection"); + console.log(reason); + } else console.log("Unhandled Rejection at:", promise, "reason:", reason); +}); + +// start the app +await app.start(); + +// shutdown process +async function shutdown() { + logger("shutting down"); + + await app.stop(); + + process.exit(0); +} +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..c836cf7 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,25 @@ +import debug, { Debugger } from "debug"; + +if (!process.env.DEBUG) debug.enable("bakery,bakery:*"); + +type Listener = (logger: Debugger, ...args: any[]) => void; +const listeners = new Set(); + +export function addListener(listener: Listener) { + listeners.add(listener); +} +export function removeListener(listener: Listener) { + listeners.delete(listener); +} + +// listen for logs +debug.log = function (this: Debugger, ...args: any[]) { + for (const listener of listeners) { + listener(this, ...args); + } + console.log.apply(this, args); +}; + +const logger = debug("bakery"); + +export { logger }; diff --git a/src/modules/address-book.ts b/src/modules/address-book.ts new file mode 100644 index 0000000..15a8d5a --- /dev/null +++ b/src/modules/address-book.ts @@ -0,0 +1,66 @@ +import { NostrEvent, kinds } from 'nostr-tools'; +import _throttle from 'lodash.throttle'; + +import { getInboxes, getOutboxes } from '@satellite-earth/core/helpers/nostr/mailboxes.js'; +import { logger } from '../logger.js'; +import App from '../app/index.js'; +import PubkeyBatchLoader from './pubkey-batch-loader.js'; + +/** Loads 10002 events for pubkeys */ +export default class AddressBook { + log = logger.extend('AddressBook'); + app: App; + loader: PubkeyBatchLoader; + + get extraRelays() { + return this.loader.extraRelays; + } + set extraRelays(v: string[]) { + this.loader.extraRelays = v; + } + + constructor(app: App) { + this.app = app; + + this.loader = new PubkeyBatchLoader(kinds.RelayList, this.app.pool, (pubkey) => { + return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.RelayList], authors: [pubkey] }])?.[0]; + }); + + this.loader.on('event', (event) => this.app.eventStore.addEvent(event)); + this.loader.on('batch', (found, failed) => { + this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`); + }); + } + + getMailboxes(pubkey: string) { + return this.loader.getEvent(pubkey); + } + + getOutboxes(pubkey: string) { + const mailboxes = this.getMailboxes(pubkey); + return mailboxes && getOutboxes(mailboxes); + } + + getInboxes(pubkey: string) { + const mailboxes = this.getMailboxes(pubkey); + return mailboxes && getInboxes(mailboxes); + } + + handleEvent(event: NostrEvent) { + this.loader.handleEvent(event); + } + + async loadMailboxes(pubkey: string, relays?: string[]) { + return this.loader.getOrLoadEvent(pubkey, relays); + } + + async loadOutboxes(pubkey: string, relays?: string[]) { + const mailboxes = await this.loadMailboxes(pubkey, relays); + return mailboxes && getOutboxes(mailboxes); + } + + async loadInboxes(pubkey: string, relays?: string[]) { + const mailboxes = await this.loadMailboxes(pubkey, relays); + return mailboxes && getInboxes(mailboxes); + } +} diff --git a/src/modules/cautious-pool.ts b/src/modules/cautious-pool.ts new file mode 100644 index 0000000..937cf82 --- /dev/null +++ b/src/modules/cautious-pool.ts @@ -0,0 +1,95 @@ +import EventEmitter from 'events'; +import { SimplePool, VerifiedEvent } from 'nostr-tools'; +import { AbstractRelay } from 'nostr-tools/relay'; +import { normalizeURL } from 'nostr-tools/utils'; + +import { logger } from '../logger.js'; + +export type TestRelay = (relay: AbstractRelay, challenge: string) => boolean; + +type EventMap = { + challenge: [AbstractRelay, string]; + connected: [AbstractRelay]; + closed: [AbstractRelay]; +}; + +export default class CautiousPool extends SimplePool { + log = logger.extend('CautiousPool'); + isSelf?: TestRelay; + blacklist = new Set(); + + challenges = new Map(); + authenticated = new Map(); + + emitter = new EventEmitter(); + constructor(isSelf?: TestRelay) { + super(); + + this.isSelf = isSelf; + } + + async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise { + url = normalizeURL(url); + + const parsed = new URL(url); + if (parsed.host === 'localhost' || parsed.host === '127.0.0.1') throw new Error('Cant connect to localhost'); + + if (this.blacklist.has(url)) throw new Error('Cant connect to self'); + + const relay = await super.ensureRelay(url, params); + if (this.checkRelay(relay)) throw new Error('Cant connect to self'); + + this.emitter.emit('connected', relay); + + relay._onauth = (challenge) => { + if (this.checkRelay(relay, challenge)) { + this.authenticated.set(relay.url, false); + this.challenges.set(relay.url, challenge); + this.emitter.emit('challenge', relay, challenge); + } + }; + + relay.onnotice = () => {}; + + relay.onclose = () => { + this.challenges.delete(relay.url); + this.authenticated.delete(relay.url); + this.emitter.emit('closed', relay); + }; + + return relay; + } + + private checkRelay(relay: AbstractRelay, challenge?: string) { + // @ts-expect-error + challenge = challenge || relay.challenge; + + if (challenge) { + if (this.isSelf && this.isSelf(relay, challenge)) { + this.log(`Found ${relay.url} connects to ourselves, adding to blacklist`); + this.blacklist.add(relay.url); + relay.close(); + relay.connect = () => { + throw new Error('Cant connect to self'); + }; + return true; + } + } + + return false; + } + + isAuthenticated(relay: string | AbstractRelay) { + return !!this.authenticated.get(typeof relay === 'string' ? relay : relay.url); + } + + async authenticate(url: string | AbstractRelay, auth: VerifiedEvent) { + const relay = typeof url === 'string' ? await this.ensureRelay(url) : url; + + return await relay.auth(async (draft) => auth); + } + + [Symbol.iterator](): IterableIterator<[string, AbstractRelay]> { + return this.relays[Symbol.iterator](); + } +} diff --git a/src/modules/community-multiplexer.ts b/src/modules/community-multiplexer.ts new file mode 100644 index 0000000..c2bc5ee --- /dev/null +++ b/src/modules/community-multiplexer.ts @@ -0,0 +1,89 @@ +import { type Database } from 'better-sqlite3'; +import { WebSocket, WebSocketServer } from 'ws'; +import { type IncomingMessage } from 'http'; +import { randomBytes } from 'crypto'; +import { NostrEvent, SimplePool } from 'nostr-tools'; + +import { HyperConnectionManager } from './hyper-connection-manager.js'; +import { logger } from '../logger.js'; +import { CommunityProxy } from './community-proxy.js'; +import { IEventStore } from '@satellite-earth/core'; + +export class CommunityMultiplexer { + log = logger.extend('community-multiplexer'); + db: Database; + eventStore: IEventStore; + pool: SimplePool; + connectionManager: HyperConnectionManager; + + communities = new Map(); + + constructor(db: Database, eventStore: IEventStore) { + this.db = db; + this.eventStore = eventStore; + this.pool = new SimplePool(); + + this.connectionManager = new HyperConnectionManager(randomBytes(32).toString('hex')); + + this.syncCommunityDefinitions(); + } + + attachToServer(wss: WebSocketServer) { + wss.on('connection', this.handleConnection.bind(this)); + } + + handleConnection(ws: WebSocket, req: IncomingMessage) { + if (!req.url) return false; + + const url = new URL(req.url, `http://${req.headers.host}`); + const pubkey = url.pathname.split('/')[1] as string | undefined; + if (!pubkey || pubkey.length !== 64) return false; + + try { + let community = this.communities.get(pubkey); + if (!community) community = this.getCommunityProxy(pubkey); + + // connect the socket to the relay + community.relay.handleConnection(ws, req); + return true; + } catch (error) { + this.log('Failed handle ws connection to', pubkey); + console.log(error); + return false; + } + } + + syncCommunityDefinitions() { + this.log('Syncing community definitions'); + const sub = this.pool.subscribeMany(['wss://nostrue.com'], [{ kinds: [12012] }], { + onevent: (event) => this.eventStore.addEvent(event), + oneose: () => sub.close(), + }); + } + + getCommunityProxy(pubkey: string) { + this.log('Looking for community definition', pubkey); + let definition: NostrEvent | undefined = undefined; + + const local = this.eventStore.getEventsForFilters([{ kinds: [12012], authors: [pubkey] }]); + if (local[0]) definition = local[0]; + + if (!definition) throw new Error('Failed to find community definition'); + + this.log('Creating community proxy', pubkey); + const community = new CommunityProxy(this.db, definition, this.connectionManager); + + community.connect(); + this.communities.set(pubkey, community); + + return community; + } + + stop() { + for (const [pubkey, community] of this.communities) { + community.stop(); + } + this.communities.clear(); + this.connectionManager.stop(); + } +} diff --git a/src/modules/community-proxy.ts b/src/modules/community-proxy.ts new file mode 100644 index 0000000..3d3a79c --- /dev/null +++ b/src/modules/community-proxy.ts @@ -0,0 +1,175 @@ +import { type Database } from "better-sqlite3"; +import { Debugger } from "debug"; +import { Filter, NostrEvent, Relay, kinds } from "nostr-tools"; +import { NostrRelay, RelayActions } from "@satellite-earth/core"; +import { Subscription } from "nostr-tools/abstract-relay"; + +import { LabeledEventStore } from "./labeled-event-store.js"; +import { HyperConnectionManager } from "./hyper-connection-manager.js"; +import { logger } from "../logger.js"; + +/** Used to connect to and sync with remote communities */ +export class CommunityProxy { + log: Debugger; + database: Database; + connectionManager: HyperConnectionManager; + definition: NostrEvent; + + upstream?: Relay; + eventStore: LabeledEventStore; + relay: NostrRelay; + + get addresses() { + return this.definition.tags.filter((t) => t[0] === "r" && t[1]).map((t) => t[1]); + } + + constructor(database: Database, communityDefinition: NostrEvent, connectionManager: HyperConnectionManager) { + this.database = database; + this.connectionManager = connectionManager; + this.definition = communityDefinition; + this.log = logger.extend("community-proxy:" + communityDefinition.pubkey); + + this.eventStore = new LabeledEventStore(this.database, communityDefinition.pubkey); + this.eventStore.setup(); + this.relay = new NostrRelay(this.eventStore); + + // handle incoming events and pass them to the upstream relay + this.relay.registerEventHandler(async (ctx, next) => { + // send event to upstream relay + if (this.upstream) { + const result = this.upstream.publish(ctx.event); + this.log("Sent event to upstream", ctx.event.id); + return result; + } else throw new Error("Not connected to upstream"); + }); + + this.relay.on("subscription:created", (subscription, ws) => { + this.syncChannelsFromFilters(subscription.filters); + }); + this.relay.on("subscription:updated", (subscription, ws) => { + this.syncChannelsFromFilters(subscription.filters); + }); + } + + protected async connectUpstream() { + if (this.upstream) { + if (this.upstream.connected) this.upstream.close(); + this.upstream = undefined; + } + + const hyperAddress = this.definition.tags.find((t) => t[0] === "r" && t[1] && t[2] === "hyper")?.[1]; + let address = this.definition.tags.find((t) => t[0] === "r" && t[1].startsWith("ws"))?.[1]; + + if (hyperAddress) { + const serverInfo = await this.connectionManager.getLocalAddress(hyperAddress); + address = new URL(`ws://${serverInfo.address}:${serverInfo.port}`).toString(); + } + + if (!address) throw new Error("Failed to find connection address"); + + try { + this.log("Connecting to upstream", address); + this.upstream = await Relay.connect(address); + + this.upstream.onclose = () => { + this.log("Upstream connection closed"); + this.upstream = undefined; + }; + } catch (error) { + this.log("Failed to connect to upstream"); + if (error instanceof Error) this.log(error); + } + } + + async connect() { + if (this.upstream) return; + await this.connectUpstream(); + + setTimeout(() => { + this.syncMetadata(); + this.syncDeletions(); + }, 100); + } + + handleEvent(event: NostrEvent) { + try { + switch (event.kind) { + case kinds.EventDeletion: + this.handleDeleteEvent(event); + break; + default: + this.eventStore.addEvent(event); + break; + } + } catch (error) { + this.log("Failed to handle event"); + console.log(error); + } + } + + handleDeleteEvent(deleteEvent: NostrEvent) { + const communityPubkey = this.definition.pubkey; + + const ids = RelayActions.handleDeleteEvent( + this.eventStore, + deleteEvent, + deleteEvent.pubkey === communityPubkey ? () => true : undefined, + ); + + if (ids.length) this.log(`Deleted`, ids.length, "events"); + } + + syncMetadata() { + if (!this.upstream) return; + + this.log("Opening subscription to sync metadata"); + this.upstream.subscribe([{ kinds: [kinds.Metadata, kinds.RelayList, 12012, 39000, 39001, 39002] }], { + id: "metadata-sync", + onevent: (event) => this.handleEvent(event), + onclose: () => this.log("Closed metadata sync"), + }); + } + + syncDeletions() { + if (!this.upstream) return; + + this.log("Opening subscription to sync deletions"); + + this.upstream.subscribe([{ kinds: [kinds.EventDeletion] }], { + id: "deletion-sync", + onevent: (event) => this.handleEvent(event), + onclose: () => this.log("Closed deletion sync"), + }); + } + + private syncChannelsFromFilters(filters: Filter[]) { + const channels = new Set(); + for (const filter of filters) { + if (filter["#h"]) filter["#h"].forEach((c) => channels.add(c)); + } + for (const channel of channels) { + this.syncChannel(channel); + } + } + + channelSubs = new Map(); + syncChannel(channel: string) { + if (!this.upstream) return; + if (this.channelSubs.has(channel)) return; + + this.log("Opening subscription to sync channel", channel); + const sub = this.upstream.subscribe([{ kinds: [9, 10, 11, 12], "#h": [channel] }], { + id: `channel-${channel}-sync`, + onevent: (event) => this.eventStore.addEvent(event), + onclose: () => { + this.channelSubs.delete(channel); + }, + }); + + this.channelSubs.set(channel, sub); + } + + stop() { + this.upstream?.close(); + } +} diff --git a/src/modules/config-manager.ts b/src/modules/config-manager.ts new file mode 100644 index 0000000..22201f8 --- /dev/null +++ b/src/modules/config-manager.ts @@ -0,0 +1,61 @@ +import { JSONFileSync } from "lowdb/node"; +import _throttle from "lodash.throttle"; +import { uniqueNamesGenerator, adjectives, colors, animals } from "unique-names-generator"; +import { PrivateNodeConfig } from "@satellite-earth/core/types/private-node-config.js"; +import { ReactiveJsonFileSync } from "@satellite-earth/core"; + +import { logger } from "../logger.js"; + +export const defaultConfig: PrivateNodeConfig = { + name: uniqueNamesGenerator({ + dictionaries: [colors, adjectives, animals], + }), + description: "", + + autoListen: false, + runReceiverOnBoot: true, + runScrapperOnBoot: false, + logsEnabled: true, + requireReadAuth: false, + publicAddresses: [], + + hyperEnabled: false, + + enableTorConnections: true, + enableI2PConnections: true, + enableHyperConnections: false, + routeAllTrafficThroughTor: false, + + gossipEnabled: false, + gossipInterval: 10 * 60_000, + gossipBroadcastRelays: [], +}; + +export default class ConfigManager extends ReactiveJsonFileSync { + log = logger.extend("ConfigManager"); + + constructor(path: string) { + super(new JSONFileSync(path), defaultConfig); + + this.on("loaded", (config) => { + // explicitly set default values if fields are not set + for (const [key, value] of Object.entries(defaultConfig)) { + // @ts-expect-error + if (config[key] === undefined) { + // @ts-expect-error + config[key] = value; + } + } + + this.write(); + }); + } + + setField(field: keyof PrivateNodeConfig, value: any) { + this.log(`Setting ${field} to ${value}`); + // @ts-expect-error + this.data[field] = value; + + this.write(); + } +} diff --git a/src/modules/contact-book.ts b/src/modules/contact-book.ts new file mode 100644 index 0000000..d622ed3 --- /dev/null +++ b/src/modules/contact-book.ts @@ -0,0 +1,54 @@ +import { NostrEvent, kinds } from 'nostr-tools'; +import _throttle from 'lodash.throttle'; + +import { COMMON_CONTACT_RELAYS } from '../env.js'; +import { logger } from '../logger.js'; +import App from '../app/index.js'; +import PubkeyBatchLoader from './pubkey-batch-loader.js'; + +/** Loads 3 contact lists for pubkeys */ +export default class ContactBook { + log = logger.extend('ContactsBook'); + app: App; + loader: PubkeyBatchLoader; + extraRelays = COMMON_CONTACT_RELAYS; + + constructor(app: App) { + this.app = app; + + this.loader = new PubkeyBatchLoader(kinds.Contacts, this.app.pool, (pubkey) => { + return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.Contacts], authors: [pubkey] }])?.[0]; + }); + + this.loader.on('event', (event) => this.app.eventStore.addEvent(event)); + this.loader.on('batch', (found, failed) => { + this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`); + }); + } + + getContacts(pubkey: string) { + return this.loader.getEvent(pubkey); + } + + getFollowedPubkeys(pubkey: string): string[] { + const contacts = this.getContacts(pubkey); + if (contacts) { + return contacts.tags + .filter((tag) => { + return tag[0] === 'p'; + }) + .map((tag) => { + return tag[1]; + }); + } + return []; + } + + handleEvent(event: NostrEvent) { + this.loader.handleEvent(event); + } + + async loadContacts(pubkey: string, relays: string[] = []) { + return this.loader.getOrLoadEvent(pubkey, relays); + } +} diff --git a/src/modules/control/config-actions.ts b/src/modules/control/config-actions.ts new file mode 100644 index 0000000..f9c713c --- /dev/null +++ b/src/modules/control/config-actions.ts @@ -0,0 +1,49 @@ +import { WebSocket } from 'ws'; +import { ConfigMessage, ConfigResponse } from '@satellite-earth/core/types/control-api/config.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; + +/** handles ['CONTROL', 'CONFIG', ...] messages */ +export default class ConfigActions implements ControlMessageHandler { + app: App; + name = 'CONFIG'; + + private subscribed = new Set(); + + constructor(app: App) { + this.app = app; + + // when config changes send it to the subscribed sockets + this.app.config.on('changed', (config) => { + for (const sock of this.subscribed) { + this.send(sock, ['CONTROL', 'CONFIG', 'CHANGED', config]); + } + }); + } + + handleMessage(sock: WebSocket | NodeJS.Process, message: ConfigMessage) { + const method = message[2]; + switch (method) { + case 'SUBSCRIBE': + this.subscribed.add(sock); + sock.once('close', () => this.subscribed.delete(sock)); + this.send(sock, ['CONTROL', 'CONFIG', 'CHANGED', this.app.config.data]); + return true; + + case 'SET': + const field = message[3]; + const value = message[4]; + + this.app.config.setField(field, value); + return true; + + default: + return false; + } + } + + send(sock: WebSocket | NodeJS.Process, response: ConfigResponse) { + sock.send?.(JSON.stringify(response)); + } +} diff --git a/src/modules/control/control-api.ts b/src/modules/control/control-api.ts new file mode 100644 index 0000000..7191524 --- /dev/null +++ b/src/modules/control/control-api.ts @@ -0,0 +1,130 @@ +import { WebSocket, WebSocketServer } from 'ws'; +import { type IncomingMessage } from 'http'; +import { ControlResponse } from '@satellite-earth/core/types/control-api/index.js'; + +import type App from '../../app/index.js'; +import { logger } from '../../logger.js'; + +export type ControlMessage = ['CONTROL', string, string, ...any[]]; +export interface ControlMessageHandler { + app: App; + name: string; + handleConnection?(ws: WebSocket | NodeJS.Process): void; + handleDisconnect?(socket: WebSocket): void; + handleMessage(sock: WebSocket | NodeJS.Process, message: ControlMessage): boolean | Promise; +} + +/** handles web socket connections and 'CONTROL' messages */ +export default class ControlApi { + app: App; + auth?: string; + log = logger.extend('ControlApi'); + handlers = new Map(); + + authenticatedConnections = new Set(); + + constructor(app: App, auth?: string) { + this.app = app; + this.auth = auth; + } + + registerHandler(handler: ControlMessageHandler) { + this.handlers.set(handler.name, handler); + } + unregisterHandler(handler: ControlMessageHandler) { + this.handlers.delete(handler.name); + } + + /** start listening for incoming ws connections */ + attachToServer(wss: WebSocketServer) { + wss.on('connection', this.handleConnection.bind(this)); + } + + handleConnection(ws: WebSocket, req: IncomingMessage) { + ws.on('message', (data, isBinary) => { + this.handleRawMessage(ws, data as Buffer); + }); + + for (const [id, handler] of this.handlers) { + handler.handleConnection?.(ws); + } + + ws.once('close', () => this.handleDisconnect(ws)); + } + handleDisconnect(ws: WebSocket) { + this.authenticatedConnections.delete(ws); + + for (const [id, handler] of this.handlers) { + handler.handleDisconnect?.(ws); + } + } + + attachToProcess(p: NodeJS.Process) { + p.on('message', (message) => { + if ( + Array.isArray(message) && + message[0] === 'CONTROL' && + typeof message[1] === 'string' && + typeof message[2] === 'string' + ) { + this.handleMessage(p, message as ControlMessage); + } + }); + + for (const [id, handler] of this.handlers) { + handler.handleConnection?.(p); + } + } + + /** handle a ws message */ + async handleRawMessage(ws: WebSocket | NodeJS.Process, message: Buffer) { + try { + const data = JSON.parse(message.toString()) as string[]; + + try { + if ( + Array.isArray(data) && + data[0] === 'CONTROL' && + typeof data[1] === 'string' && + typeof data[2] === 'string' + ) { + if (this.authenticatedConnections.has(ws) || data[1] === 'AUTH') { + await this.handleMessage(ws, data as ControlMessage); + } + } + } catch (err) { + this.log('Failed to handle Control message', message.toString('utf-8')); + this.log(err); + } + } catch (error) { + // failed to parse JSON, do nothing + } + } + + /** handle a ['CONTROL', ...] message */ + async handleMessage(sock: WebSocket | NodeJS.Process, message: ControlMessage) { + // handle ['CONTROL', 'AUTH', ] messages + if (message[1] === 'AUTH' && message[2] === 'CODE') { + const code = message[3]; + if (code === this.auth) { + this.authenticatedConnections.add(sock); + this.send(sock, ['CONTROL', 'AUTH', 'SUCCESS']); + } else { + this.send(sock, ['CONTROL', 'AUTH', 'INVALID', 'Invalid Auth Code']); + } + return true; + } + + const handler = this.handlers.get(message[1]); + if (handler) { + return await handler.handleMessage(sock, message); + } + + this.log('Failed to handle Control message', message); + return false; + } + + send(sock: WebSocket | NodeJS.Process, response: ControlResponse) { + sock.send?.(JSON.stringify(response)); + } +} diff --git a/src/modules/control/database-actions.ts b/src/modules/control/database-actions.ts new file mode 100644 index 0000000..692fcc8 --- /dev/null +++ b/src/modules/control/database-actions.ts @@ -0,0 +1,66 @@ +import { WebSocket } from "ws"; +import os from "node:os"; +import { DatabaseMessage, DatabaseResponse, DatabaseStats } from "@satellite-earth/core/types/control-api/database.js"; + +import App from "../../app/index.js"; +import { ControlMessageHandler } from "./control-api.js"; + +export default class DatabaseActions implements ControlMessageHandler { + app: App; + name = "DATABASE"; + + subscribed = new Set(); + + constructor(app: App) { + this.app = app; + + // update all subscribed sockets every 5 seconds + let last: DatabaseStats | undefined = undefined; + setInterval(() => { + const stats = this.getStats(); + if (stats.count !== last?.count || stats.size !== last.size) { + for (const sock of this.subscribed) { + this.send(sock, ["CONTROL", "DATABASE", "STATS", stats]); + } + } + last = stats; + }, 5_000); + } + + private getStats() { + const count = this.app.database.count(); + const size = this.app.database.size(); + + return { count, size }; + } + + handleMessage(sock: WebSocket | NodeJS.Process, message: DatabaseMessage): boolean { + const action = message[2]; + switch (action) { + case "SUBSCRIBE": + this.subscribed.add(sock); + sock.once("close", () => this.subscribed.delete(sock)); + this.send(sock, ["CONTROL", "DATABASE", "STATS", this.getStats()]); + return true; + + case "UNSUBSCRIBE": + this.subscribed.delete(sock); + return true; + + case "STATS": + this.send(sock, ["CONTROL", "DATABASE", "STATS", this.getStats()]); + return true; + + case "CLEAR": + this.app.database.clear(); + return true; + + default: + return false; + } + } + + send(sock: WebSocket | NodeJS.Process, response: DatabaseResponse) { + sock.send?.(JSON.stringify(response)); + } +} diff --git a/src/modules/control/decryption-cache.ts b/src/modules/control/decryption-cache.ts new file mode 100644 index 0000000..8238d6b --- /dev/null +++ b/src/modules/control/decryption-cache.ts @@ -0,0 +1,50 @@ +import { WebSocket } from 'ws'; +import { + DecryptionCacheMessage, + DecryptionCacheResponse, +} from '@satellite-earth/core/types/control-api/decryption-cache.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; + +/** handles ['CONTROL', 'DECRYPTION-CACHE', ...] messages */ +export default class DecryptionCacheActions implements ControlMessageHandler { + app: App; + name = 'DECRYPTION-CACHE'; + + constructor(app: App) { + this.app = app; + } + + handleMessage(sock: WebSocket | NodeJS.Process, message: DecryptionCacheMessage) { + const method = message[2]; + switch (method) { + case 'ADD-CONTENT': + this.app.decryptionCache.addEventContent(message[3], message[4]); + return true; + + case 'CLEAR-PUBKEY': + this.app.decryptionCache.clearPubkey(message[3]); + return true; + + case 'CLEAR': + this.app.decryptionCache.clearAll(); + return true; + + case 'REQUEST': + this.app.decryptionCache.getEventsContent(message[3]).then((contents) => { + for (const { event, content } of contents) + this.send(sock, ['CONTROL', 'DECRYPTION-CACHE', 'CONTENT', event, content]); + this.send(sock, ['CONTROL', 'DECRYPTION-CACHE', 'END']); + }); + return true; + + default: + return false; + } + } + + send(sock: WebSocket | NodeJS.Process, response: DecryptionCacheResponse) { + sock.send?.(JSON.stringify(response)); + } +} diff --git a/src/modules/control/dm-actions.ts b/src/modules/control/dm-actions.ts new file mode 100644 index 0000000..29b2bc5 --- /dev/null +++ b/src/modules/control/dm-actions.ts @@ -0,0 +1,31 @@ +import { WebSocket } from 'ws'; +import { DirectMessageMessage } from '@satellite-earth/core/types/control-api/direct-messages.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; + +/** handles ['CONTROL', 'DM', ...] messages */ +export default class DirectMessageActions implements ControlMessageHandler { + app: App; + name = 'DM'; + + constructor(app: App) { + this.app = app; + } + + handleMessage(sock: WebSocket | NodeJS.Process, message: DirectMessageMessage) { + const method = message[2]; + switch (method) { + case 'OPEN': + this.app.directMessageManager.openConversation(message[3], message[4]); + return true; + + case 'CLOSE': + this.app.directMessageManager.closeConversation(message[3], message[4]); + return true; + + default: + return false; + } + } +} diff --git a/src/modules/control/logs-actions.ts b/src/modules/control/logs-actions.ts new file mode 100644 index 0000000..743e63a --- /dev/null +++ b/src/modules/control/logs-actions.ts @@ -0,0 +1,27 @@ +import { WebSocket } from 'ws'; +import { LogsMessage } from '@satellite-earth/core/types/control-api/logs.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; + +/** handles ['CONTROL', 'DM', ...] messages */ +export default class LogsActions implements ControlMessageHandler { + app: App; + name = 'LOGS'; + + constructor(app: App) { + this.app = app; + } + + handleMessage(sock: WebSocket | NodeJS.Process, message: LogsMessage) { + const method = message[2]; + switch (method) { + case 'CLEAR': + this.app.logStore.clearLogs(message[3] ? { service: message[3] } : undefined); + return true; + + default: + return false; + } + } +} diff --git a/src/modules/control/notification-actions.ts b/src/modules/control/notification-actions.ts new file mode 100644 index 0000000..315074c --- /dev/null +++ b/src/modules/control/notification-actions.ts @@ -0,0 +1,43 @@ +import { WebSocket } from 'ws'; +import { NotificationsMessage, NotificationsResponse } from '@satellite-earth/core/types/control-api/notifications.js'; + +import { ControlMessageHandler } from './control-api.js'; +import type App from '../../app/index.js'; +import { NostrEvent } from 'nostr-tools'; + +export default class NotificationActions implements ControlMessageHandler { + app: App; + name = 'NOTIFICATIONS'; + + constructor(app: App) { + this.app = app; + } + + handleMessage(sock: WebSocket | NodeJS.Process, message: NotificationsMessage): boolean { + const action = message[2]; + switch (action) { + case 'GET-VAPID-KEY': + this.send(sock, ['CONTROL', 'NOTIFICATIONS', 'VAPID-KEY', this.app.notifications.webPushKeys.publicKey]); + return true; + + case 'REGISTER': + this.app.notifications.addOrUpdateChannel(message[3]); + return true; + + case 'NOTIFY': + const event: NostrEvent | undefined = this.app.eventStore.getEventsForFilters([{ ids: [message[3]] }])?.[0]; + if (event) this.app.notifications.notify(event); + return true; + + case 'UNREGISTER': + this.app.notifications.removeChannel(message[3]); + return true; + + default: + return false; + } + } + send(sock: WebSocket | NodeJS.Process, response: NotificationsResponse) { + sock.send?.(JSON.stringify(response)); + } +} diff --git a/src/modules/control/receiver-actions.ts b/src/modules/control/receiver-actions.ts new file mode 100644 index 0000000..e297ced --- /dev/null +++ b/src/modules/control/receiver-actions.ts @@ -0,0 +1,29 @@ +import { WebSocket } from 'ws'; +import { ReceiverMessage } from '@satellite-earth/core/types/control-api/receiver.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; + +export default class ReceiverActions implements ControlMessageHandler { + app: App; + name = 'RECEIVER'; + + constructor(app: App) { + this.app = app; + } + handleMessage(sock: WebSocket | NodeJS.Process, message: ReceiverMessage): boolean { + const action = message[2]; + switch (action) { + case 'START': + this.app.receiver.start(); + return true; + + case 'STOP': + this.app.receiver.stop(); + return true; + + default: + return false; + } + } +} diff --git a/src/modules/control/remote-auth-actions.ts b/src/modules/control/remote-auth-actions.ts new file mode 100644 index 0000000..3065c2f --- /dev/null +++ b/src/modules/control/remote-auth-actions.ts @@ -0,0 +1,69 @@ +import { WebSocket } from 'ws'; +import { verifyEvent } from 'nostr-tools'; +import { RemoteAuthMessage, RemoteAuthResponse } from '@satellite-earth/core/types/control-api/remote-auth.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; + +/** handles ['CONTROL', 'REMOTE-AUTH', ...] messages */ +export default class RemoteAuthActions implements ControlMessageHandler { + app: App; + name = 'REMOTE-AUTH'; + + private subscribed = new Set(); + + constructor(app: App) { + this.app = app; + + // when config changes send it to the subscribed sockets + this.app.pool.emitter.on('challenge', (relay, challenge) => { + for (const sock of this.subscribed) { + this.send(sock, [ + 'CONTROL', + 'REMOTE-AUTH', + 'STATUS', + relay.url, + challenge, + !!this.app.pool.authenticated.get(relay.url), + ]); + } + }); + } + + sendAllStatuses(sock: WebSocket | NodeJS.Process) { + for (const [url, relay] of this.app.pool) { + const challenge = this.app.pool.challenges.get(url); + const authenticated = this.app.pool.isAuthenticated(url); + + if (challenge) { + this.send(sock, ['CONTROL', 'REMOTE-AUTH', 'STATUS', url, challenge, authenticated]); + } + } + } + + async handleMessage(sock: WebSocket | NodeJS.Process, message: RemoteAuthMessage) { + const method = message[2]; + switch (method) { + case 'SUBSCRIBE': + this.subscribed.add(sock); + sock.once('close', () => this.subscribed.delete(sock)); + this.sendAllStatuses(sock); + return true; + case 'UNSUBSCRIBE': + this.subscribed.delete(sock); + return true; + case 'AUTHENTICATE': + const event = message[3]; + if (verifyEvent(event)) { + const relay = event.tags.find((t) => (t[0] = 'relay'))?.[1]; + if (relay) await this.app.pool.authenticate(relay, event); + } + default: + return false; + } + } + + send(sock: WebSocket | NodeJS.Process, response: RemoteAuthResponse) { + sock.send?.(JSON.stringify(response)); + } +} diff --git a/src/modules/control/report-actions.ts b/src/modules/control/report-actions.ts new file mode 100644 index 0000000..e0bfb42 --- /dev/null +++ b/src/modules/control/report-actions.ts @@ -0,0 +1,93 @@ +import { WebSocket } from 'ws'; +import { ReportArguments } from '@satellite-earth/core/types'; +import { ReportsMessage } from '@satellite-earth/core/types/control-api/reports.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; +import Report from '../reports/report.js'; +import { logger } from '../../logger.js'; +import REPORT_CLASSES from '../reports/reports/index.js'; + +/** handles ['CONTROL', 'REPORT', ...] messages */ +export default class ReportActions implements ControlMessageHandler { + app: App; + name = 'REPORT'; + log = logger.extend('ReportActions'); + + types: { + [k in keyof ReportArguments]?: typeof Report; + } = REPORT_CLASSES; + + private reports = new Map>>(); + + constructor(app: App) { + this.app = app; + } + + private getReportsForSocket(socket: WebSocket | NodeJS.Process) { + let map = this.reports.get(socket); + if (map) return map; + map = new Map(); + this.reports.set(socket, map); + return map; + } + + handleDisconnect(ws: WebSocket): void { + // close all reports for socket on disconnect + const reports = this.reports.get(ws); + + if (reports) { + for (const [id, report] of reports) report.close(); + + if (reports.size) this.log(`Closed ${reports.size} reports for disconnected socket`); + this.reports.delete(ws); + } + } + + // TODO: maybe move some of this logic out to a manager class so the control action class can be simpler + async handleMessage(sock: WebSocket | NodeJS.Process, message: ReportsMessage) { + const method = message[2]; + switch (method) { + case 'SUBSCRIBE': { + const reports = this.getReportsForSocket(sock); + const id = message[3]; + const type = message[4]; + const args = message[5]; + + let report = reports.get(id) as Report | undefined; + if (!report) { + const ReportClass = this.types[type]; + if (!ReportClass) throw new Error('Missing class for report type: ' + type); + + this.log(`Creating ${type} ${id} report with args`, JSON.stringify(args)); + + report = new ReportClass(id, this.app, sock); + reports.set(id, report); + } + + await report.run(args); + return true; + } + case 'CLOSE': { + const reports = this.getReportsForSocket(sock); + const id = message[3]; + const report = reports.get(id); + if (report) { + await report.close(); + reports.delete(id); + } + return true; + } + default: + return false; + } + } + + cleanup() { + for (const [sock, reports] of this.reports) { + for (const [id, report] of reports) { + report.close(); + } + } + } +} diff --git a/src/modules/control/scrapper-actions.ts b/src/modules/control/scrapper-actions.ts new file mode 100644 index 0000000..6cb890a --- /dev/null +++ b/src/modules/control/scrapper-actions.ts @@ -0,0 +1,37 @@ +import { WebSocket } from 'ws'; +import { ScrapperMessage } from '@satellite-earth/core/types/control-api/scrapper.js'; + +import type App from '../../app/index.js'; +import { type ControlMessageHandler } from './control-api.js'; + +export default class ScrapperActions implements ControlMessageHandler { + app: App; + name = 'SCRAPPER'; + + constructor(app: App) { + this.app = app; + } + handleMessage(sock: WebSocket | NodeJS.Process, message: ScrapperMessage): boolean { + const action = message[2]; + switch (action) { + case 'START': + this.app.scrapper.start(); + return true; + + case 'STOP': + this.app.scrapper.stop(); + return true; + + case 'ADD-PUBKEY': + this.app.scrapper.addPubkey(message[3]); + return true; + + case 'REMOVE-PUBKEY': + this.app.scrapper.removePubkey(message[3]); + return true; + + default: + return false; + } + } +} diff --git a/src/modules/decryption-cache/decryption-cache.ts b/src/modules/decryption-cache/decryption-cache.ts new file mode 100644 index 0000000..8333fed --- /dev/null +++ b/src/modules/decryption-cache/decryption-cache.ts @@ -0,0 +1,153 @@ +import { mapParams } from '@satellite-earth/core/helpers/sql.js'; +import { MigrationSet } from '@satellite-earth/core/sqlite'; +import { type Database } from 'better-sqlite3'; +import { EventEmitter } from 'events'; + +import { logger } from '../../logger.js'; +import { EventRow, parseEventRow } from '@satellite-earth/core/sqlite-event-store'; +import { NostrEvent } from 'nostr-tools'; + +const migrations = new MigrationSet('decryption-cache'); + +// Version 1 +migrations.addScript(1, async (db, log) => { + db.prepare( + ` + CREATE TABLE "decryption_cache" ( + "event" TEXT(64) NOT NULL, + "content" TEXT NOT NULL, + PRIMARY KEY("event") + ); + `, + ).run(); +}); + +// Version 2, search +migrations.addScript(2, async (db, log) => { + // create external Content fts5 table + db.prepare( + `CREATE VIRTUAL TABLE IF NOT EXISTS decryption_cache_fts USING fts5(content, content='decryption_cache', tokenize='trigram')`, + ).run(); + log(`Created decryption cache search table`); + + // create triggers to sync table + db.prepare( + ` + CREATE TRIGGER IF NOT EXISTS decryption_cache_ai AFTER INSERT ON decryption_cache BEGIN + INSERT INTO decryption_cache_fts(rowid, content) VALUES (NEW.rowid, NEW.content); + END; + `, + ).run(); + db.prepare( + ` + CREATE TRIGGER IF NOT EXISTS decryption_cache_ad AFTER DELETE ON decryption_cache BEGIN + INSERT INTO decryption_cache_ai(decryption_cache_ai, rowid, content) VALUES('delete', OLD.rowid, OLD.content); + END; + `, + ).run(); + + // populate table + const inserted = db + .prepare(`INSERT INTO decryption_cache_fts (rowid, content) SELECT rowid, content FROM decryption_cache`) + .run(); + log(`Indexed ${inserted.changes} decrypted events in search table`); +}); + +type EventMap = { + cache: [string, string]; +}; + +export default class DecryptionCache extends EventEmitter { + database: Database; + log = logger.extend('DecryptionCache'); + + constructor(database: Database) { + super(); + this.database = database; + } + + setup() { + return migrations.run(this.database); + } + + /** cache the decrypted content of an event */ + addEventContent(id: string, plaintext: string) { + const result = this.database + .prepare<[string, string]>(`INSERT INTO decryption_cache (event, content) VALUES (?, ?)`) + .run(id, plaintext); + + if (result.changes > 0) { + this.log(`Saved content for ${id}`); + + this.emit('cache', id, plaintext); + } + } + + /** remove all cached content relating to a pubkey */ + clearPubkey(pubkey: string) { + // this.database.prepare(`DELETE FROM decryption_cache INNER JOIN events ON event=events.id`) + } + + /** clear all cached content */ + clearAll() { + this.database.prepare(`DELETE FROM decryption_cache`).run(); + } + + async search( + search: string, + filter?: { conversation?: [string, string]; order?: 'rank' | 'created_at' }, + ): Promise<{ event: NostrEvent; plaintext: string }[]> { + const params: any[] = []; + const andConditions: string[] = []; + + let sql = `SELECT events.*, decryption_cache.content as plaintext FROM decryption_cache_fts + INNER JOIN decryption_cache ON decryption_cache_fts.rowid = decryption_cache.rowid + INNER JOIN events ON decryption_cache.event = events.id`; + + andConditions.push('decryption_cache_fts MATCH ?'); + params.push(search); + + // filter down by authors + if (filter?.conversation) { + sql += `\nINNER JOIN tags ON tag.e = events.id AND tags.t = 'p'`; + andConditions.push(`(tags.v = ? AND events.pubkey = ?) OR (tags.v = ? AND events.pubkey = ?)`); + params.push(...filter.conversation, ...Array.from(filter.conversation).reverse()); + } + + if (andConditions.length > 0) { + sql += ` WHERE ${andConditions.join(' AND ')}`; + } + + switch (filter?.order) { + case 'rank': + sql += ' ORDER BY rank'; + break; + + case 'created_at': + default: + sql += ' ORDER BY events.created_at DESC'; + break; + } + + return this.database + .prepare(sql) + .all(...params) + .map((row) => ({ event: parseEventRow(row), plaintext: row.plaintext })); + } + + async getEventContent(id: string) { + const result = this.database + .prepare<[string], { event: string; content: string }>(`SELECT * FROM decryption_cache WHERE event=?`) + .get(id); + + return result?.content; + } + async getEventsContent(ids: string[]) { + return this.database + .prepare< + string[], + { event: string; content: string } + >(`SELECT * FROM decryption_cache WHERE event IN ${mapParams(ids)}`) + .all(...ids); + } +} diff --git a/src/modules/direct-message-manager.ts b/src/modules/direct-message-manager.ts new file mode 100644 index 0000000..056c3d7 --- /dev/null +++ b/src/modules/direct-message-manager.ts @@ -0,0 +1,168 @@ +import { NostrEvent, kinds } from 'nostr-tools'; +import { SubCloser } from 'nostr-tools/abstract-pool'; +import { Subscription } from 'nostr-tools/abstract-relay'; +import { EventEmitter } from 'events'; + +import { getInboxes } from '@satellite-earth/core/helpers/nostr/mailboxes.js'; +import { logger } from '../logger.js'; +import type App from '../app/index.js'; +import { getRelaysFromContactList } from '@satellite-earth/core/helpers/nostr/contacts.js'; + +type EventMap = { + open: [string, string]; + close: [string, string]; + message: [NostrEvent]; +}; + +/** handles sending and receiving direct messages */ +export default class DirectMessageManager extends EventEmitter { + log = logger.extend('DirectMessageManager'); + app: App; + + private explicitRelays: string[] = []; + + constructor(app: App) { + super(); + this.app = app; + + // Load profiles for participants when + // a conversation thread is opened + this.on('open', (a, b) => { + this.app.profileBook.loadProfile(a, this.app.addressBook.getOutboxes(a)); + this.app.profileBook.loadProfile(b, this.app.addressBook.getOutboxes(b)); + }); + + // emit a "message" event when a new kind4 message is detected + this.app.eventStore.on('event:inserted', (event) => { + if (event.kind === kinds.EncryptedDirectMessage) this.emit('message', event); + }); + } + + /** sends a DM event to the receivers inbox relays */ + async forwardMessage(event: NostrEvent) { + if (event.kind !== kinds.EncryptedDirectMessage) return; + + const addressedTo = event.tags.find((t) => t[0] === 'p')?.[1]; + if (!addressedTo) return; + + // get users inboxes + let relays = await this.app.addressBook.loadInboxes(addressedTo); + + if (!relays || relays.length === 0) { + // try to send the DM to the users legacy app relays + const contacts = await this.app.contactBook.loadContacts(addressedTo); + if (contacts) { + const appRelays = getRelaysFromContactList(contacts); + + if (appRelays) relays = appRelays.filter((r) => r.write).map((r) => r.url); + } + } + + if (!relays || relays.length === 0) { + // use fallback relays + relays = this.explicitRelays; + } + + this.log(`Forwarding message to ${relays.length} relays`); + const results = await Promise.allSettled(this.app.pool.publish(relays, event)); + + return results; + } + + private getConversationKey(a: string, b: string) { + if (a < b) return a + ':' + b; + else return b + ':' + a; + } + + watching = new Map>(); + async watchInbox(pubkey: string) { + if (this.watching.has(pubkey)) return; + + this.log(`Watching ${pubkey} inboxes for mail`); + const mailboxes = await this.app.addressBook.loadMailboxes(pubkey); + if (!mailboxes) { + this.log(`Failed to get ${pubkey} mailboxes`); + return; + } + + const relays = getInboxes(mailboxes, this.explicitRelays); + const subscriptions = new Map(); + + for (const url of relays) { + const subscribe = async () => { + const relay = await this.app.pool.ensureRelay(url); + const sub = relay.subscribe([{ kinds: [kinds.EncryptedDirectMessage], '#p': [pubkey] }], { + onevent: (event) => { + this.app.eventStore.addEvent(event); + }, + onclose: () => { + // reconnect if we are still watching this pubkey + if (this.watching.has(pubkey)) { + this.log(`Reconnecting to ${relay.url} for ${pubkey} inbox DMs`); + setTimeout(() => subscribe(), 30_000); + } + }, + }); + + subscriptions.set(relay.url, sub); + }; + + subscribe(); + } + this.watching.set(pubkey, subscriptions); + } + stopWatchInbox(pubkey: string) { + const subs = this.watching.get(pubkey); + if (subs) { + this.watching.delete(pubkey); + for (const [_, sub] of subs) { + sub.close(); + } + } + } + + subscriptions = new Map(); + async openConversation(a: string, b: string) { + const key = this.getConversationKey(a, b); + + if (this.subscriptions.has(key)) return; + + const aMailboxes = await this.app.addressBook.loadMailboxes(a); + const bMailboxes = await this.app.addressBook.loadMailboxes(b); + + // If inboxes for either user cannot be determined, either because nip65 + // was not found, or nip65 had no listed read relays, fall back to explicit + const aInboxes = aMailboxes ? getInboxes(aMailboxes, this.explicitRelays) : this.explicitRelays; + const bInboxes = bMailboxes ? getInboxes(bMailboxes, this.explicitRelays) : this.explicitRelays; + + const relays = new Set([...aInboxes, ...bInboxes]); + + let events = 0; + const sub = this.app.pool.subscribeMany( + Array.from(relays), + [{ kinds: [kinds.EncryptedDirectMessage], authors: [a, b], '#p': [a, b] }], + { + onevent: (event) => { + events += +this.app.eventStore.addEvent(event); + }, + oneose: () => { + if (events) this.log(`Found ${events} new messages`); + }, + }, + ); + + this.log(`Opened conversation ${key} on ${relays.size} relays`); + this.subscriptions.set(key, sub); + this.emit('open', a, b); + } + closeConversation(a: string, b: string) { + const key = this.getConversationKey(a, b); + + const sub = this.subscriptions.get(key); + if (sub) { + sub.close(); + this.subscriptions.delete(key); + this.emit('close', a, b); + } + } +} diff --git a/src/modules/gossip.ts b/src/modules/gossip.ts new file mode 100644 index 0000000..41b9160 --- /dev/null +++ b/src/modules/gossip.ts @@ -0,0 +1,132 @@ +import { SimpleSigner } from 'applesauce-signer/signers/simple-signer'; +import { EventTemplate, SimplePool } from 'nostr-tools'; +import { getTagValue } from 'applesauce-core/helpers'; +import { IEventStore, NostrRelay } from '@satellite-earth/core'; +import dayjs, { Dayjs } from 'dayjs'; + +import { logger } from '../logger.js'; +import InboundNetworkManager from './network/inbound/index.js'; + +function buildGossipTemplate(self: string, address: string, network: string): EventTemplate { + return { + kind: 30166, + content: '', + tags: [ + ['d', address], + ['n', network], + ['p', self], + ['T', 'PrivateInbox'], + ...NostrRelay.SUPPORTED_NIPS.map((nip) => ['N', String(nip)]), + ], + created_at: dayjs().unix(), + }; +} + +export default class Gossip { + log = logger.extend('Gossip'); + network: InboundNetworkManager; + signer: SimpleSigner; + pool: SimplePool; + relay: NostrRelay; + eventStore: IEventStore; + + running = false; + // default every 30 minutes + interval = 30 * 60_000; + broadcastRelays: string[] = []; + + constructor( + network: InboundNetworkManager, + signer: SimpleSigner, + pool: SimplePool, + relay: NostrRelay, + eventStore: IEventStore, + ) { + this.network = network; + this.signer = signer; + this.pool = pool; + this.relay = relay; + this.eventStore = eventStore; + } + + async gossip() { + const pubkey = await this.signer.getPublicKey(); + + if (this.broadcastRelays.length === 0) return; + + if (this.network.hyper.available && this.network.hyper.address) { + this.log('Publishing hyper gossip'); + await this.pool.publish( + this.broadcastRelays, + await this.signer.signEvent(buildGossipTemplate(pubkey, this.network.hyper.address, 'hyper')), + ); + } + + if (this.network.tor.available && this.network.tor.address) { + this.log('Publishing tor gossip'); + await this.pool.publish( + this.broadcastRelays, + await this.signer.signEvent(buildGossipTemplate(pubkey, this.network.tor.address, 'tor')), + ); + } + + if (this.network.i2p.available && this.network.i2p.address) { + this.log('Publishing i2p gossip'); + await this.pool.publish( + this.broadcastRelays, + await this.signer.signEvent(buildGossipTemplate(pubkey, this.network.i2p.address, 'i2p')), + ); + } + } + + private async update() { + if (!this.running) return; + await this.gossip(); + + setTimeout(this.update.bind(this), this.interval); + } + + start() { + if (this.running) return; + this.running = true; + + this.log(`Starting gossip on ${this.broadcastRelays.join(', ')}`); + setTimeout(this.update.bind(this), 5000); + } + + stop() { + this.log('Stopping gossip'); + this.running = false; + } + + private lookups = new Map(); + async lookup(pubkey: string) { + const last = this.lookups.get(pubkey); + + const filter = { authors: [pubkey], '#p': [pubkey], kinds: [30166] }; + + // no cache or expired + if (last === undefined || !last.isAfter(dayjs())) { + await new Promise((res) => { + this.lookups.set(pubkey, dayjs().add(1, 'hour')); + + const sub = this.pool.subscribeMany(this.broadcastRelays, [filter], { + onevent: (event) => this.eventStore.addEvent(event), + oneose: () => { + sub.close(); + res(); + }, + }); + }); + } + + const events = this.eventStore.getEventsForFilters([filter]); + + const addresses: string[] = []; + for (const event of events) { + const url = getTagValue(event, 'd'); + if (url) addresses.push(url); + } + return addresses; + } +} diff --git a/src/modules/graph/index.ts b/src/modules/graph/index.ts new file mode 100644 index 0000000..bcb624f --- /dev/null +++ b/src/modules/graph/index.ts @@ -0,0 +1,105 @@ +import { NostrEvent, kinds } from 'nostr-tools'; +import App from '../../app/index.js'; + +export type Node = { p: string; z: number; n: number }; + +// TODO: this should be moved to core + +export default class Graph { + contacts: Record }> = {}; + app: App; + + constructor(app: App) { + this.app = app; + } + + init() { + const events = this.app.eventStore.getEventsForFilters([{ kinds: [kinds.Contacts] }]); + for (let event of events) { + this.add(event); + } + } + + add(event: NostrEvent) { + if (event.kind === kinds.Contacts) { + this.addContacts(event); + } + } + + addContacts(event: NostrEvent) { + const existing = this.contacts[event.pubkey]; + + // Add or overwrite an existing (older) contacts list + if (!existing || existing.created_at < event.created_at) { + const following = new Set(event.tags.filter((tag) => tag[0] === 'p').map((tag) => tag[1])); + + this.contacts[event.pubkey] = { + created_at: event.created_at, + set: following, + }; + } + } + + getNodes(roots: string[] = []): Node[] { + const u: Record = {}; + + // Init u with root pubkeys + for (let p of roots) { + u[p] = { z: 0, n: 1 }; + } + + const populate = (pubkeys: string[], z: number) => { + for (let p of pubkeys) { + // If pubkey's contacts don't exist, skip it + if (!this.contacts[p]) { + continue; + } + + // Iterate across pubkey's contacts, if the + // contact has not been recorded, create an + // entry at the current degrees of separation, + // otherwise increment the number of occurances + this.contacts[p].set.forEach((c) => { + // Don't count self-follow + if (p === c) { + return; + } + + if (!u[c]) { + u[c] = { z, n: 1 }; + } else { + if (u[c].z > z) { + return; + } + + u[c].n++; + } + }); + } + }; + + // Populate u with all the pubkeys that + // are directly followed by root pubkey + populate(roots, 1); + + // On the second pass, populate u with + // all the pubkeys that are followed + // by any pubkey that root follows + populate( + Object.keys(u).filter((p) => { + return u[p].z > 0; + }), + 2, + ); + + // Return list of pubkeys sorted by degrees + // of separation and number of occurances + return Object.keys(u) + .map((p) => { + return { ...u[p], p }; + }) + .sort((a, b) => { + return a.z === b.z ? b.n - a.n : a.z - b.z; + }); + } +} diff --git a/src/modules/hyper-connection-manager.ts b/src/modules/hyper-connection-manager.ts new file mode 100644 index 0000000..dde1f0f --- /dev/null +++ b/src/modules/hyper-connection-manager.ts @@ -0,0 +1,65 @@ +import net from 'net'; +import HyperDHT from 'hyperdht'; +import { pipeline } from 'streamx'; +import { logger } from '../logger.js'; + +const START_PORT = 25100; + +export class HyperConnectionManager { + log = logger.extend(`hyper-connection-manager`); + sockets = new Map(); + servers = new Map(); + node: HyperDHT; + + lastPort = START_PORT; + + constructor(privateKey: string) { + this.node = new HyperDHT({ + keyPair: HyperDHT.keyPair(Buffer.from(privateKey, 'hex')), + }); + } + + protected bind(pubkey: string) { + return new Promise((res) => { + const proxy = net.createServer({ allowHalfOpen: true }, (socket_) => { + const socket = this.node.connect(Buffer.from(pubkey, 'hex'), { + reusableSocket: true, + }); + + // @ts-expect-error + socket.setKeepAlive(5000); + + socket.on('open', () => { + // connect the sockets + pipeline(socket_, socket, socket_); + }); + socket.on('error', (error) => { + this.log('Failed to connect to', pubkey); + this.log(error); + }); + }); + + this.servers.set(pubkey, proxy); + + const port = this.lastPort++; + proxy.listen(port, '127.0.0.1', () => { + this.log('Bound hyper address', pubkey, 'to port:', port); + res(proxy); + }); + }); + } + + async getLocalAddress(pubkey: string) { + let server = this.servers.get(pubkey); + if (!server) server = await this.bind(pubkey); + + return server!.address() as net.AddressInfo; + } + + stop() { + for (const [pubkey, server] of this.servers) { + server.close(); + } + this.servers.clear(); + } +} diff --git a/src/modules/labeled-event-store.ts b/src/modules/labeled-event-store.ts new file mode 100644 index 0000000..f5f3cfe --- /dev/null +++ b/src/modules/labeled-event-store.ts @@ -0,0 +1,79 @@ +import { Database } from 'better-sqlite3'; +import { Filter, NostrEvent } from 'nostr-tools'; +import { IEventStore, SQLiteEventStore } from '@satellite-earth/core'; +import { logger } from '../logger.js'; +import { MigrationSet } from '@satellite-earth/core/sqlite'; + +export function mapParams(params: any[]) { + return `(${params.map(() => `?`).join(', ')})`; +} + +const migrations = new MigrationSet('labeled-event-store'); + +// Version 1 +migrations.addScript(1, async (db, log) => { + db.prepare( + ` + CREATE TABLE IF NOT EXISTS event_labels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event TEXT(64) REFERENCES events(id), + label TEXT + ) + `, + ).run(); + + db.prepare('CREATE INDEX IF NOT EXISTS event_labels_label ON event_labels(label)').run(); + db.prepare('CREATE INDEX IF NOT EXISTS event_labels_event ON event_labels(event)').run(); +}); + +/** An event store that is can only see a subset of events int the database */ +export class LabeledEventStore extends SQLiteEventStore implements IEventStore { + label: string; + readAll = false; + + constructor(db: Database, label: string) { + super(db); + this.label = label; + + this.log = logger.extend(`event-store:` + label); + } + + async setup() { + await super.setup(); + await migrations.run(this.db); + } + + override buildConditionsForFilters(filter: Filter) { + const parts = super.buildConditionsForFilters(filter); + + if (!this.readAll) { + parts.joins.push('INNER JOIN event_labels ON events.id = event_labels.event'); + parts.conditions.push('event_labels.label = ?'); + parts.parameters.push(this.label); + return parts; + } + + return parts; + } + + addEvent(event: NostrEvent) { + const inserted = super.addEvent(event); + + const hasLabel = !!this.db + .prepare('SELECT * FROM event_labels WHERE event = ? AND label = ?') + .get(event.id, this.label); + if (!hasLabel) this.db.prepare(`INSERT INTO event_labels (event, label) VALUES (?, ?)`).run(event.id, this.label); + + return inserted; + } + + removeEvents(ids: string[]) { + this.db.prepare(`DELETE FROM event_labels WHERE event IN ${mapParams(ids)}`).run(...ids); + return super.removeEvents(ids); + } + + removeEvent(id: string) { + this.db.prepare(`DELETE FROM event_labels WHERE event = ?`).run(id); + return super.removeEvent(id); + } +} diff --git a/src/modules/log-store/log-store.ts b/src/modules/log-store/log-store.ts new file mode 100644 index 0000000..34a19e6 --- /dev/null +++ b/src/modules/log-store/log-store.ts @@ -0,0 +1,163 @@ +import { type Database as SQLDatabase } from 'better-sqlite3'; +import { MigrationSet } from '@satellite-earth/core/sqlite'; +import EventEmitter from 'events'; +import { nanoid } from 'nanoid'; +import { Debugger } from 'debug'; + +import { logger } from '../../logger.js'; + +type EventMap = { + log: [LogEntry]; + clear: [string | undefined]; +}; + +export type LogEntry = { + id: string; + service: string; + timestamp: number; + message: string; +}; +export type DatabaseLogEntry = LogEntry & { + id: number | bigint; +}; + +const migrations = new MigrationSet('log-store'); + +// version 1 +migrations.addScript(1, async (db, log) => { + db.prepare( + ` + CREATE TABLE IF NOT EXISTS "logs" ( + "id" TEXT NOT NULL UNIQUE, + "timestamp" INTEGER NOT NULL, + "service" TEXT NOT NULL, + "message" TEXT NOT NULL, + PRIMARY KEY("id") + ); + `, + ).run(); + log('Created logs table'); + + db.prepare('CREATE INDEX IF NOT EXISTS logs_service ON logs(service)'); + log('Created logs service index'); +}); + +export default class LogStore extends EventEmitter { + database: SQLDatabase; + debug: Debugger; + + constructor(database: SQLDatabase) { + super(); + this.database = database; + this.debug = logger; + } + + async setup() { + return await migrations.run(this.database); + } + + addEntry(service: string, timestamp: Date | number, message: string) { + const unix = timestamp instanceof Date ? Math.round(timestamp.valueOf() / 1000) : timestamp; + const entry = { + id: nanoid(), + service, + timestamp: unix, + message, + }; + + this.queue.push(entry); + this.emit('log', entry); + + if (!this.running) this.write(); + } + + running = false; + queue: LogEntry[] = []; + private write() { + if (this.running) return; + this.running = true; + + const BATCH_SIZE = 5000; + + const inserted: (number | bigint)[] = []; + const failed: LogEntry[] = []; + + this.database.transaction(() => { + let i = 0; + while (this.queue.length) { + const entry = this.queue.shift()!; + try { + const { lastInsertRowid } = this.database + .prepare< + [string, string, number, string] + >(`INSERT INTO "logs" (id, service, timestamp, message) VALUES (?, ?, ?, ?)`) + .run(entry.id, entry.service, entry.timestamp, entry.message); + + inserted.push(lastInsertRowid); + } catch (error) { + failed.push(entry); + } + + if (++i >= BATCH_SIZE) break; + } + })(); + + for (const entry of failed) { + // Don't know what to do here... + } + + if (this.queue.length > 0) setTimeout(this.write.bind(this), 1000); + else this.running = false; + } + + getLogs(filter?: { service?: string; since?: number; until?: number; limit?: number }) { + const conditions: string[] = []; + const parameters: (string | number)[] = []; + + let sql = `SELECT * FROM logs`; + + if (filter?.service) { + conditions.push(`service LIKE CONCAT(?,'%')`); + parameters.push(filter?.service); + } + if (filter?.since) { + conditions.push('timestamp>=?'); + parameters.push(filter?.since); + } + if (filter?.until) { + conditions.push('timestamp<=?'); + parameters.push(filter?.until); + } + if (conditions.length > 0) sql += ` WHERE ${conditions.join(' AND ')}`; + + if (filter?.limit) { + sql += ' LIMIT ?'; + parameters.push(filter.limit); + } + return this.database.prepare(sql).all(...parameters); + } + + clearLogs(filter?: { service?: string; since?: number; until?: number }) { + const conditions: string[] = []; + const parameters: (string | number)[] = []; + + let sql = `DELETE FROM logs`; + + if (filter?.service) { + conditions.push('service=?'); + parameters.push(filter?.service); + } + if (filter?.since) { + conditions.push('timestamp>=?'); + parameters.push(filter?.since); + } + if (filter?.until) { + conditions.push('timestamp<=?'); + parameters.push(filter?.until); + } + if (conditions.length > 0) sql += ` WHERE ${conditions.join(' AND ')}`; + + this.database.prepare(sql).run(...parameters); + this.emit('clear', filter?.service); + } +} diff --git a/src/modules/network/inbound/hyper.ts b/src/modules/network/inbound/hyper.ts new file mode 100644 index 0000000..276b2eb --- /dev/null +++ b/src/modules/network/inbound/hyper.ts @@ -0,0 +1,71 @@ +import HolesailServer from 'holesail-server'; +import { encodeAddress } from 'hyper-address'; +import { hexToBytes } from '@noble/hashes/utils'; +import { AddressInfo } from 'net'; + +import App from '../../../app/index.js'; +import { InboundInterface } from '../interfaces.js'; +import { logger } from '../../../logger.js'; + +/** manages a holesail-server instance that points to the app.server http server */ +export default class HyperInbound implements InboundInterface { + app: App; + hyper?: HolesailServer; + log = logger.extend('Network:Inbound:Hyper'); + + get available() { + return true; + } + running = false; + error?: Error; + address?: string; + + constructor(app: App) { + this.app = app; + } + + async start(address: AddressInfo) { + try { + this.running = true; + this.error = undefined; + + this.log(`Importing and starting hyperdht node`); + + const { default: HolesailServer } = await import('holesail-server'); + const { getOrCreateNode } = await import('../../../sidecars/hyperdht.js'); + + const hyper = (this.hyper = new HolesailServer()); + hyper.dht = getOrCreateNode(); + + return new Promise((res) => { + hyper.serve( + { + port: address.port, + address: address.address, + secure: false, + buffSeed: this.app.secrets.get('hyperKey'), + }, + () => { + const address = 'http://' + encodeAddress(hexToBytes(hyper.getPublicKey())); + this.address = address; + + this.log(`Listening on ${address}`); + res(); + }, + ); + }); + } catch (error) { + this.running = false; + if (error instanceof Error) this.error = error; + } + } + + async stop() { + this.log('Shutting down'); + // disabled because holesail-server destroys the hyperdht node + // this.hyper?.destroy(); + this.running = false; + this.address = undefined; + this.error = undefined; + } +} diff --git a/src/modules/network/inbound/i2p.ts b/src/modules/network/inbound/i2p.ts new file mode 100644 index 0000000..67b5400 --- /dev/null +++ b/src/modules/network/inbound/i2p.ts @@ -0,0 +1,76 @@ +import type { AddressInfo } from 'net'; +import type { I2pSamStream } from '@diva.exchange/i2p-sam'; + +import App from '../../../app/index.js'; +import { I2P_SAM_ADDRESS } from '../../../env.js'; +import { logger } from '../../../logger.js'; +import { InboundInterface } from '../interfaces.js'; + +export default class I2PInbound implements InboundInterface { + app: App; + log = logger.extend('Network:Inbound:I2P'); + + available = !!I2P_SAM_ADDRESS; + running = false; + address?: string; + error?: Error; + + private forward?: I2pSamStream; + + constructor(app: App) { + this.app = app; + } + + async start(address: AddressInfo) { + try { + if (this.running) return; + this.running = true; + + const [host, port] = I2P_SAM_ADDRESS?.split(':') ?? []; + if (!host || !port) throw new Error(`Malformed proxy address ${I2P_SAM_ADDRESS}`); + + this.log('Importing I2P SAM package'); + const { createForward } = await import('@diva.exchange/i2p-sam'); + + // try to get the last key pair that was used + const privateKey = this.app.secrets.get('i2pPrivateKey'); + const publicKey = this.app.secrets.get('i2pPublicKey'); + + this.log('Creating forwarding stream'); + this.forward = await createForward({ + sam: { + host: host, + portTCP: parseInt(port), + privateKey, + publicKey, + }, + forward: { + host: address.address, + port: address.port, + }, + }); + + this.address = 'http://' + this.forward.getB32Address(); + + this.log(`Listening on ${this.address}`); + + // save the key pair for later + this.app.secrets.set('i2pPrivateKey', this.forward.getPrivateKey()); + this.app.secrets.set('i2pPublicKey', this.forward.getPublicKey()); + } catch (error) { + this.running = false; + if (error instanceof Error) this.error = error; + } + } + + async stop() { + if (!this.running) return; + this.running = false; + + if (this.forward) { + this.log('Closing forwarding stream'); + this.forward.close(); + this.forward = undefined; + } + } +} diff --git a/src/modules/network/inbound/index.ts b/src/modules/network/inbound/index.ts new file mode 100644 index 0000000..c1c8261 --- /dev/null +++ b/src/modules/network/inbound/index.ts @@ -0,0 +1,79 @@ +import App from '../../../app/index.js'; +import HyperInbound from './hyper.js'; +import { logger } from '../../../logger.js'; +import { getIPAddresses } from '../../../helpers/ip.js'; +import TorInbound from './tor.js'; +import ConfigManager from '../../config-manager.js'; +import I2PInbound from './i2p.js'; + +/** manages all inbound servers on other networks: hyper, tor, i2p, etc... */ +export default class InboundNetworkManager { + app: App; + log = logger.extend('Network:Inbound'); + hyper: HyperInbound; + tor: TorInbound; + i2p: I2PInbound; + + running = false; + get addresses() { + const ip = getIPAddresses(); + const hyper = this.hyper.address; + const tor = this.tor.address; + + return [...(ip ?? []), ...(tor ?? []), ...(hyper ?? [])]; + } + + constructor(app: App) { + this.app = app; + + this.hyper = new HyperInbound(app); + this.tor = new TorInbound(app); + this.i2p = new I2PInbound(app); + + this.listenToAppConfig(app.config); + } + + private getAddress() { + const address = this.app.server.address(); + + if (typeof address === 'string' || address === null) + throw new Error('External servers started when server does not have an address'); + + return address; + } + + private update(config = this.app.config.data) { + if (!this.running) return; + const address = this.getAddress(); + + if (this.hyper.available && config.hyperEnabled !== this.hyper.running) { + if (config.hyperEnabled) this.hyper.start(address); + else this.hyper.stop(); + } + + if (this.tor.available) { + if (!this.tor.running) this.tor.start(address); + } + + if (this.i2p.available) { + if (!this.i2p.running) this.i2p.start(address); + } + } + + /** A helper method to make the manager run off of the app config */ + listenToAppConfig(config: ConfigManager) { + config.on('updated', this.update.bind(this)); + } + + start() { + this.running = true; + this.update(); + } + + async stop() { + this.running = false; + await this.hyper.stop(); + await this.tor.stop(); + await this.i2p.stop(); + } +} diff --git a/src/modules/network/inbound/tor.ts b/src/modules/network/inbound/tor.ts new file mode 100644 index 0000000..cf008cc --- /dev/null +++ b/src/modules/network/inbound/tor.ts @@ -0,0 +1,29 @@ +import { AddressInfo } from 'net'; + +import App from '../../../app/index.js'; +import { TOR_ADDRESS } from '../../../env.js'; +import { logger } from '../../../logger.js'; +import { InboundInterface } from '../interfaces.js'; + +export default class TorInbound implements InboundInterface { + app: App; + log = logger.extend('Network:Inbound:Tor'); + + readonly available = !!TOR_ADDRESS; + readonly running = !!TOR_ADDRESS; + readonly address = TOR_ADDRESS; + error?: Error; + + constructor(app: App) { + this.app = app; + } + + async start(address: AddressInfo) { + // not implemented yet + if (TOR_ADDRESS) this.log(`Listening on ${TOR_ADDRESS}`); + } + + async stop() { + // not implemented yet + } +} diff --git a/src/modules/network/interfaces.ts b/src/modules/network/interfaces.ts new file mode 100644 index 0000000..355dd49 --- /dev/null +++ b/src/modules/network/interfaces.ts @@ -0,0 +1,22 @@ +import { AddressInfo } from 'net'; + +export interface InboundInterface { + available: boolean; + running: boolean; + error?: Error; + + address?: string; + start(address: AddressInfo): Promise; + stop(): Promise; +} + +export interface OutboundInterface { + available: boolean; + running: boolean; + error?: Error; + + type: 'SOCKS5' | 'HTTP'; + address?: string; + start(): Promise; + stop(): Promise; +} diff --git a/src/modules/network/outbound/hyper.ts b/src/modules/network/outbound/hyper.ts new file mode 100644 index 0000000..3aa6826 --- /dev/null +++ b/src/modules/network/outbound/hyper.ts @@ -0,0 +1,57 @@ +import type { createProxy } from 'hyper-socks5-proxy'; +import getPort from 'get-port'; +import EventEmitter from 'events'; + +import { logger } from '../../../logger.js'; +import { OutboundInterface } from '../interfaces.js'; + +type EventMap = { + started: []; + stopped: []; +}; + +export default class HyperOutbound extends EventEmitter implements OutboundInterface { + log = logger.extend('Network:Outbound:Hyper'); + private port?: number; + private proxy?: ReturnType; + + running = false; + error?: Error; + readonly type = 'SOCKS5'; + address?: string; + get available() { + return true; + } + + async start() { + if (this.running) return; + this.running = true; + + try { + const { createProxy } = await import('hyper-socks5-proxy'); + const { getOrCreateNode } = await import('../../../sidecars/hyperdht.js'); + + this.port = await getPort({ port: 1080 }); + this.proxy = createProxy({ node: await getOrCreateNode() }); + + this.log('Starting SOCKS5 proxy'); + this.address = `127.0.0.1:${this.port}`; + this.proxy.listen(this.port, '127.0.0.1'); + this.log(`Proxy listening on ${this.address}`); + this.emit('started'); + } catch (error) { + this.running = false; + if (error instanceof Error) this.error = error; + } + } + + async stop() { + if (!this.running) return; + this.running = false; + + this.log('Stopping'); + await new Promise((res) => this.proxy?.close(() => res())); + this.proxy = undefined; + this.emit('stopped'); + } +} diff --git a/src/modules/network/outbound/i2p.ts b/src/modules/network/outbound/i2p.ts new file mode 100644 index 0000000..30b9d0e --- /dev/null +++ b/src/modules/network/outbound/i2p.ts @@ -0,0 +1,31 @@ +import { logger } from '../../../logger.js'; +import { OutboundInterface } from '../interfaces.js'; +import { I2P_PROXY, I2P_PROXY_TYPE } from '../../../env.js'; +import { testTCPConnection } from '../../../helpers/network.js'; + +export default class I2POutbound implements OutboundInterface { + log = logger.extend('Network:Outbound:I2P'); + + running = false; + error?: Error; + readonly type = I2P_PROXY_TYPE; + readonly address = I2P_PROXY; + readonly available = !!I2P_PROXY; + + async start() { + try { + if (this.running) return; + this.running = true; + + this.log(`Connecting to ${I2P_PROXY}`); + const [host, port] = this.address?.split(':') ?? []; + if (!host || !port) throw new Error('Malformed proxy address'); + await testTCPConnection(host, parseInt(port), 3000); + } catch (error) { + this.running = false; + if (error instanceof Error) this.error = error; + } + } + + async stop() {} +} diff --git a/src/modules/network/outbound/index.ts b/src/modules/network/outbound/index.ts new file mode 100644 index 0000000..0ec708c --- /dev/null +++ b/src/modules/network/outbound/index.ts @@ -0,0 +1,134 @@ +import { PacProxyAgent } from 'pac-proxy-agent'; +import _throttle from 'lodash.throttle'; + +import { logger } from '../../../logger.js'; +import ConfigManager from '../../config-manager.js'; +import HyperOutbound from './hyper.js'; +import TorOutbound from './tor.js'; +import I2POutbound from './i2p.js'; + +export class OutboundNetworkManager { + log = logger.extend('Network:Outbound'); + hyper: HyperOutbound; + tor: TorOutbound; + i2p: I2POutbound; + + running = false; + agent: PacProxyAgent; + + enableHyperConnections = false; + enableTorConnections = false; + enableI2PConnections = false; + routeAllTrafficThroughTor = false; + + constructor() { + this.hyper = new HyperOutbound(); + this.tor = new TorOutbound(); + this.i2p = new I2POutbound(); + + this.agent = new PacProxyAgent(this.buildPacURI(), { fallbackToDirect: true }); + } + + private buildPacURI() { + const statements: string[] = []; + + if (this.i2p.available && this.enableI2PConnections) { + statements.push( + ` +if (shExpMatch(host, "*.i2p")) +{ + return "${this.i2p.type} ${this.i2p.address}"; +} +`.trim(), + ); + } + + if (this.tor.available && this.enableTorConnections) { + statements.push( + ` +if (shExpMatch(host, "*.onion")) +{ + return "${this.tor.type} ${this.tor.address}"; +} +`.trim(), + ); + } + + if (this.hyper.available && this.enableHyperConnections) { + statements.push( + ` +if (shExpMatch(host, "*.hyper")) +{ + return "${this.hyper.type} ${this.hyper.address}"; +} +`.trim(), + ); + } + + if (this.routeAllTrafficThroughTor && this.tor.available) { + // if tor is available, route all traffic through it + statements.push(`${this.tor.type} ${this.tor.address}`); + this.log('Routing all traffic through tor proxy'); + } else { + statements.push('return "DIRECT";'); + } + + const PACFile = ` +// SPDX-License-Identifier: CC0-1.0 + +function FindProxyForURL(url, host) +{ + ${statements.join('\n')} +} +`.trim(); + + return 'pac+data:application/x-ns-proxy-autoconfig;base64,' + btoa(PACFile); + } + + updateAgent(uri = this.buildPacURI()) { + this.log('Updating PAC proxy agent'); + // copied from https://github.com/TooTallNate/proxy-agents/blob/main/packages/pac-proxy-agent/src/index.ts#L79C22-L79C51 + this.agent.uri = new URL(uri.replace(/^pac\+/i, '')); + + // forces the agent to refetch the resolver and pac file + this.agent.resolverPromise = undefined; + } + + updateAgentThrottle: () => void = _throttle(this.updateAgent.bind(this), 100); + + /** A helper method to make the manager run off of the app config */ + listenToAppConfig(config: ConfigManager) { + config.on('updated', (c) => { + this.enableHyperConnections = c.hyperEnabled && c.enableHyperConnections; + this.enableTorConnections = c.enableTorConnections; + this.enableI2PConnections = c.enableI2PConnections; + this.routeAllTrafficThroughTor = c.routeAllTrafficThroughTor; + + if (this.hyper.available && this.enableHyperConnections !== this.hyper.running) { + if (this.enableHyperConnections) this.hyper.start(); + else this.hyper.stop(); + } + + if (this.tor.available && this.enableTorConnections !== this.tor.running) { + if (this.enableTorConnections) this.tor.start(); + else this.tor.stop(); + } + + if (this.i2p.available && this.enableI2PConnections !== this.i2p.running) { + if (this.enableI2PConnections) this.i2p.start(); + else this.i2p.stop(); + } + + this.updateAgentThrottle(); + }); + } + + async stop() { + await this.hyper.stop(); + await this.tor.stop(); + } +} + +const outboundNetwork = new OutboundNetworkManager(); + +export default outboundNetwork; diff --git a/src/modules/network/outbound/tor.ts b/src/modules/network/outbound/tor.ts new file mode 100644 index 0000000..3b03a63 --- /dev/null +++ b/src/modules/network/outbound/tor.ts @@ -0,0 +1,31 @@ +import { logger } from '../../../logger.js'; +import { OutboundInterface } from '../interfaces.js'; +import { TOR_PROXY, TOR_PROXY_TYPE } from '../../../env.js'; +import { testTCPConnection } from '../../../helpers/network.js'; + +export default class TorOutbound implements OutboundInterface { + log = logger.extend('Network:Outbound:Tor'); + + running = false; + error?: Error; + readonly type = TOR_PROXY_TYPE; + readonly address = TOR_PROXY; + readonly available = !!TOR_PROXY; + + async start() { + try { + if (this.running) return; + this.running = true; + + this.log(`Connecting to ${TOR_PROXY}`); + const [host, port] = this.address?.split(':') ?? []; + if (!host || !port) throw new Error('Malformed proxy address'); + await testTCPConnection(host, parseInt(port), 3000); + } catch (error) { + this.running = false; + if (error instanceof Error) this.error = error; + } + } + + async stop() {} +} diff --git a/src/modules/network/outbound/websocket.ts b/src/modules/network/outbound/websocket.ts new file mode 100644 index 0000000..0b5d3bc --- /dev/null +++ b/src/modules/network/outbound/websocket.ts @@ -0,0 +1,11 @@ +import { ClientRequestArgs } from 'http'; +import { ClientOptions, WebSocket } from 'ws'; + +import outboundNetwork from './index.js'; + +/** extends the WebSocket class from ws to always use the custom http agent */ +export default class OutboundProxyWebSocket extends WebSocket { + constructor(address: string | URL, options?: ClientOptions | ClientRequestArgs) { + super(address, { agent: outboundNetwork.agent, ...options }); + } +} diff --git a/src/modules/notifications/notifications-manager.ts b/src/modules/notifications/notifications-manager.ts new file mode 100644 index 0000000..bbcd3a2 --- /dev/null +++ b/src/modules/notifications/notifications-manager.ts @@ -0,0 +1,155 @@ +import { NotificationChannel, WebPushNotification } from '@satellite-earth/core/types/control-api/notifications.js'; +import { getDMRecipient, getDMSender, getUserDisplayName, parseKind0Event } from '@satellite-earth/core/helpers/nostr'; +import { NostrEvent, kinds } from 'nostr-tools'; +import { npubEncode } from 'nostr-tools/nip19'; +import EventEmitter from 'events'; +import webPush from 'web-push'; +import dayjs from 'dayjs'; + +import { logger } from '../../logger.js'; +import App from '../../app/index.js'; + +export type NotificationsManagerState = { + channels: NotificationChannel[]; +}; + +type EventMap = { + addChannel: [NotificationChannel]; + updateChannel: [NotificationChannel]; + removeChannel: [NotificationChannel]; +}; + +export default class NotificationsManager extends EventEmitter { + log = logger.extend('Notifications'); + app: App; + lastRead: number = dayjs().unix(); + + webPushKeys: webPush.VapidKeys = webPush.generateVAPIDKeys(); + + state: NotificationsManagerState = { channels: [] }; + + get channels() { + return this.state.channels; + } + + constructor(app: App) { + super(); + this.app = app; + } + + async setup() { + this.state = ( + await this.app.state.getMutableState('notification-manager', { channels: [] }) + ).proxy; + } + + addOrUpdateChannel(channel: NotificationChannel) { + if (this.state.channels.some((c) => c.id === channel.id)) { + // update channel + this.log(`Updating channel ${channel.id} (${channel.type})`); + this.state.channels = this.state.channels.map((c) => { + if (c.id === channel.id) return channel; + else return c; + }); + this.emit('updateChannel', channel); + } else { + // add channel + this.log(`Added new channel ${channel.id} (${channel.type})`); + this.state.channels = [...this.state.channels, channel]; + this.emit('addChannel', channel); + } + } + removeChannel(id: string) { + const channel = this.state.channels.find((s) => s.id === id); + if (channel) { + this.log(`Removed channel ${id}`); + this.state.channels = this.state.channels.filter((s) => s.id !== id); + this.emit('removeChannel', channel); + } + } + + /** Whether a notification should be sent */ + shouldNotify(event: NostrEvent) { + if (event.kind !== kinds.EncryptedDirectMessage) return; + if (getDMRecipient(event) !== this.app.config.data.owner) return; + + if (event.created_at > this.lastRead) return true; + } + + /** builds a notification based on a nostr event */ + async buildNotification(event: NostrEvent) { + // TODO in the future we might need to build special notifications for channel type + switch (event.kind) { + case kinds.EncryptedDirectMessage: + const sender = getDMSender(event); + const senderProfileEvent = await this.app.profileBook.loadProfile(sender); + const senderProfile = senderProfileEvent ? parseKind0Event(senderProfileEvent) : undefined; + const senderName = getUserDisplayName(senderProfile, sender); + + return { + kind: event.kind, + event, + senderName, + senderProfile, + title: `Message from ${senderName}`, + body: 'Tap on notification to read', + icon: 'https://app.satellite.earth/logo-64x64.png', + // TODO: switch this to a satellite:// link once the native app supports it + url: `https://app.satellite.earth/messages/p/${npubEncode(sender)}`, + }; + } + } + + async notify(event: NostrEvent) { + const notification = await this.buildNotification(event); + if (!notification) return; + + this.log(`Sending notification for ${event.id} to ${this.state.channels.length} channels`); + + for (const channel of this.state.channels) { + this.log(`Sending notification "${notification.title}" to ${channel.id} (${channel.type})`); + try { + switch (channel.type) { + case 'web': + const pushNotification: WebPushNotification = { + title: notification.title, + body: notification.body, + icon: notification.icon, + url: notification.url, + event: notification.event, + }; + + await webPush.sendNotification(channel, JSON.stringify(pushNotification), { + vapidDetails: { + subject: 'mailto:admin@example.com', + publicKey: this.webPushKeys.publicKey, + privateKey: this.webPushKeys.privateKey, + }, + }); + break; + + case 'ntfy': + const headers: HeadersInit = { + Title: notification.title, + Icon: notification.icon, + Click: notification.url, + }; + + await fetch(new URL(channel.topic, channel.server), { + method: 'POST', + body: notification.body, + headers, + }).then((res) => res.text()); + break; + + default: + // @ts-expect-error + throw new Error(`Unknown channel type ${channel.type}`); + } + } catch (error) { + this.log(`Failed to notification ${channel.id} (${channel.type})`); + this.log(error); + } + } + } +} diff --git a/src/modules/profile-book.ts b/src/modules/profile-book.ts new file mode 100644 index 0000000..c3b3f48 --- /dev/null +++ b/src/modules/profile-book.ts @@ -0,0 +1,40 @@ +import { NostrEvent, kinds } from 'nostr-tools'; +import _throttle from 'lodash.throttle'; + +import { COMMON_CONTACT_RELAYS } from '../env.js'; +import { logger } from '../logger.js'; +import App from '../app/index.js'; +import PubkeyBatchLoader from './pubkey-batch-loader.js'; + +/** loads kind 0 metadata for pubkeys */ +export default class ProfileBook { + log = logger.extend('ProfileBook'); + app: App; + loader: PubkeyBatchLoader; + extraRelays = COMMON_CONTACT_RELAYS; + + constructor(app: App) { + this.app = app; + + this.loader = new PubkeyBatchLoader(kinds.Metadata, this.app.pool, (pubkey) => { + return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.Metadata], authors: [pubkey] }])?.[0]; + }); + + this.loader.on('event', (event) => this.app.eventStore.addEvent(event)); + this.loader.on('batch', (found, failed) => { + this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`); + }); + } + + getProfile(pubkey: string) { + return this.loader.getEvent(pubkey); + } + + handleEvent(event: NostrEvent) { + this.loader.handleEvent(event); + } + + async loadProfile(pubkey: string, relays: string[] = []) { + return this.loader.getOrLoadEvent(pubkey, relays); + } +} diff --git a/src/modules/pubkey-batch-loader.ts b/src/modules/pubkey-batch-loader.ts new file mode 100644 index 0000000..69127d1 --- /dev/null +++ b/src/modules/pubkey-batch-loader.ts @@ -0,0 +1,175 @@ +import { Filter, NostrEvent, SimplePool } from "nostr-tools"; +import _throttle from "lodash.throttle"; +import { EventEmitter } from "events"; +import { getInboxes, getOutboxes } from "@satellite-earth/core/helpers/nostr/mailboxes.js"; +import SuperMap from "@satellite-earth/core/helpers/super-map.js"; +import { Deferred, createDefer } from "applesauce-core/promise"; + +import { COMMON_CONTACT_RELAYS } from "../env.js"; + +type EventMap = { + event: [NostrEvent]; + batch: [number, number]; +}; + +/** Loads 10002 events for pubkeys */ +export default class PubkeyBatchLoader extends EventEmitter { + extraRelays = COMMON_CONTACT_RELAYS; + + kind: number; + pool: SimplePool; + loadFromCache?: (pubkey: string) => NostrEvent | undefined; + + get queue() { + return this.next.size; + } + + failed = new SuperMap>(() => new Set()); + + constructor(kind: number, pool: SimplePool, loadFromCache?: (pubkey: string) => NostrEvent | undefined) { + super(); + this.kind = kind; + this.pool = pool; + this.loadFromCache = loadFromCache; + } + + private cache = new Map(); + getEvent(pubkey: string) { + if (this.cache.has(pubkey)) return this.cache.get(pubkey)!; + + const event = this.loadFromCache?.(pubkey); + if (event) { + this.cache.set(pubkey, event); + return event; + } + } + + getOutboxes(pubkey: string) { + const mailboxes = this.getEvent(pubkey); + return mailboxes && getOutboxes(mailboxes); + } + + getInboxes(pubkey: string) { + const mailboxes = this.getEvent(pubkey); + return mailboxes && getInboxes(mailboxes); + } + + handleEvent(event: NostrEvent) { + if (event.kind === this.kind) { + this.emit("event", event); + const current = this.cache.get(event.pubkey); + if (!current || event.created_at > current.created_at) this.cache.set(event.pubkey, event); + } + } + + /** next queue */ + private next = new Map(); + /** currently fetching */ + private fetching = new Map(); + /** promises for next and fetching */ + private pending = new Map>(); + + private fetchEventsThrottle = _throttle(this.fetchEvents.bind(this), 1000); + private async fetchEvents() { + if (this.fetching.size > 0 || this.next.size === 0) return; + + // copy all from next queue to fetching queue + for (const [pubkey, relays] of this.next) this.fetching.set(pubkey, relays); + this.next.clear(); + + if (this.fetching.size > 0) { + const filters: Record = {}; + + for (const [pubkey, relays] of this.fetching) { + for (const relay of relays) { + filters[relay] = filters[relay] || { kinds: [this.kind], authors: [] }; + + if (!filters[relay].authors?.includes(pubkey)) { + filters[relay].authors?.push(pubkey); + } + } + } + + const requests: Record = {}; + for (const [relay, filter] of Object.entries(filters)) requests[relay] = [filter]; + + await new Promise((res) => { + const sub = this.pool.subscribeManyMap(requests, { + onevent: (event) => this.handleEvent(event), + oneose: () => { + sub.close(); + + // resolve all pending promises + let failed = 0; + let found = 0; + for (const [pubkey, relays] of this.fetching) { + const p = this.pending.get(pubkey); + if (p) { + const event = this.getEvent(pubkey) ?? null; + p.resolve(event); + if (!event) { + failed++; + for (const url of relays) this.failed.get(pubkey).add(url); + p.reject(); + } else found++; + this.pending.delete(pubkey); + } + } + this.fetching.clear(); + + this.emit("batch", found, failed); + + res(); + }, + }); + }); + + // if there are pending requests, make another request + if (this.next.size > 0) this.fetchEventsThrottle(); + } + } + + getOrLoadEvent(pubkey: string, relays: string[] = []): Promise { + // if its in the cache, return it + const event = this.getEvent(pubkey); + if (event) return Promise.resolve(event); + + // if its already being fetched, return promise + const pending = this.pending.get(pubkey); + if (pending) return pending; + + return this.loadEvent(pubkey, relays); + } + + loadEvent(pubkey: string, relays: string[] = [], ignoreFailed = false): Promise { + const urls = new Set(this.next.get(pubkey)); + + // add relays + for (const url of relays) urls.add(url); + + // add extra relays + for (const url of this.extraRelays) urls.add(url); + + // filter out failed relays + if (!ignoreFailed) { + const failed = this.failed.get(pubkey); + for (const url of failed) urls.delete(url); + } + + if (urls.size === 0) { + // nothing new to try return null + return Promise.resolve(null); + } + + // create a promise + const defer = createDefer(); + this.pending.set(pubkey, defer); + + // add pubkey and relay to next queue + this.next.set(pubkey, Array.from(urls)); + + // trigger queue + this.fetchEventsThrottle(); + return defer; + } +} diff --git a/src/modules/receiver/index.ts b/src/modules/receiver/index.ts new file mode 100644 index 0000000..8effeb0 --- /dev/null +++ b/src/modules/receiver/index.ts @@ -0,0 +1,259 @@ +import EventEmitter from 'events'; +import { NostrEvent, SimplePool, Filter } from 'nostr-tools'; +import SuperMap from '@satellite-earth/core/helpers/super-map.js'; +import { AbstractRelay, Subscription, SubscriptionParams } from 'nostr-tools/abstract-relay'; +import { getPubkeysFromList } from '@satellite-earth/core/helpers/nostr/lists.js'; +import { getInboxes, getOutboxes } from '@satellite-earth/core/helpers/nostr/mailboxes.js'; +import { getRelaysFromContactList } from '@satellite-earth/core/helpers/nostr/contacts.js'; + +import { BOOTSTRAP_RELAYS } from '../../env.js'; +import { logger } from '../../logger.js'; +import App from '../../app/index.js'; + +/** creates a new subscription and waits for it to get an event or close */ +function asyncSubscription(relay: AbstractRelay, filters: Filter[], opts: SubscriptionParams) { + let resolved = false; + + return new Promise((res, rej) => { + const sub = relay.subscribe(filters, { + onevent: (event) => { + if (!resolved) res(sub); + opts.onevent?.(event); + }, + oneose: () => { + if (!resolved) res(sub); + opts.oneose?.(); + }, + onclose: (reason) => { + if (!resolved) rej(new Error(reason)); + opts.onclose?.(reason); + }, + }); + }); +} + +type EventMap = { + started: [Receiver]; + stopped: [Receiver]; + status: [string]; + rebuild: []; + subscribed: [string, string[]]; + closed: [string, string[]]; + error: [Error]; + event: [NostrEvent]; +}; + +type ReceiverStatus = 'running' | 'starting' | 'errored' | 'stopped'; + +export default class Receiver extends EventEmitter { + log = logger.extend('Receiver'); + + _status: ReceiverStatus = 'stopped'; + get status() { + return this._status; + } + set status(v: ReceiverStatus) { + this._status = v; + this.emit('status', v); + } + + starting = true; + startupError?: Error; + + app: App; + pool: SimplePool; + + subscriptions = new Map(); + + constructor(app: App, pool?: SimplePool) { + super(); + this.app = app; + this.pool = pool || app.pool; + } + + // pubkey -> relays + private pubkeyRelays = new Map>(); + // relay url -> pubkeys + private relayPubkeys = new SuperMap>(() => new Set()); + + // the current request map in the format of relay -> pubkeys + map = new SuperMap>(() => new Set()); + + async fetchData() { + const owner = this.app.config.data.owner; + if (!owner) throw new Error('Missing owner'); + + const ownerMailboxes = await this.app.addressBook.loadMailboxes(owner); + const ownerInboxes = getInboxes(ownerMailboxes); + const ownerOutboxes = getOutboxes(ownerMailboxes); + + this.log('Searching for owner kind:3 contacts'); + const contacts = await this.app.contactBook.loadContacts(owner); + if (!contacts) throw new Error('Cant find contacts'); + + this.pubkeyRelays.clear(); + this.relayPubkeys.clear(); + + // add the owners details + this.pubkeyRelays.set(owner, new Set(ownerOutboxes)); + for (const url of ownerOutboxes) this.relayPubkeys.get(url).add(owner); + + const people = getPubkeysFromList(contacts); + + this.log(`Found ${people.length} contacts`); + + let usersWithMailboxes = 0; + let usersWithContactRelays = 0; + let usersWithFallbackRelays = 0; + + // fetch all addresses in parallel + await Promise.all( + people.map(async (person) => { + const mailboxes = await this.app.addressBook.loadMailboxes(person.pubkey, ownerInboxes ?? BOOTSTRAP_RELAYS); + + let relays = getOutboxes(mailboxes); + + // if the user does not have any mailboxes try to get the relays stored in the contact list + if (relays.length === 0) { + this.log(`Failed to find mailboxes for ${person.pubkey}`); + const contacts = await this.app.contactBook.loadContacts(person.pubkey, ownerInboxes ?? BOOTSTRAP_RELAYS); + + if (contacts && contacts.content.startsWith('{')) { + const parsed = getRelaysFromContactList(contacts); + if (parsed) { + relays = parsed.filter((r) => r.write).map((r) => r.url); + usersWithContactRelays++; + } else { + relays = BOOTSTRAP_RELAYS; + usersWithFallbackRelays++; + } + } else { + relays = BOOTSTRAP_RELAYS; + usersWithFallbackRelays++; + } + } else usersWithMailboxes++; + + // add pubkey details + this.pubkeyRelays.set(person.pubkey, new Set(relays)); + for (const url of relays) this.relayPubkeys.get(url).add(person.pubkey); + }), + ); + + this.log( + `Found ${usersWithMailboxes} users with mailboxes, ${usersWithContactRelays} user with relays in contact list, and ${usersWithFallbackRelays} using fallback relays`, + ); + } + + buildMap() { + this.map.clear(); + + // sort pubkey relays by popularity + for (const [pubkey, relays] of this.pubkeyRelays) { + const sorted = Array.from(relays).sort((a, b) => this.relayPubkeys.get(b).size - this.relayPubkeys.get(a).size); + + // add the pubkey to their top two relays + for (const url of sorted.slice(0, 2)) this.map.get(url).add(pubkey); + } + + this.emit('rebuild'); + + return this.map; + } + + private handleEvent(event: NostrEvent) { + this.emit('event', event); + } + + async updateRelaySubscription(url: string) { + const pubkeys = this.map.get(url); + if (pubkeys.size === 0) return; + + const subscription = this.subscriptions.get(url); + if (!subscription || subscription.closed) { + const relay = await this.app.pool.ensureRelay(url); + + const sub = relay.subscribe([{ authors: Array.from(pubkeys) }], { + onevent: this.handleEvent.bind(this), + onclose: () => { + this.emit('closed', url, Array.from(pubkeys)); + // wait 30 seconds then try to reconnect + setTimeout(() => { + this.updateRelaySubscription(url); + }, 30_000); + }, + }); + + this.emit('subscribed', url, Array.from(pubkeys)); + this.subscriptions.set(url, sub); + this.log(`Subscribed to ${url} for ${pubkeys.size} pubkeys`); + } else { + const hasOld = subscription.filters[0].authors?.some((p) => !pubkeys.has(p)); + const hasNew = Array.from(pubkeys).some((p) => !subscription.filters[0].authors?.includes(p)); + + if (hasNew || hasOld) { + // reset the subscription + subscription.eosed = false; + subscription.filters = [{ authors: Array.from(pubkeys) }]; + subscription.fire(); + this.log(`Subscribed to ${url} with ${pubkeys.size} pubkeys`); + } + } + } + + ensureSubscriptions() { + const promises: Promise[] = []; + + for (const [url, pubkeys] of this.map) { + const p = this.updateRelaySubscription(url).catch((error) => { + // failed to connect to relay + // this needs to be remembered and the subscription map should be rebuilt accordingly + }); + + promises.push(p); + } + + return Promise.all(promises); + } + + async start() { + if (this.status === 'running' || this.status === 'starting') return; + + try { + this.log('Starting'); + this.startupError = undefined; + this.status = 'starting'; + + await this.fetchData(); + this.buildMap(); + await this.ensureSubscriptions(); + + this.status = 'running'; + this.emit('started', this); + } catch (error) { + this.status = 'errored'; + if (error instanceof Error) { + this.startupError = error; + this.log(`Failed to start receiver`, error.message); + this.emit('error', error); + } + } + } + + /** stop receiving events and disconnect from all relays */ + stop() { + if (this.status === 'stopped') return; + + this.status = 'stopped'; + + for (const [relay, sub] of this.subscriptions) sub.close(); + this.subscriptions.clear(); + + this.log('Stopped'); + this.emit('stopped', this); + } + + destroy() { + this.stop(); + this.removeAllListeners(); + } +} diff --git a/src/modules/reports/report.ts b/src/modules/reports/report.ts new file mode 100644 index 0000000..a79a104 --- /dev/null +++ b/src/modules/reports/report.ts @@ -0,0 +1,77 @@ +import { WebSocket } from "ws"; +import { ReportErrorMessage, ReportResultMessage } from "@satellite-earth/core/types/control-api/reports.js"; +import { ReportArguments, ReportResults } from "@satellite-earth/core/types"; + +import type App from "../../app/index.js"; +import { logger } from "../../logger.js"; + +type f = () => void; + +export default class Report { + id: string; + // @ts-expect-error + readonly type: T = ""; + socket: WebSocket | NodeJS.Process; + app: App; + running = false; + log = logger.extend("Report"); + args?: ReportArguments[T]; + + private setupTeardown?: void | f; + + constructor(id: string, app: App, socket: WebSocket | NodeJS.Process) { + this.id = id; + this.socket = socket; + this.app = app; + + this.log = logger.extend("Report:" + this.type); + } + + private sendError(message: string) { + this.socket.send?.(JSON.stringify(["CONTROL", "REPORT", "ERROR", this.id, message] satisfies ReportErrorMessage)); + } + + // override when extending + /** This method is run only once when the report starts */ + async setup(args: ReportArguments[T]): Promise {} + /** this method is run every time the client sends new arguments */ + async execute(args: ReportArguments[T]) {} + /** this method is run when the report is closed */ + cleanup() {} + + // private methods + protected send(result: ReportResults[T]) { + this.socket.send?.( + JSON.stringify(["CONTROL", "REPORT", "RESULT", this.id, result] satisfies ReportResultMessage), + ); + } + + // public api + async run(args: ReportArguments[T]) { + try { + this.args = args; + if (this.running === false) { + // hack to make sure the .log is extended correctly + this.log = logger.extend("Report:" + this.type); + + this.setupTeardown = await this.setup(args); + } + + this.log(`Executing with args`, JSON.stringify(args)); + await this.execute(args); + this.running = true; + } catch (error) { + if (error instanceof Error) this.sendError(error.message); + else this.sendError("Unknown server error"); + + if (error instanceof Error) this.log("Error: " + error.message); + + throw error; + } + } + close() { + this.setupTeardown?.(); + this.cleanup(); + this.running = false; + } +} diff --git a/src/modules/reports/reports/conversations.ts b/src/modules/reports/reports/conversations.ts new file mode 100644 index 0000000..32c092e --- /dev/null +++ b/src/modules/reports/reports/conversations.ts @@ -0,0 +1,115 @@ +import { ReportArguments, ReportResults } from '@satellite-earth/core/types'; +import { NostrEvent } from 'nostr-tools'; +import { getTagValue } from '@satellite-earth/core/helpers/nostr'; +import SuperMap from '@satellite-earth/core/helpers/super-map.js'; + +import Report from '../report.js'; + +export default class ConversationsReport extends Report<'CONVERSATIONS'> { + readonly type = 'CONVERSATIONS'; + + private async getConversationResult(self: string, other: string) { + const sent = this.app.database.db + .prepare<[string, string], { pubkey: string; count: number; lastMessage: number }>( + ` + SELECT tags.v as pubkey, count(events.id) as count, max(events.created_at) as lastMessage FROM tags + INNER JOIN events ON events.id = tags.e + WHERE events.kind = 4 AND tags.t = 'p' AND events.pubkey = ? AND tags.v = ?`, + ) + .get(self, other); + + const received = this.app.database.db + .prepare<[string, string], { pubkey: string; count: number; lastMessage: number }>( + ` + SELECT events.pubkey, count(events.id) as count, max(events.created_at) as lastMessage FROM events + INNER JOIN tags ON tags.e = events.id + WHERE events.kind = 4 AND tags.t = 'p' AND tags.v = ? AND events.pubkey = ?`, + ) + .get(self, other); + + const result: ReportResults['CONVERSATIONS'] = { + pubkey: other, + count: (received?.count ?? 0) + (sent?.count ?? 0), + sent: 0, + received: 0, + }; + + if (received) { + result.received = received.count; + result.lastReceived = received.lastMessage; + } + if (sent) { + result.sent = sent.count; + result.lastSent = sent.lastMessage; + } + + return result; + } + private async getAllConversationResults(self: string) { + const sent = this.app.database.db + .prepare<[string], { pubkey: string; count: number; lastMessage: number }>( + ` + SELECT tags.v as pubkey, count(tags.v) as count, max(events.created_at) as lastMessage FROM tags + INNER JOIN events ON events.id = tags.e + WHERE events.kind = 4 AND tags.t = 'p' AND events.pubkey = ? + GROUP BY tags.v`, + ) + .all(self); + + const received = this.app.database.db + .prepare<[string], { pubkey: string; count: number; lastMessage: number }>( + ` + SELECT events.pubkey, count(events.pubkey) as count, max(events.created_at) as lastMessage FROM events + INNER JOIN tags ON tags.e = events.id + WHERE events.kind = 4 AND tags.t = 'p' AND tags.v = ? + GROUP BY events.pubkey`, + ) + .all(self); + + const results = new SuperMap((pubkey) => ({ + pubkey, + count: sent.length + received.length, + sent: 0, + received: 0, + })); + + for (const { pubkey, count, lastMessage } of received) { + const result = results.get(pubkey); + result.received = count; + result.lastReceived = lastMessage; + } + for (const { pubkey, count, lastMessage } of sent) { + const result = results.get(pubkey); + result.sent = count; + result.lastSent = lastMessage; + } + + return Array.from(results.values()).sort( + (a, b) => Math.max(b.lastReceived ?? 0, b.lastSent ?? 0) - Math.max(a.lastReceived ?? 0, a.lastSent ?? 0), + ); + } + + async setup(args: ReportArguments['CONVERSATIONS']) { + const listener = (event: NostrEvent) => { + const from = event.pubkey; + const to = getTagValue(event, 'p'); + if (!to) return; + + const self = args.pubkey; + + // get the latest stats from the database + this.getConversationResult(self, self === from ? to : from).then((result) => this.send(result)); + }; + + this.app.directMessageManager.on('message', listener); + return () => this.app.directMessageManager.off('message', listener); + } + + async execute(args: ReportArguments['CONVERSATIONS']) { + const results = await this.getAllConversationResults(args.pubkey); + + for (const result of results) { + this.send(result); + } + } +} diff --git a/src/modules/reports/reports/dm-search.ts b/src/modules/reports/reports/dm-search.ts new file mode 100644 index 0000000..c166bd1 --- /dev/null +++ b/src/modules/reports/reports/dm-search.ts @@ -0,0 +1,11 @@ +import { ReportArguments } from '@satellite-earth/core/types'; +import Report from '../report.js'; + +export default class DMSearchReport extends Report<'DM_SEARCH'> { + readonly type = 'DM_SEARCH'; + + async execute(args: ReportArguments['DM_SEARCH']) { + const results = await this.app.decryptionCache.search(args.query, args); + for (const result of results) this.send(result); + } +} diff --git a/src/modules/reports/reports/events-summary.ts b/src/modules/reports/reports/events-summary.ts new file mode 100644 index 0000000..dc3de88 --- /dev/null +++ b/src/modules/reports/reports/events-summary.ts @@ -0,0 +1,69 @@ +import { ReportArguments } from '@satellite-earth/core/types'; +import { EventRow, parseEventRow } from '@satellite-earth/core'; +import Report from '../report.js'; + +export default class EventsSummaryReport extends Report<'EVENTS_SUMMARY'> { + readonly type = 'EVENTS_SUMMARY'; + + async execute(args: ReportArguments['EVENTS_SUMMARY']): Promise { + let sql = ` + SELECT + events.*, + COUNT(l.id) AS reactions, + COUNT(s.id) AS shares, + COUNT(r.id) AS replies, + (events.kind || ':' || events.pubkey || ':' || events.d) as address + FROM events + LEFT JOIN tags ON ( tags.t = 'e' AND tags.v = events.id ) OR ( tags.t = 'a' AND tags.v = address ) + LEFT JOIN events AS l ON l.id = tags.e AND l.kind = 7 + LEFT JOIN events AS s ON s.id = tags.e AND (s.kind = 6 OR s.kind = 16) + LEFT JOIN events AS r ON r.id = tags.e AND r.kind = 1 + `; + + const params: any[] = []; + const conditions: string[] = []; + + if (args.kind !== undefined) { + conditions.push(`events.kind = ?`); + params.push(args.kind); + } + if (args.pubkey !== undefined) { + conditions.push(`events.pubkey = ?`); + params.push(args.pubkey); + } + + if (conditions.length > 0) { + sql += ` WHERE ${conditions.join(' AND ')}\n`; + } + + sql += ' GROUP BY events.id\n'; + + switch (args.order) { + case 'created_at': + sql += ` ORDER BY events.created_at DESC\n`; + break; + default: + case 'interactions': + sql += ` ORDER BY reactions + shares + replies DESC\n`; + break; + } + + let limit = args.limit || 100; + sql += ` LIMIT ?`; + params.push(limit); + + const rows = await this.app.database.db + .prepare(sql) + .all(...params); + + const results = rows.map((row) => { + const event = parseEventRow(row); + + return { event, reactions: row.reactions, shares: row.shares, replies: row.replies }; + }); + + for (const result of results) { + this.send(result); + } + } +} diff --git a/src/modules/reports/reports/index.ts b/src/modules/reports/reports/index.ts new file mode 100644 index 0000000..3e609ba --- /dev/null +++ b/src/modules/reports/reports/index.ts @@ -0,0 +1,30 @@ +import { ReportArguments } from '@satellite-earth/core/types'; +import Report from '../report.js'; + +import OverviewReport from './overview.js'; +import ConversationsReport from './conversations.js'; +import LogsReport from './logs.js'; +import ServicesReport from './services.js'; +import DMSearchReport from './dm-search.js'; +import ScrapperStatusReport from './scrapper-status.js'; +import ReceiverStatusReport from './receiver-status.js'; +import NetworkStatusReport from './network-status.js'; +import NotificationChannelsReport from './notification-channels.js'; +import EventsSummaryReport from './events-summary.js'; + +const REPORT_CLASSES: { + [k in keyof ReportArguments]?: typeof Report; +} = { + OVERVIEW: OverviewReport, + CONVERSATIONS: ConversationsReport, + LOGS: LogsReport, + SERVICES: ServicesReport, + DM_SEARCH: DMSearchReport, + SCRAPPER_STATUS: ScrapperStatusReport, + RECEIVER_STATUS: ReceiverStatusReport, + NETWORK_STATUS: NetworkStatusReport, + NOTIFICATION_CHANNELS: NotificationChannelsReport, + EVENTS_SUMMARY: EventsSummaryReport, +}; + +export default REPORT_CLASSES; diff --git a/src/modules/reports/reports/logs.ts b/src/modules/reports/reports/logs.ts new file mode 100644 index 0000000..7a6fefa --- /dev/null +++ b/src/modules/reports/reports/logs.ts @@ -0,0 +1,23 @@ +import { ReportArguments } from '@satellite-earth/core/types'; + +import { LogEntry } from '../../log-store/log-store.js'; +import Report from '../report.js'; + +/** WARNING: be careful of calling this.log in this class. it could trigger an infinite loop of logging */ +export default class LogsReport extends Report<'LOGS'> { + readonly type = 'LOGS'; + + async setup() { + const listener = (entry: LogEntry) => { + if (!this.args?.service || entry.service === this.args.service) this.send(entry); + }; + + this.app.logStore.on('log', listener); + return () => this.app.logStore.off('log', listener); + } + + async execute(args: ReportArguments['LOGS']) { + const logs = this.app.logStore.getLogs({ service: args.service, limit: 500 }); + for (const entry of logs) this.send(entry); + } +} diff --git a/src/modules/reports/reports/network-status.ts b/src/modules/reports/reports/network-status.ts new file mode 100644 index 0000000..4d1ee28 --- /dev/null +++ b/src/modules/reports/reports/network-status.ts @@ -0,0 +1,69 @@ +import Report from '../report.js'; + +export default class NetworkStatusReport extends Report<'NETWORK_STATUS'> { + readonly type = 'NETWORK_STATUS'; + + update() { + const torIn = this.app.inboundNetwork.tor; + const torOut = this.app.outboundNetwork.tor; + const hyperIn = this.app.inboundNetwork.hyper; + const hyperOut = this.app.outboundNetwork.hyper; + const i2pIn = this.app.inboundNetwork.i2p; + const i2pOut = this.app.outboundNetwork.i2p; + + this.send({ + tor: { + inbound: { + available: torIn.available, + running: torIn.running, + error: torIn.error?.message, + address: torIn.address, + }, + outbound: { + available: torOut.available, + running: torOut.running, + error: torOut.error?.message, + }, + }, + hyper: { + inbound: { + available: hyperIn.available, + running: hyperIn.running, + error: hyperIn.error?.message, + address: hyperIn.address, + }, + outbound: { + available: hyperOut.available, + running: hyperOut.running, + error: hyperOut.error?.message, + }, + }, + i2p: { + inbound: { + available: i2pIn.available, + running: i2pIn.running, + error: i2pIn.error?.message, + address: i2pIn.address, + }, + outbound: { + available: i2pOut.available, + running: i2pOut.running, + error: i2pOut.error?.message, + }, + }, + }); + } + + async setup() { + const listener = this.update.bind(this); + + // NOTE: set and interval since there are not events to listen to yet + const i = setInterval(listener, 1000); + + return () => clearInterval(i); + } + + async execute(args: {}): Promise { + this.update(); + } +} diff --git a/src/modules/reports/reports/notification-channels.ts b/src/modules/reports/reports/notification-channels.ts new file mode 100644 index 0000000..4cd7466 --- /dev/null +++ b/src/modules/reports/reports/notification-channels.ts @@ -0,0 +1,29 @@ +import { NotificationChannel } from '@satellite-earth/core/types/control-api/notifications.js'; +import Report from '../report.js'; + +export default class NotificationChannelsReport extends Report<'NOTIFICATION_CHANNELS'> { + readonly type = 'NOTIFICATION_CHANNELS'; + + async setup() { + const listener = this.send.bind(this); + const removeListener = (channel: NotificationChannel) => { + this.send(['removed', channel.id]); + }; + + this.app.notifications.on('addChannel', listener); + this.app.notifications.on('updateChannel', listener); + this.app.notifications.on('removeChannel', removeListener); + + return () => { + this.app.notifications.off('addChannel', listener); + this.app.notifications.off('updateChannel', listener); + this.app.notifications.off('removeChannel', removeListener); + }; + } + + async execute(args: {}): Promise { + for (const channel of this.app.notifications.channels) { + this.send(channel); + } + } +} diff --git a/src/modules/reports/reports/overview.ts b/src/modules/reports/reports/overview.ts new file mode 100644 index 0000000..c011e00 --- /dev/null +++ b/src/modules/reports/reports/overview.ts @@ -0,0 +1,40 @@ +import { NostrEvent } from 'nostr-tools'; +import { ReportArguments } from '@satellite-earth/core/types'; + +import Report from '../report.js'; + +export default class OverviewReport extends Report<'OVERVIEW'> { + readonly type = 'OVERVIEW'; + + async setup() { + const listener = (event: NostrEvent) => { + // update summary for pubkey + const result = this.app.database.db + .prepare< + [string], + { pubkey: string; events: number; active: number } + >(`SELECT pubkey, COUNT(events.id) as \`events\`, MAX(created_at) as \`active\` FROM events WHERE pubkey=?`) + .get(event.pubkey); + + if (result) this.send(result); + }; + + this.app.eventStore.on('event:inserted', listener); + return () => { + this.app.eventStore.off('event:inserted', listener); + }; + } + + async execute(args: ReportArguments['OVERVIEW']) { + const results = await this.app.database.db + .prepare< + [], + { pubkey: string; events: number; active: number } + >(`SELECT pubkey, COUNT(events.id) as \`events\`, MAX(created_at) as \`active\` FROM events GROUP BY pubkey ORDER BY \`events\` DESC`) + .all(); + + for (const result of results) { + this.send(result); + } + } +} diff --git a/src/modules/reports/reports/receiver-status.ts b/src/modules/reports/reports/receiver-status.ts new file mode 100644 index 0000000..33937c6 --- /dev/null +++ b/src/modules/reports/reports/receiver-status.ts @@ -0,0 +1,38 @@ +import Report from '../report.js'; + +export default class ReceiverStatusReport extends Report<'RECEIVER_STATUS'> { + readonly type = 'RECEIVER_STATUS'; + + update() { + this.send({ + status: this.app.receiver.status, + startError: this.app.receiver.startupError?.message, + subscriptions: Array.from(this.app.receiver.map).map(([relay, pubkeys]) => ({ + relay, + pubkeys: Array.from(pubkeys), + active: !!this.app.receiver.subscriptions.get(relay), + closed: !!this.app.receiver.subscriptions.get(relay)?.closed, + })), + }); + } + + async setup() { + const listener = this.update.bind(this); + + this.app.receiver.on('status', listener); + this.app.receiver.on('subscribed', listener); + this.app.receiver.on('closed', listener); + this.app.receiver.on('error', listener); + + return () => { + this.app.receiver.off('status', listener); + this.app.receiver.off('subscribed', listener); + this.app.receiver.off('closed', listener); + this.app.receiver.off('error', listener); + }; + } + + async execute(args: {}): Promise { + this.update(); + } +} diff --git a/src/modules/reports/reports/scrapper-status.ts b/src/modules/reports/reports/scrapper-status.ts new file mode 100644 index 0000000..6611c4e --- /dev/null +++ b/src/modules/reports/reports/scrapper-status.ts @@ -0,0 +1,55 @@ +import { NostrEvent } from 'nostr-tools'; +import _throttle from 'lodash.throttle'; +import Report from '../report.js'; + +export default class ScrapperStatusReport extends Report<'SCRAPPER_STATUS'> { + readonly type = 'SCRAPPER_STATUS'; + + eventsPerSecond: number[] = [0]; + + update() { + const averageEventsPerSecond = this.eventsPerSecond.reduce((m, v) => m + v, 0) / this.eventsPerSecond.length; + const pubkeys = this.app.scrapper.state.pubkeys; + + let activeSubscriptions = 0; + for (const [pubkey, scrapper] of this.app.scrapper.scrappers) { + for (const [relay, relayScrapper] of scrapper.relayScrappers) { + if (relayScrapper.running) activeSubscriptions++; + } + } + + this.send({ + running: this.app.scrapper.running, + eventsPerSecond: averageEventsPerSecond, + activeSubscriptions, + pubkeys, + }); + } + + async setup() { + const onEvent = (event: NostrEvent) => { + this.eventsPerSecond[0]++; + }; + + this.app.scrapper.on('event', onEvent); + + const tick = setInterval(() => { + // start a new second + this.eventsPerSecond.unshift(0); + + // limit to 60 seconds + while (this.eventsPerSecond.length > 60) this.eventsPerSecond.pop(); + + this.update(); + }, 1000); + + return () => { + this.app.scrapper.off('event', onEvent); + clearInterval(tick); + }; + } + + async execute(args: {}): Promise { + this.update(); + } +} diff --git a/src/modules/reports/reports/services.ts b/src/modules/reports/reports/services.ts new file mode 100644 index 0000000..ab24fa5 --- /dev/null +++ b/src/modules/reports/reports/services.ts @@ -0,0 +1,12 @@ +import Report from '../report.js'; + +export default class ServicesReport extends Report<'SERVICES'> { + readonly type = 'SERVICES'; + + async execute() { + const services = this.app.database.db + .prepare<[], { id: string }>(`SELECT service as id FROM logs GROUP BY service`) + .all(); + for (const service of services) this.send(service); + } +} diff --git a/src/modules/scrapper/index.ts b/src/modules/scrapper/index.ts new file mode 100644 index 0000000..752982d --- /dev/null +++ b/src/modules/scrapper/index.ts @@ -0,0 +1,145 @@ +import SuperMap from "@satellite-earth/core/helpers/super-map.js"; +import { EventEmitter } from "events"; +import { NostrEvent } from "nostr-tools"; +import { Deferred, createDefer } from "applesauce-core/promise"; + +import App from "../../app/index.js"; +import { logger } from "../../logger.js"; +import { getPubkeysFromList } from "@satellite-earth/core/helpers/nostr/lists.js"; +import PubkeyScrapper from "./pubkey-scrapper.js"; + +const MAX_TASKS = 10; + +type EventMap = { + event: [NostrEvent]; +}; + +export type ScrapperState = { + pubkeys: string[]; +}; + +export default class Scrapper extends EventEmitter { + app: App; + log = logger.extend("scrapper:service"); + + state: ScrapperState = { pubkeys: [] }; + + // pubkey -> relay -> scrapper + scrappers = new SuperMap((pubkey) => { + const scrapper = new PubkeyScrapper(this.app, pubkey); + scrapper.on("event", (event) => this.emit("event", event)); + return scrapper; + }); + + constructor(app: App) { + super(); + this.app = app; + } + + async setup() { + this.state = (await this.app.state.getMutableState("scrapper", { pubkeys: [] })).proxy; + } + + async ensureData() { + if (!this.app.config.data.owner) throw new Error("Owner not setup yet"); + + // get mailboxes and contacts + const mailboxes = await this.app.addressBook.loadMailboxes(this.app.config.data.owner); + const contacts = await this.app.contactBook.loadContacts(this.app.config.data.owner); + + if (!contacts) throw new Error("Missing contact list"); + + return { contacts: getPubkeysFromList(contacts), mailboxes }; + } + + private async scrapeOwner() { + if (!this.running) return; + + try { + if (!this.app.config.data.owner) throw new Error("Owner not setup yet"); + + const scrapper = this.scrappers.get(this.app.config.data.owner); + await scrapper.loadNext(); + } catch (error) { + // eat error + } + + setTimeout(this.scrapeOwner.bind(this), 1000); + } + + private async scrapeForPubkey(pubkey: string, relay?: string) { + const scrapper = this.scrappers.get(pubkey); + if (relay) scrapper.additionalRelays = [relay]; + + return await scrapper.loadNext(); + } + + tasks = new Set>(); + private block?: Deferred; + private waitForBlock() { + if (this.block) return this.block; + + this.block = createDefer(); + return this.block; + } + private unblock() { + if (this.block) { + this.block?.resolve(); + this.block = undefined; + } + } + + async scrapeContacts() { + if (!this.running) return; + + const { contacts } = await this.ensureData(); + + for (const person of contacts) { + // check if the pubkey should be scraped + if (!this.state.pubkeys.includes(person.pubkey)) continue; + + // await here if the task queue if full + if (this.tasks.size >= MAX_TASKS) await this.waitForBlock(); + + // check running again since this is resuming + if (!this.running) return; + + const promise = this.scrapeForPubkey(person.pubkey, person.relay); + + // add it to the tasks array + this.tasks.add(promise); + + promise + .catch((err) => { + // eat the error + }) + .finally(() => { + this.tasks.delete(promise); + this.unblock(); + }); + } + + // set timeout for next batch + setTimeout(this.scrapeContacts.bind(this), 1000); + } + + running = false; + start() { + this.running = true; + + this.scrapeOwner(); + this.scrapeContacts(); + } + + stop() { + this.running = false; + } + + addPubkey(pubkey: string) { + this.state.pubkeys.push(pubkey); + } + + removePubkey(pubkey: string) { + this.state.pubkeys = this.state.pubkeys.filter((p) => p !== pubkey); + } +} diff --git a/src/modules/scrapper/pubkey-relay-scrapper.ts b/src/modules/scrapper/pubkey-relay-scrapper.ts new file mode 100644 index 0000000..fc28c00 --- /dev/null +++ b/src/modules/scrapper/pubkey-relay-scrapper.ts @@ -0,0 +1,115 @@ +import dayjs from 'dayjs'; +import { EventEmitter } from 'events'; +import { NostrEvent } from 'nostr-tools'; +import { Debugger } from 'debug'; +import { AbstractRelay, Subscription } from 'nostr-tools/abstract-relay'; + +import { logger } from '../../logger.js'; + +function stripProtocol(url: string) { + return url.replace(/^\w+\:\/\//, ''); +} + +const DEFAULT_LIMIT = 1000; + +export type PubkeyRelayScrapperState = { + cursor?: number; + complete?: boolean; +}; + +type EventMap = { + event: [NostrEvent]; + chunk: [{ count: number; cursor: number }]; +}; + +export default class PubkeyRelayScrapper extends EventEmitter { + pubkey: string; + relay: AbstractRelay; + log: Debugger; + + running = false; + error?: Error; + state: PubkeyRelayScrapperState = {}; + + get cursor() { + return this.state.cursor || dayjs().unix(); + } + set cursor(v: number) { + this.state.cursor = v; + } + get complete() { + return this.state.complete || false; + } + set complete(v: boolean) { + this.state.complete = v; + } + + private subscription?: Subscription; + + constructor(pubkey: string, relay: AbstractRelay, state?: PubkeyRelayScrapperState) { + super(); + + this.pubkey = pubkey; + this.relay = relay; + if (state) this.state = state; + + this.log = logger.extend('scrapper:' + pubkey + ':' + stripProtocol(relay.url)); + } + + async loadNext() { + // don't run if its already running, complete, or has an error + if (this.running || this.complete || this.error) return; + + this.running = true; + + // wait for relay connection + await this.relay.connect(); + + const cursor = this.state.cursor || dayjs().unix(); + this.log(`Requesting from ${dayjs.unix(cursor).format('lll')} (${cursor})`); + + // return a promise to wait for the subscription to end + return new Promise((res, rej) => { + let count = 0; + let newCursor = cursor; + this.subscription = this.relay.subscribe([{ authors: [this.pubkey], until: cursor, limit: DEFAULT_LIMIT }], { + onevent: (event) => { + this.emit('event', event); + count++; + + newCursor = Math.min(newCursor, event.created_at); + }, + oneose: () => { + this.running = false; + + // if no events where returned, mark complete + if (count === 0) { + // connection closed before events could be returned, ignore complete + if (this.subscription?.closed === true) return; + + this.complete = true; + this.log('Got 0 events, complete'); + } else { + this.log(`Got ${count} events and moved cursor to ${dayjs.unix(newCursor).format('lll')} (${newCursor})`); + } + + this.state.cursor = newCursor - 1; + this.emit('chunk', { count, cursor: this.cursor }); + + this.subscription?.close(); + res(); + }, + onclose: (reason) => { + if (reason !== 'closed by caller') { + // unexpected close + this.log(`Error: ${reason}`); + this.error = new Error(reason); + + rej(this.error); + } + res(); + }, + }); + }); + } +} diff --git a/src/modules/scrapper/pubkey-scrapper.ts b/src/modules/scrapper/pubkey-scrapper.ts new file mode 100644 index 0000000..c73e8f6 --- /dev/null +++ b/src/modules/scrapper/pubkey-scrapper.ts @@ -0,0 +1,83 @@ +import App from '../../app/index.js'; +import { NostrEvent } from 'nostr-tools'; +import { EventEmitter } from 'events'; +import { Debugger } from 'debug'; + +import { getOutboxes } from '@satellite-earth/core/helpers/nostr/mailboxes.js'; +import PubkeyRelayScrapper, { PubkeyRelayScrapperState } from './pubkey-relay-scrapper.js'; +import { logger } from '../../logger.js'; + +type EventMap = { + event: [NostrEvent]; +}; + +export default class PubkeyScrapper extends EventEmitter { + app: App; + pubkey: string; + additionalRelays: string[] = []; + log: Debugger; + + private failed = new Set(); + relayScrappers = new Map(); + + constructor(app: App, pubkey: string) { + super(); + this.app = app; + this.pubkey = pubkey; + + this.log = logger.extend('scrapper:' + this.pubkey); + } + + async ensureData() { + // get mailboxes + this.app.profileBook.loadProfile(this.pubkey); + const mailboxes = await this.app.addressBook.loadMailboxes(this.pubkey); + + return { mailboxes }; + } + + async loadNext() { + const { mailboxes } = await this.ensureData(); + + const outboxes = getOutboxes(mailboxes); + + const relays = [...outboxes, ...this.additionalRelays]; + const scrappers: PubkeyRelayScrapper[] = []; + for (const url of relays) { + if (this.failed.has(url)) continue; + + try { + let scrapper = this.relayScrappers.get(url); + if (!scrapper) { + const relay = await this.app.pool.ensureRelay(url); + scrapper = new PubkeyRelayScrapper(this.pubkey, relay); + scrapper.on('event', (event) => this.emit('event', event)); + + // load the state from the database + const state = await this.app.state.getMutableState( + `${this.pubkey}|${relay.url}`, + {}, + ); + if (state) scrapper.state = state.proxy; + + this.relayScrappers.set(url, scrapper); + } + + scrappers.push(scrapper); + } catch (error) { + this.failed.add(url); + if (error instanceof Error) this.log(`Failed to create relay scrapper for ${url}`, error.message); + } + } + + // call loadNext on the one with the latest cursor + const incomplete = scrappers + .filter((s) => !s.complete && !s.running && !s.error) + .sort((a, b) => b.cursor - a.cursor); + + const next = incomplete[0]; + if (next) { + await next.loadNext(); + } + } +} diff --git a/src/modules/secrets-manager.ts b/src/modules/secrets-manager.ts new file mode 100644 index 0000000..19e4b6d --- /dev/null +++ b/src/modules/secrets-manager.ts @@ -0,0 +1,125 @@ +import _throttle from 'lodash.throttle'; +import { generateSecretKey } from 'nostr-tools'; +import EventEmitter from 'events'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import webPush from 'web-push'; +import crypto from 'crypto'; +import fs from 'fs'; + +import { logger } from '../logger.js'; + +type Secrets = { + nostrKey: Uint8Array; + vapidPrivateKey: string; + vapidPublicKey: string; + hyperKey: Buffer; + i2pPrivateKey?: string; + i2pPublicKey?: string; +}; +type RawJson = Partial<{ + nostrKey: string; + vapidPrivateKey: string; + vapidPublicKey: string; + hyperKey: string; + i2pPrivateKey?: string; + i2pPublicKey?: string; +}>; + +type EventMap = { + /** fires when file is loaded */ + loaded: []; + /** fires when a field is set */ + changed: [keyof Secrets, any]; + /** fires when file is loaded or changed */ + updated: []; + saved: []; +}; + +export default class SecretsManager extends EventEmitter { + log = logger.extend('SecretsManager'); + protected secrets?: Secrets; + path: string; + + constructor(path: string) { + super(); + this.path = path; + } + + get(secret: T): Secrets[T] { + if (!this.secrets) throw new Error('Secrets not loaded'); + return this.secrets[secret]; + } + set(secret: T, value: Secrets[T]) { + if (!this.secrets) throw new Error('Secrets not loaded'); + this.secrets[secret] = value; + + this.emit('changed', secret, value); + this.emit('updated'); + this.write(); + } + + read() { + this.log('Loading secrets'); + + let json: Record = {}; + + try { + json = JSON.parse(fs.readFileSync(this.path, { encoding: 'utf-8' })); + } catch (error) {} + + let changed = false; + + const secrets = {} as Secrets; + + if (!json.nostrKey) { + this.log('Generating new nostr key'); + secrets.nostrKey = generateSecretKey(); + changed = true; + } else secrets.nostrKey = hexToBytes(json.nostrKey); + + if (!json.vapidPrivateKey || !json.vapidPublicKey) { + this.log('Generating new vapid key'); + const keys = webPush.generateVAPIDKeys(); + secrets.vapidPrivateKey = keys.privateKey; + secrets.vapidPublicKey = keys.publicKey; + changed = true; + } else { + secrets.vapidPrivateKey = json.vapidPrivateKey; + secrets.vapidPublicKey = json.vapidPublicKey; + } + + if (!json.hyperKey) { + this.log('Generating new hyper key'); + secrets.hyperKey = crypto.randomBytes(32); + changed = true; + } else secrets.hyperKey = Buffer.from(json.hyperKey, 'hex'); + + secrets.i2pPrivateKey = json.i2pPrivateKey; + secrets.i2pPublicKey = json.i2pPublicKey; + + this.secrets = secrets; + + this.emit('loaded'); + this.emit('updated'); + + if (changed) this.write(); + } + write() { + if (!this.secrets) throw new Error('Secrets not loaded'); + + this.log('Saving'); + + const json: RawJson = { + nostrKey: bytesToHex(this.secrets?.nostrKey), + vapidPrivateKey: this.secrets.vapidPrivateKey, + vapidPublicKey: this.secrets.vapidPublicKey, + hyperKey: this.secrets.hyperKey?.toString('hex'), + i2pPrivateKey: this.secrets.i2pPrivateKey, + i2pPublicKey: this.secrets.i2pPublicKey, + }; + + fs.writeFileSync(this.path, JSON.stringify(json, null, 2), { encoding: 'utf-8' }); + + this.emit('saved'); + } +} diff --git a/src/modules/state/application-state-manager.ts b/src/modules/state/application-state-manager.ts new file mode 100644 index 0000000..c4e2b42 --- /dev/null +++ b/src/modules/state/application-state-manager.ts @@ -0,0 +1,49 @@ +import { MigrationSet } from '@satellite-earth/core/sqlite'; +import { Database } from 'better-sqlite3'; + +import { MutableState } from './mutable-state.js'; + +const migrations = new MigrationSet('application-state'); + +migrations.addScript(1, async (db, log) => { + db.prepare( + ` + CREATE TABLE "application_state" ( + "id" TEXT NOT NULL, + "state" TEXT, + PRIMARY KEY("id") + ); + `, + ).run(); + + log('Created application state table'); +}); + +export default class ApplicationStateManager { + private mutableState = new Map>(); + + database: Database; + constructor(database: Database) { + this.database = database; + } + + async setup() { + await migrations.run(this.database); + } + + async getMutableState(key: string, initialState: T) { + const cached = this.mutableState.get(key); + if (cached) return cached as MutableState; + + const state = new MutableState(this.database, key, initialState); + await state.read(); + this.mutableState.set(key, state); + return state; + } + + async saveAll() { + for (const [key, state] of this.mutableState) { + await state.save(); + } + } +} diff --git a/src/modules/state/mutable-state.ts b/src/modules/state/mutable-state.ts new file mode 100644 index 0000000..78a62ce --- /dev/null +++ b/src/modules/state/mutable-state.ts @@ -0,0 +1,91 @@ +import { EventEmitter } from 'events'; +import { Database } from 'better-sqlite3'; +import _throttle from 'lodash.throttle'; +import { Debugger } from 'debug'; + +import { logger } from '../../logger.js'; + +type EventMap = { + /** fires when file is loaded */ + loaded: [T]; + /** fires when a field is set */ + changed: [T, string, any]; + /** fires when state is loaded or changed */ + updated: [T]; + saved: [T]; +}; + +export class MutableState extends EventEmitter> { + state?: T; + log: Debugger; + + private _proxy?: T; + + /** A Proxy object that will automatically save when mutated */ + get proxy() { + if (!this._proxy) throw new Error('Cant access state before initialized'); + return this._proxy; + } + + key: string; + database: Database; + + constructor(database: Database, key: string, initialState: T) { + super(); + this.state = initialState; + this.key = key; + this.database = database; + this.log = logger.extend(`State:` + key); + this.createProxy(); + } + + private createProxy() { + if (!this.state) return; + + return (this._proxy = new Proxy(this.state, { + get(target, prop, receiver) { + return Reflect.get(target, prop, receiver); + }, + set: (target, p, newValue, receiver) => { + Reflect.set(target, p, newValue, receiver); + this.emit('changed', target as T, String(p), newValue); + this.emit('updated', target as T); + this.throttleSave(); + return newValue; + }, + })); + } + + private throttleSave = _throttle(this.save.bind(this), 30_000); + + async read() { + const row = await this.database + .prepare<[string], { id: string; state: string }>(`SELECT id, state FROM application_state WHERE id=?`) + .get(this.key); + + const state: T | undefined = row ? (JSON.parse(row.state) as T) : undefined; + if (state && this.state) { + Object.assign(this.state, state); + this.log('Loaded'); + } + + if (!this.state) throw new Error(`Missing initial state for ${this.key}`); + + this.createProxy(); + + if (this.state) { + this.emit('loaded', this.state); + this.emit('updated', this.state); + } + } + async save() { + if (!this.state) return; + + await this.database + .prepare<[string, string]>(`INSERT OR REPLACE INTO application_state (id, state) VALUES (?, ?)`) + .run(this.key, JSON.stringify(this.state)); + + this.log('Saved'); + this.emit('saved', this.state); + } +} diff --git a/src/modules/switchboard/switchboard.ts b/src/modules/switchboard/switchboard.ts new file mode 100644 index 0000000..a3f99d4 --- /dev/null +++ b/src/modules/switchboard/switchboard.ts @@ -0,0 +1,92 @@ +import { RawData, WebSocket } from 'ws'; +import { IncomingMessage } from 'http'; +import { logger } from '../../logger.js'; +import OutboundProxyWebSocket from '../network/outbound/websocket.js'; +import { isHexKey } from 'applesauce-core/helpers'; +import App from '../../app/index.js'; + +export default class Switchboard { + private app: App; + private log = logger.extend('Switchboard'); + + constructor(app: App) { + this.app = app; + } + + public handleConnection(downstream: WebSocket, req: IncomingMessage) { + let upstream: WebSocket | undefined; + + const handleMessage = async (message: RawData) => { + try { + // Parse JSON from the raw buffer + const data = JSON.parse(typeof message === 'string' ? message : message.toString('utf-8')); + + if (!Array.isArray(data)) throw new Error('Message is not an array'); + + if (data[0] === 'PROXY' && data[1]) { + let addresses: string[]; + if (isHexKey(data[1])) { + addresses = await this.app.gossip.lookup(data[1]); + } else addresses = [data[1]]; + + if (addresses.length === 0) { + downstream.send(JSON.stringify(['PROXY', 'ERROR', 'Lookup failed'])); + return; + } + + this.app.relay.disconnectSocket(downstream); + downstream.send(JSON.stringify(['PROXY', 'CONNECTING'])); + + let error: Error | undefined = undefined; + for (const address of addresses) { + try { + upstream = new OutboundProxyWebSocket(address); + + // wait for connection + await new Promise((res, rej) => { + upstream?.once('open', () => res()); + upstream?.once('error', (error) => rej(error)); + }); + + this.log(`Proxy connection to ${address}`); + + // clear last error + error = undefined; + + // Forward from client to target relay + downstream.on('message', (message, isBinary) => { + upstream?.send(message, { binary: isBinary }); + }); + + // Forward back from target relay to client + upstream.on('message', (message, isBinary) => { + downstream.send(message, { binary: isBinary }); + }); + + // connect the close events + upstream.on('close', () => downstream.close()); + downstream.on('close', () => upstream?.close()); + + // tell downstream its connected + downstream.send(JSON.stringify(['PROXY', 'CONNECTED'])); + + // Step away from the connection + downstream.off('message', handleMessage); + } catch (err) { + upstream = undefined; + if (err instanceof Error) error = err; + } + } + + // send the error back if we failed to connect to any address + if (error) downstream.send(JSON.stringify(['PROXY', 'ERROR', error.message])); + } + } catch (err) { + this.log('Failed to handle message', err); + } + }; + downstream.on('message', handleMessage); + + this.app.relay.handleConnection(downstream, req); + } +} diff --git a/src/sidecars/hyperdht.ts b/src/sidecars/hyperdht.ts new file mode 100644 index 0000000..42f8f45 --- /dev/null +++ b/src/sidecars/hyperdht.ts @@ -0,0 +1,19 @@ +import HyperDHT from 'hyperdht'; +import { logger } from '../logger.js'; + +const log = logger.extend('HyperDHT'); +let node: HyperDHT | undefined; + +export function getOrCreateNode() { + if (node) return node; + + log('Creating HyperDHT Node'); + return (node = new HyperDHT()); +} + +export function destroyNode() { + if (node) { + node.destroy(); + node = undefined; + } +} diff --git a/src/types/holesail-server.d.ts b/src/types/holesail-server.d.ts new file mode 100644 index 0000000..59f7917 --- /dev/null +++ b/src/types/holesail-server.d.ts @@ -0,0 +1,24 @@ +declare module 'holesail-server' { + import HyperDHT, { KeyPair, Server } from 'hyperdht'; + + type ServeArgs = { + secure?: boolean; + buffSeed?: Buffer; + port?: number; + address?: string; + }; + + export default class HolesailServer { + dht: HyperDHT; + server: Server | null; + seed: Buffer | null; + keyPair: KeyPair | null; + buffer: Buffer | null; + secure?: boolean; + + keyPairGenerator(buffer?: Buffer): KeyPair; + serve(args: ServeArgs, callback?: () => void): void; + destroy(): 0; + getPublicKey(): string; + } +} diff --git a/src/types/hyperdht.d.ts b/src/types/hyperdht.d.ts new file mode 100644 index 0000000..b7e0465 --- /dev/null +++ b/src/types/hyperdht.d.ts @@ -0,0 +1,38 @@ +declare module 'hyperdht' { + import type { Socket } from 'net'; + import type EventEmitter from 'events'; + + class NoiseStreamSocket extends Socket { + remotePublicKey: Buffer; + } + + export class Server extends EventEmitter<{ + listening: []; + connection: [NoiseStreamSocket]; + }> { + address(): { host: string; port: string; publicKey: Buffer } | null; + + listen(keyPair: KeyPair): Promise; + } + + type KeyPair = { + publicKey: Buffer; + secretKey: Buffer; + }; + + export default class HyperDHT { + constructor(opts?: { keyPair: KeyPair; bootstrap?: string[] }); + + createServer( + opts?: { + firewall?: (removePublicKey: Buffer, remoteHandshakePayload: any) => boolean; + }, + onconnection?: (socket: NoiseStreamSocket) => void, + ): Server; + destroy(opts?: { force: boolean }): Promise; + + connect(host: Buffer, opts?: { reusableSocket: boolean }): Socket; + + static keyPair(seed?: Buffer): KeyPair; + } +} diff --git a/src/types/streamx.d.ts b/src/types/streamx.d.ts new file mode 100644 index 0000000..c053c9d --- /dev/null +++ b/src/types/streamx.d.ts @@ -0,0 +1,5 @@ +declare module 'streamx' { + import { Duplex, Stream } from 'stream'; + + export function pipeline(...streams: Stream[]): Duplex; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5d2a2cb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ES2020", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "outDir": "dist", + "skipLibCheck": true, + "declaration": true, + "allowJs": true, + "strict": true, + "sourceMap": true + }, + "include": ["src"] +}