diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..80531d6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist +data +nostrudel +node_modules diff --git a/nostrudel b/nostrudel index 5b2113b..e623c7b 160000 --- a/nostrudel +++ b/nostrudel @@ -1 +1 @@ -Subproject commit 5b2113b3b9c5d79990624d11dcd282cbc9266511 +Subproject commit e623c7b4aaf15ea4acdbbe61c5760b2bb6bec3f6 diff --git a/package.json b/package.json index 4dbc308..806ddce 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "node .", "dev": "nodemon --loader @swc-node/register/esm src/index.ts", "build": "tsc", - "format": "prettier -w . --ignore-path .gitignore" + "format": "prettier -w ." }, "files": [ "dist", @@ -24,12 +24,14 @@ "author": "hzrd149", "license": "MIT", "dependencies": { - "@diva.exchange/i2p-sam": "^5.4.1", - "@noble/hashes": "^1.7.0", + "@diva.exchange/i2p-sam": "^5.4.2", + "@noble/hashes": "^1.7.1", "@satellite-earth/core": "^0.5.0", - "applesauce-core": "^0.10.0", - "applesauce-signer": "^0.10.0", - "better-sqlite3": "^11.7.2", + "applesauce-core": "next", + "applesauce-factory": "0.0.0-next-20250206174509", + "applesauce-loaders": "^0.10.0", + "applesauce-signers": "next", + "better-sqlite3": "^11.8.1", "blossom-client-sdk": "^2.1.1", "cors": "^2.8.5", "dayjs": "^1.11.13", @@ -49,23 +51,25 @@ "nostr-tools": "^2.10.4", "pac-proxy-agent": "^7.1.0", "process-streams": "^1.0.3", - "streamx": "^2.21.1", + "rx-nostr": "^3.5.0", + "rxjs": "^7.8.1", + "streamx": "^2.22.0", "unique-names-generator": "^4.7.1", "web-push": "^3.6.7", "ws": "^8.18.0" }, "devDependencies": { - "@changesets/cli": "^2.27.11", + "@changesets/cli": "^2.27.12", "@swc-node/register": "^1.10.9", - "@swc/core": "^1.10.7", + "@swc/core": "^1.10.14", "@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/node": "^22.13.1", "@types/web-push": "^3.6.4", - "@types/ws": "^8.5.13", + "@types/ws": "^8.5.14", "nodemon": "^3.1.9", "prettier": "^3.4.2", "typescript": "^5.7.3" @@ -76,5 +80,6 @@ ], "exec": "node", "signal": "SIGTERM" - } + }, + "packageManager": "pnpm@9.14.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39f8fda..6f4cd50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,23 +9,29 @@ importers: .: dependencies: '@diva.exchange/i2p-sam': - specifier: ^5.4.1 - version: 5.4.1 + specifier: ^5.4.2 + version: 5.4.2 '@noble/hashes': - specifier: ^1.7.0 - version: 1.7.0 + specifier: ^1.7.1 + version: 1.7.1 '@satellite-earth/core': specifier: ^0.5.0 version: 0.5.0(typescript@5.7.3) applesauce-core: + specifier: next + version: 0.0.0-next-20250206174509(typescript@5.7.3) + applesauce-factory: + specifier: 0.0.0-next-20250206174509 + version: 0.0.0-next-20250206174509(typescript@5.7.3) + applesauce-loaders: 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) + applesauce-signers: + specifier: next + version: 0.0.0-next-20250206174509(typescript@5.7.3) better-sqlite3: - specifier: ^11.7.2 - version: 11.7.2 + specifier: ^11.8.1 + version: 11.8.1 blossom-client-sdk: specifier: ^2.1.1 version: 2.1.1 @@ -83,9 +89,15 @@ importers: process-streams: specifier: ^1.0.3 version: 1.0.3 + rx-nostr: + specifier: ^3.5.0 + version: 3.5.0 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 streamx: - specifier: ^2.21.1 - version: 2.21.1 + specifier: ^2.22.0 + version: 2.22.0 unique-names-generator: specifier: ^4.7.1 version: 4.7.1 @@ -97,14 +109,14 @@ importers: version: 8.18.0 devDependencies: '@changesets/cli': - specifier: ^2.27.11 - version: 2.27.11 + specifier: ^2.27.12 + version: 2.27.12 '@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) + version: 1.10.9(@swc/core@1.10.14)(@swc/types@0.1.17)(typescript@5.7.3) '@swc/core': - specifier: ^1.10.7 - version: 1.10.7 + specifier: ^1.10.14 + version: 1.10.14 '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.12 @@ -121,14 +133,14 @@ importers: specifier: ^4.1.9 version: 4.1.9 '@types/node': - specifier: ^22.10.6 - version: 22.10.6 + specifier: ^22.13.1 + version: 22.13.1 '@types/web-push': specifier: ^3.6.4 version: 3.6.4 '@types/ws': - specifier: ^8.5.13 - version: 8.5.13 + specifier: ^8.5.14 + version: 8.5.14 nodemon: specifier: ^3.1.9 version: 3.1.9 @@ -141,18 +153,24 @@ importers: packages: - '@babel/runtime@7.26.0': - resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + '@babel/runtime@7.26.7': + resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==} engines: {node: '>=6.9.0'} - '@cashu/cashu-ts@2.1.0': - resolution: {integrity: sha512-qFfFz1dx9keJxumjk5FyTvI1j0Yp/P5LXDy0cGO4Xlp3WYKOI1nykNOTPd+bTY9vSkvIM+xuXRer9BtQxqHtwA==} + '@cashu/cashu-ts@2.0.0-rc1': + resolution: {integrity: sha512-39459l7x/fUMEgOsCdGLLl6rMekO4nbv+wEuavmyElh8hgN8t66wcb29AJvdFTb6K3lPACKF2rs/jAlPYrN7Ng==} + + '@cashu/cashu-ts@2.2.0': + resolution: {integrity: sha512-7b6pGyjjpm3uAJvmOL+ztpRxqp1qnmzGpydp+Pu30pOjxj93EhejPTJVrZMDJ0P35y6u5+5jIjHF4k0fpovvmg==} + + '@cashu/crypto@0.2.7': + resolution: {integrity: sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==} '@cashu/crypto@0.3.4': resolution: {integrity: sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==} - '@changesets/apply-release-plan@7.0.7': - resolution: {integrity: sha512-qnPOcmmmnD0MfMg9DjU1/onORFyRpDXkMMl2IJg9mECY6RnxL3wN0TCCc92b2sXt1jt8DgjAUUsZYGUGTdYIXA==} + '@changesets/apply-release-plan@7.0.8': + resolution: {integrity: sha512-qjMUj4DYQ1Z6qHawsn7S71SujrExJ+nceyKKyI9iB+M5p9lCL55afuEd6uLBPRpLGWQwkwvWegDHtwHJb1UjpA==} '@changesets/assemble-release-plan@6.0.5': resolution: {integrity: sha512-IgvBWLNKZd6k4t72MBTBK3nkygi0j3t3zdC1zrfusYo0KpdsvnDjrMM9vPnTCLCMlfNs55jRL4gIMybxa64FCQ==} @@ -160,8 +178,8 @@ packages: '@changesets/changelog-git@0.2.0': resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} - '@changesets/cli@2.27.11': - resolution: {integrity: sha512-1QislpE+nvJgSZZo9+Lj3Lno5pKBgN46dAV8IVxKJy9wX8AOrs9nn5pYVZuDpoxWJJCALmbfOsHkyxujgetQSg==} + '@changesets/cli@2.27.12': + resolution: {integrity: sha512-9o3fOfHYOvBnyEn0mcahB7wzaA3P4bGJf8PNqGit5PKaMEFdsRixik+txkrJWd2VX+O6wRFXpxQL8j/1ANKE9g==} hasBin: true '@changesets/config@3.0.5': @@ -206,8 +224,8 @@ packages: '@changesets/write@0.3.2': resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==} - '@diva.exchange/i2p-sam@5.4.1': - resolution: {integrity: sha512-/X2ezrRSxAxrEmk+z9c1fIurb1XjUyEQkvdgUdQezdOhC48XPj4Ed4LOpODGacFvOhwSwX1RAy7xV39PA2ki0w==} + '@diva.exchange/i2p-sam@5.4.2': + resolution: {integrity: sha512-uaYZlSHdqxOQhtmPzO8g/Hzdp2Vd5rQ5wZMDPYeLUr2FOMYc0pwRpThyc6GfPQ8d0yTAbqemzu/uEGARxJ/gtA==} engines: {node: '>=16.0.0'} '@emnapi/core@1.3.1': @@ -243,8 +261,8 @@ packages: '@noble/curves@1.2.0': resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} - '@noble/curves@1.8.0': - resolution: {integrity: sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==} + '@noble/curves@1.8.1': + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} engines: {node: ^14.21.3 || >=16} '@noble/hashes@1.3.1': @@ -255,8 +273,8 @@ packages: resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} engines: {node: '>= 16'} - '@noble/hashes@1.7.0': - resolution: {integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==} + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} engines: {node: ^14.21.3 || >=16} '@noble/secp256k1@1.7.1': @@ -338,20 +356,20 @@ packages: '@scure/base@1.1.1': resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} - '@scure/base@1.2.1': - resolution: {integrity: sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ==} + '@scure/base@1.2.4': + resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} '@scure/bip32@1.3.1': resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} - '@scure/bip32@1.6.1': - resolution: {integrity: sha512-jSO+5Ud1E588Y+LFo8TaB8JVPNAZw/lGGao+1SepHDeTs2dFLurdNIAgUuDlwezqEjRjElkCJajVrtrZaBxvaQ==} + '@scure/bip32@1.6.2': + resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} '@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==} + '@scure/bip39@1.5.4': + resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} '@swc-node/core@1.13.3': resolution: {integrity: sha512-OGsvXIid2Go21kiNqeTIn79jcaX4l0G93X2rAnas4LFoDyA9wAwVK7xZdm+QsKoMn5Mus2yFLCc4OtX2dD/PWA==} @@ -369,68 +387,68 @@ packages: '@swc-node/sourcemap-support@0.5.1': resolution: {integrity: sha512-JxIvIo/Hrpv0JCHSyRpetAdQ6lB27oFYhv0PKCNf1g2gUXOjpeR1exrXccRxLMuAV5WAmGFBwRnNOJqN38+qtg==} - '@swc/core-darwin-arm64@1.10.7': - resolution: {integrity: sha512-SI0OFg987P6hcyT0Dbng3YRISPS9uhLX1dzW4qRrfqQdb0i75lPJ2YWe9CN47HBazrIA5COuTzrD2Dc0TcVsSQ==} + '@swc/core-darwin-arm64@1.10.14': + resolution: {integrity: sha512-Dh4VyrhDDb05tdRmqJ/MucOPMTnrB4pRJol18HVyLlqu1HOT5EzonUniNTCdQbUXjgdv5UVJSTE1lYTzrp+myA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.10.7': - resolution: {integrity: sha512-RFIAmWVicD/l3RzxgHW0R/G1ya/6nyMspE2cAeDcTbjHi0I5qgdhBWd6ieXOaqwEwiCd0Mot1g2VZrLGoBLsjQ==} + '@swc/core-darwin-x64@1.10.14': + resolution: {integrity: sha512-KpzotL/I0O12RE3tF8NmQErINv0cQe/0mnN/Q50ESFzB5kU6bLgp2HMnnwDTm/XEZZRJCNe0oc9WJ5rKbAJFRQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.10.7': - resolution: {integrity: sha512-QP8vz7yELWfop5mM5foN6KkLylVO7ZUgWSF2cA0owwIaziactB2hCPZY5QU690coJouk9KmdFsPWDnaCFUP8tg==} + '@swc/core-linux-arm-gnueabihf@1.10.14': + resolution: {integrity: sha512-20yRXZjMJVz1wp1TcscKiGTVXistG+saIaxOmxSNQia1Qun3hSWLL+u6+5kXbfYGr7R2N6kqSwtZbIfJI25r9Q==} 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==} + '@swc/core-linux-arm64-gnu@1.10.14': + resolution: {integrity: sha512-Gy7cGrNkiMfPxQyLGxdgXPwyWzNzbHuWycJFcoKBihxZKZIW8hkPBttkGivuLC+0qOgsV2/U+S7tlvAju7FtmQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.10.7': - resolution: {integrity: sha512-gp5Un3EbeSThBIh6oac5ZArV/CsSmTKj5jNuuUAuEsML3VF9vqPO+25VuxCvsRf/z3py+xOWRaN2HY/rjMeZog==} + '@swc/core-linux-arm64-musl@1.10.14': + resolution: {integrity: sha512-+oYVqJvFw62InZ8PIy1rBACJPC2WTe4vbVb9kM1jJj2D7dKLm9acnnYIVIDsM5Wo7Uab8RvPHXVbs19IBurzuw==} 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==} + '@swc/core-linux-x64-gnu@1.10.14': + resolution: {integrity: sha512-OmEbVEKQFLQVHwo4EJl9osmlulURy46k232Opfpn/1ji0t2KcNCci3POsnfMuoZjLkGJv8vGNJdPQxX+CP+wSA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.10.7': - resolution: {integrity: sha512-XeDoURdWt/ybYmXLCEE8aSiTOzEn0o3Dx5l9hgt0IZEmTts7HgHHVeRgzGXbR4yDo0MfRuX5nE1dYpTmCz0uyA==} + '@swc/core-linux-x64-musl@1.10.14': + resolution: {integrity: sha512-OZW+Icm8DMPqHbhdxplkuG8qrNnPk5i7xJOZWYi1y5bTjgGFI4nEzrsmmeHKMdQTaWwsFrm3uK1rlyQ48MmXmg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.10.7': - resolution: {integrity: sha512-nYAbi/uLS+CU0wFtBx8TquJw2uIMKBnl04LBmiVoFrsIhqSl+0MklaA9FVMGA35NcxSJfcm92Prl2W2LfSnTqQ==} + '@swc/core-win32-arm64-msvc@1.10.14': + resolution: {integrity: sha512-sTvc+xrDQXy3HXZFtTEClY35Efvuc3D+busYm0+rb1+Thau4HLRY9WP+sOKeGwH9/16rzfzYEqD7Ds8A9ykrHw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.10.7': - resolution: {integrity: sha512-+aGAbsDsIxeLxw0IzyQLtvtAcI1ctlXVvVcXZMNXIXtTURM876yNrufRo4ngoXB3jnb1MLjIIjgXfFs/eZTUSw==} + '@swc/core-win32-ia32-msvc@1.10.14': + resolution: {integrity: sha512-j2iQ4y9GWTKtES5eMU0sDsFdYni7IxME7ejFej25Tv3Fq4B+U9tgtYWlJwh1858nIWDXelHiKcSh/UICAyVMdQ==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.10.7': - resolution: {integrity: sha512-TBf4clpDBjF/UUnkKrT0/th76/zwvudk5wwobiTFqDywMApHip5O0VpBgZ+4raY2TM8k5+ujoy7bfHb22zu17Q==} + '@swc/core-win32-x64-msvc@1.10.14': + resolution: {integrity: sha512-TYtWkUSMkjs0jGPeWdtWbex4B+DlQZmN/ySVLiPI+EltYCLEXsFMkVFq6aWn48dqFHggFK0UYfvDrJUR2c3Qxg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.10.7': - resolution: {integrity: sha512-py91kjI1jV5D5W/Q+PurBdGsdU5TFbrzamP7zSCqLdMcHkKi3rQEM5jkQcZr0MXXSJTaayLxS3MWYTBIkzPDrg==} + '@swc/core@1.10.14': + resolution: {integrity: sha512-WSrnE6JRnH20ZYjOOgSS4aOaPv9gxlkI2KRkN24kagbZnPZMnN8bZZyzw1rrLvwgpuRGv17Uz+hflosbR+SP6w==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '*' @@ -465,35 +483,38 @@ packages: '@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/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@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/lodash@4.17.15': + resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/ms@0.7.34': - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} '@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/node@22.13.1': + resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} '@types/qs@6.9.18': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} @@ -507,11 +528,14 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/web-push@3.6.4': resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} - '@types/ws@8.5.13': - resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + '@types/ws@8.5.14': + resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} '@zxing/text-encoding@0.9.0': resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} @@ -536,14 +560,23 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + applesauce-content@0.0.0-next-20250206174509: + resolution: {integrity: sha512-+LXzDZdANYwZGKnpc0CFoEgF0KgKuOcYTEUrf0X79ezKejCEGwvn1UvNce2c8MIWfwhaHBTJq8kQO2gKaODQ9A==} + + applesauce-core@0.0.0-next-20250206174509: + resolution: {integrity: sha512-+5G8y4WwjJVvL2dfn56w0mYhm4GjL0hoMk4ZJBM7FUBcpuq40UoBsslheIFaLYuzzZHvd28EtJycbyhBKM86pQ==} + 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-factory@0.0.0-next-20250206174509: + resolution: {integrity: sha512-E5OTJLYSh5vejQmuHZOqxLajJwPiQOFnlkhKQSBMeA2RyxClOuuDIm8Qj3PXlC1T9kAqIqL/CsieUcJ1jhUWdQ==} - applesauce-signer@0.10.0: - resolution: {integrity: sha512-2Cn2ZUxk47cBJBFoUl9DB37mjgg8/8GwTAG7csPqtooJ4nUh0ylO4Gh1Mr/401Lc8tjZJvpVBySWpCeXrnG1rQ==} + applesauce-loaders@0.10.0: + resolution: {integrity: sha512-cEPV8nSJKfmvVVRaSDc/3TRaDE8s0CSFJGzEvSYBTjokZBvQz2QZrlQXbwTvYD90XrWFH1LJ8dLDDnEUehaOow==} + + applesauce-signers@0.0.0-next-20250206174509: + resolution: {integrity: sha512-SfV1nG9YhF9aKS1c8uUg2ciNmetBbUgmV58l8/nnulHatGDDNtX+d34XGKxXXGRHUHk7bYtFKeeYSBNw4Wm94A==} argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -572,6 +605,9 @@ packages: b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -581,8 +617,8 @@ packages: bare-dns@1.0.5: resolution: {integrity: sha512-Hqb9dpUe4M7y9N6oRV0XhHzUkUliyUE7qw/q3cmn0PF06fdBXooy45fvKGeYUUv6OCvXyLKfi7b7QyZS7r/YoQ==} - bare-dns@2.0.2: - resolution: {integrity: sha512-xEry4yLFAetelqgYGRMC9/82b/jq9Q9l2a+mX+VouWlZeORIoLWV4OXMUilRy4WfdgGTd8QMMqPlmbE5rX3hnw==} + bare-dns@2.0.4: + resolution: {integrity: sha512-ZZ6Elwi9in2nf9UyPMmm9A0i0G4ndh3G3vcvK3dMKn74adyv7sh6GO7wfu/5SEKLfVmWcj85v0rod74DU8yjcQ==} engines: {bare: '>=1.7.0'} bare-events@2.5.4: @@ -597,12 +633,12 @@ packages: 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==} + bare-pipe@4.0.3: + resolution: {integrity: sha512-vy5dK8AEvxBJxZyFr+2RDf8kOsVD6gDlP81UjvmDsfUFoKrA21mDLrUopQjmr3AJbMKbuZOinXfItURaZqWa4w==} engines: {bare: '>=1.7.0'} - bare-stream@2.6.4: - resolution: {integrity: sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA==} + bare-stream@2.6.5: + resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} peerDependencies: bare-buffer: '*' bare-events: '*' @@ -615,8 +651,8 @@ packages: 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==} + bare-tcp@2.0.3: + resolution: {integrity: sha512-Sy0EVAZFKLjcbS+TxEPV5nhtSNsw6ErFRTJBQoRn62kRsEcaKkPNcwx+N4fJy+A1SovChby4rN8AvDhoyI/rEg==} engines: {bare: '>=1.7.0'} base64-js@1.5.1: @@ -630,8 +666,8 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - better-sqlite3@11.7.2: - resolution: {integrity: sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==} + better-sqlite3@11.8.1: + resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -713,6 +749,9 @@ packages: resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} engines: {node: '>= 0.4'} + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -796,6 +835,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -820,6 +862,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -832,8 +878,11 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} - dht-rpc@6.16.0: - resolution: {integrity: sha512-2Tf1E8eWQp+fHje9/n6UdVVky1C80LoGa2VPUorJ8qL5fu8xhvQQ9YiujjgbVIdwSka0oP3VFv5daAx8u6YRPg==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dht-rpc@6.16.2: + resolution: {integrity: sha512-1CIoo0mCRNI8r0vDozV/exQ1mg2Y04zBaQ34WBWzNjGhZbaABPmWI0EZSpbAGdZ3+pBbGXjk1aQkadF9S7RzeQ==} dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -879,13 +928,17 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-object-atoms@1.1.0: - resolution: {integrity: sha512-Ujz8Al/KfOVR7fkaghAB1WvnLsdYxHDWmfoi2vlA2jZWRg31XhIC1a4B+/I24muD8iSbHxJ1JkrfqmWb65P/Mw==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -919,6 +972,9 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -940,8 +996,8 @@ packages: resolution: {integrity: sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==} hasBin: true - fastq@1.18.0: - resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + fastq@1.19.0: + resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -971,8 +1027,9 @@ packages: debug: optional: true - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + for-each@0.3.4: + resolution: {integrity: sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==} + engines: {node: '>= 0.4'} forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -1169,6 +1226,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1207,8 +1268,8 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} - kademlia-routing-table@1.0.3: - resolution: {integrity: sha512-ag1nLPaCSxbLnG6zeTBoGkdm+JSUi2JHMiNynHBoRG8se2w9yfyzepF3oX1B52HOEldtbuxTde5NxbNUtnwGqA==} + kademlia-routing-table@1.0.6: + resolution: {integrity: sha512-Ve6jwIlUCYvUzBnXnzVRHDZCFgXURW9gmF3r7n05kZs/2rNbLHXwGdcq0qIaSwdmJCvtosgR4JensnVU65hzNQ==} light-bolt11-decoder@3.2.0: resolution: {integrity: sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==} @@ -1226,6 +1287,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lowdb@7.0.1: resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} engines: {node: '>=18'} @@ -1234,6 +1298,21 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -1249,6 +1328,69 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.4: + resolution: {integrity: sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1314,8 +1456,8 @@ packages: engines: {node: ^18 || >=20} hasBin: true - napi-build-utils@1.0.2: - resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} nat-sampler@1.0.1: resolution: {integrity: sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==} @@ -1328,8 +1470,8 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - node-abi@3.72.0: - resolution: {integrity: sha512-a28z9xAQXvDh40lVCknWCP5zYTZt6Av8HZqZ63U5OWxTcP20e3oOIy8yHkYfctQM2adR8ru1GxWCkS0gS+WYKA==} + node-abi@3.74.0: + resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} engines: {node: '>=10'} node-fetch@2.7.0: @@ -1368,6 +1510,9 @@ packages: typescript: optional: true + nostr-typedef@0.9.0: + resolution: {integrity: sha512-nLTzhlYcRnLQGUJ5YfvGAUDyGFHjGH6Qozltl/wV3UXelmiUwjrwI8IIxQNkbgVMv+zmbFi/m1xKHxIvVfG09w==} + nostr-wasm@0.1.0: resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} @@ -1375,8 +1520,8 @@ packages: 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==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} on-finished@2.4.1: @@ -1424,8 +1569,8 @@ packages: resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} engines: {node: '>= 14'} - package-manager-detector@0.2.8: - resolution: {integrity: sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==} + package-manager-detector@0.2.9: + resolution: {integrity: sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==} parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -1465,8 +1610,8 @@ packages: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} - prebuild-install@7.1.2: - resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} hasBin: true @@ -1540,6 +1685,15 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1554,6 +1708,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rx-nostr@3.5.0: + resolution: {integrity: sha512-SFk/WTYKW1GAecyxLKNQlkdedrFfGDeT8nz8wTFriIqd2I6jSV5lm7jBkStcnB4ncDK7GOf4QoLcZfxA+OqaQw==} + rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -1573,8 +1730,8 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true @@ -1692,8 +1849,8 @@ packages: resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} engines: {node: '>=18'} - streamx@2.21.1: - resolution: {integrity: sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==} + streamx@2.22.0: + resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} @@ -1763,6 +1920,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1778,8 +1938,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - udx-native@1.17.2: - resolution: {integrity: sha512-QAcwyS2ORfZIpdzHQqCmUMTm47ZXNA3bero4WWGP69fjzD6NwBnG4Bolsi3nF1vzrlH6dEP7SkqkGVb7E2nu8A==} + udx-native@1.17.3: + resolution: {integrity: sha512-/7k5ZCZa+uTvdB7YG54EgJzCyp1q7s2SonKwxCv8vVwulm9T5CGevsONIgLs7MG5eC8/QdBuMVgjCPHoiAhZ/g==} undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} @@ -1787,10 +1947,25 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-names-generator@4.7.1: resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==} engines: {node: '>=8'} + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -1816,6 +1991,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + web-encoding@1.1.5: resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} @@ -1871,29 +2052,47 @@ packages: z32@1.1.0: resolution: {integrity: sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: - '@babel/runtime@7.26.0': + '@babel/runtime@7.26.7': dependencies: regenerator-runtime: 0.14.1 - '@cashu/cashu-ts@2.1.0': + '@cashu/cashu-ts@2.0.0-rc1': + dependencies: + '@cashu/crypto': 0.2.7 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + buffer: 6.0.3 + + '@cashu/cashu-ts@2.2.0': dependencies: '@cashu/crypto': 0.3.4 - '@noble/curves': 1.8.0 - '@noble/hashes': 1.7.0 - '@scure/bip32': 1.6.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + buffer: 6.0.3 + + '@cashu/crypto@0.2.7': + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 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 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 buffer: 6.0.3 - '@changesets/apply-release-plan@7.0.7': + '@changesets/apply-release-plan@7.0.8': dependencies: '@changesets/config': 3.0.5 '@changesets/get-version-range-type': 0.4.0 @@ -1907,7 +2106,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.1 '@changesets/assemble-release-plan@6.0.5': dependencies: @@ -1916,15 +2115,15 @@ snapshots: '@changesets/should-skip-package': 0.1.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - semver: 7.6.3 + semver: 7.7.1 '@changesets/changelog-git@0.2.0': dependencies: '@changesets/types': 6.0.0 - '@changesets/cli@2.27.11': + '@changesets/cli@2.27.12': dependencies: - '@changesets/apply-release-plan': 7.0.7 + '@changesets/apply-release-plan': 7.0.8 '@changesets/assemble-release-plan': 6.0.5 '@changesets/changelog-git': 0.2.0 '@changesets/config': 3.0.5 @@ -1946,10 +2145,10 @@ snapshots: fs-extra: 7.0.1 mri: 1.2.0 p-limit: 2.3.0 - package-manager-detector: 0.2.8 + package-manager-detector: 0.2.9 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.1 spawndamnit: 3.0.1 term-size: 2.2.1 @@ -1972,7 +2171,7 @@ snapshots: '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.6.3 + semver: 7.7.1 '@changesets/get-release-plan@4.0.6': dependencies: @@ -2035,7 +2234,7 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 - '@diva.exchange/i2p-sam@5.4.1': + '@diva.exchange/i2p-sam@5.4.2': dependencies: nanoid: 5.0.9 rfc4648: 1.5.4 @@ -2068,7 +2267,7 @@ snapshots: noise-handshake: 3.1.0 sodium-secretstream: 1.1.1 sodium-universal: 4.0.1 - streamx: 2.21.1 + streamx: 2.22.0 timeout-refresh: 2.0.1 unslab: 1.3.0 transitivePeerDependencies: @@ -2076,14 +2275,14 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.26.0 + '@babel/runtime': 7.26.7 '@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 + '@babel/runtime': 7.26.7 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -2107,15 +2306,15 @@ snapshots: dependencies: '@noble/hashes': 1.3.2 - '@noble/curves@1.8.0': + '@noble/curves@1.8.1': dependencies: - '@noble/hashes': 1.7.0 + '@noble/hashes': 1.7.1 '@noble/hashes@1.3.1': {} '@noble/hashes@1.3.2': {} - '@noble/hashes@1.7.0': {} + '@noble/hashes@1.7.1': {} '@noble/secp256k1@1.7.1': {} @@ -2129,7 +2328,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.18.0 + fastq: 1.19.0 '@oxc-resolver/binding-darwin-arm64@1.12.0': optional: true @@ -2170,7 +2369,7 @@ snapshots: '@satellite-earth/core@0.5.0(typescript@5.7.3)': dependencies: - better-sqlite3: 11.7.2 + better-sqlite3: 11.8.1 blossom-client-sdk: 0.9.1 blossom-server-sdk: 0.4.0 cors: 2.8.5 @@ -2193,7 +2392,7 @@ snapshots: '@scure/base@1.1.1': {} - '@scure/base@1.2.1': {} + '@scure/base@1.2.4': {} '@scure/bip32@1.3.1': dependencies: @@ -2201,32 +2400,32 @@ snapshots: '@noble/hashes': 1.3.1 '@scure/base': 1.1.1 - '@scure/bip32@1.6.1': + '@scure/bip32@1.6.2': dependencies: - '@noble/curves': 1.8.0 - '@noble/hashes': 1.7.0 - '@scure/base': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.4 '@scure/bip39@1.2.1': dependencies: '@noble/hashes': 1.3.1 '@scure/base': 1.1.1 - '@scure/bip39@1.5.1': + '@scure/bip39@1.5.4': dependencies: - '@noble/hashes': 1.7.0 - '@scure/base': 1.2.1 + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.4 - '@swc-node/core@1.13.3(@swc/core@1.10.7)(@swc/types@0.1.17)': + '@swc-node/core@1.13.3(@swc/core@1.10.14)(@swc/types@0.1.17)': dependencies: - '@swc/core': 1.10.7 + '@swc/core': 1.10.14 '@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)': + '@swc-node/register@1.10.9(@swc/core@1.10.14)(@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/core': 1.13.3(@swc/core@1.10.14)(@swc/types@0.1.17) '@swc-node/sourcemap-support': 0.5.1 - '@swc/core': 1.10.7 + '@swc/core': 1.10.14 colorette: 2.0.20 debug: 4.4.0(supports-color@5.5.0) oxc-resolver: 1.12.0 @@ -2242,51 +2441,51 @@ snapshots: source-map-support: 0.5.21 tslib: 2.8.1 - '@swc/core-darwin-arm64@1.10.7': + '@swc/core-darwin-arm64@1.10.14': optional: true - '@swc/core-darwin-x64@1.10.7': + '@swc/core-darwin-x64@1.10.14': optional: true - '@swc/core-linux-arm-gnueabihf@1.10.7': + '@swc/core-linux-arm-gnueabihf@1.10.14': optional: true - '@swc/core-linux-arm64-gnu@1.10.7': + '@swc/core-linux-arm64-gnu@1.10.14': optional: true - '@swc/core-linux-arm64-musl@1.10.7': + '@swc/core-linux-arm64-musl@1.10.14': optional: true - '@swc/core-linux-x64-gnu@1.10.7': + '@swc/core-linux-x64-gnu@1.10.14': optional: true - '@swc/core-linux-x64-musl@1.10.7': + '@swc/core-linux-x64-musl@1.10.14': optional: true - '@swc/core-win32-arm64-msvc@1.10.7': + '@swc/core-win32-arm64-msvc@1.10.14': optional: true - '@swc/core-win32-ia32-msvc@1.10.7': + '@swc/core-win32-ia32-msvc@1.10.14': optional: true - '@swc/core-win32-x64-msvc@1.10.7': + '@swc/core-win32-x64-msvc@1.10.14': optional: true - '@swc/core@1.10.7': + '@swc/core@1.10.14': 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/core-darwin-arm64': 1.10.14 + '@swc/core-darwin-x64': 1.10.14 + '@swc/core-linux-arm-gnueabihf': 1.10.14 + '@swc/core-linux-arm64-gnu': 1.10.14 + '@swc/core-linux-arm64-musl': 1.10.14 + '@swc/core-linux-x64-gnu': 1.10.14 + '@swc/core-linux-x64-musl': 1.10.14 + '@swc/core-win32-arm64-msvc': 1.10.14 + '@swc/core-win32-ia32-msvc': 1.10.14 + '@swc/core-win32-x64-msvc': 1.10.14 '@swc/counter@0.1.3': {} @@ -2303,30 +2502,28 @@ snapshots: '@types/better-sqlite3@7.6.12': dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/connect@3.4.38': dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/cors@2.8.17': dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/debug@4.1.12': dependencies: - '@types/ms': 0.7.34 - - '@types/dom-serial@1.0.6': {} + '@types/ms': 2.1.0 '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/qs': 6.9.18 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -2338,21 +2535,29 @@ snapshots: '@types/qs': 6.9.18 '@types/serve-static': 1.15.7 + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/http-errors@2.0.4': {} '@types/lodash.throttle@4.1.9': dependencies: - '@types/lodash': 4.17.14 + '@types/lodash': 4.17.15 - '@types/lodash@4.17.14': {} + '@types/lodash@4.17.15': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 '@types/mime@1.3.5': {} - '@types/ms@0.7.34': {} + '@types/ms@2.1.0': {} '@types/node@12.20.55': {} - '@types/node@22.10.6': + '@types/node@22.13.1': dependencies: undici-types: 6.20.0 @@ -2363,21 +2568,23 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@types/send': 0.17.4 + '@types/unist@3.0.3': {} + '@types/web-push@3.6.4': dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 - '@types/ws@8.5.13': + '@types/ws@8.5.14': dependencies: - '@types/node': 22.10.6 + '@types/node': 22.13.1 '@zxing/text-encoding@0.9.0': optional: true @@ -2398,9 +2605,26 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - applesauce-core@0.10.0(typescript@5.7.3): + applesauce-content@0.0.0-next-20250206174509(typescript@5.7.3): dependencies: - '@scure/base': 1.2.1 + '@cashu/cashu-ts': 2.0.0-rc1 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3) + mdast-util-find-and-replace: 3.0.2 + nostr-tools: 2.10.4(typescript@5.7.3) + remark: 15.0.1 + remark-parse: 11.0.0 + unified: 11.0.5 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + applesauce-core@0.0.0-next-20250206174509(typescript@5.7.3): + dependencies: + '@scure/base': 1.2.4 debug: 4.4.0(supports-color@5.5.0) fast-deep-equal: 3.1.3 hash-sum: 2.0.0 @@ -2412,9 +2636,13 @@ snapshots: - supports-color - typescript - applesauce-net@0.10.0(typescript@5.7.3): + applesauce-core@0.10.0(typescript@5.7.3): dependencies: - applesauce-core: 0.10.0(typescript@5.7.3) + '@scure/base': 1.2.4 + 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 @@ -2422,14 +2650,33 @@ snapshots: - supports-color - typescript - applesauce-signer@0.10.0(typescript@5.7.3): + applesauce-factory@0.0.0-next-20250206174509(typescript@5.7.3): + dependencies: + applesauce-content: 0.0.0-next-20250206174509(typescript@5.7.3) + applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3) + nanoid: 5.0.9 + nostr-tools: 2.10.4(typescript@5.7.3) + transitivePeerDependencies: + - supports-color + - typescript + + applesauce-loaders@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) + nanoid: 5.0.9 + nostr-tools: 2.10.4(typescript@5.7.3) + rx-nostr: 3.5.0 + rxjs: 7.8.1 + transitivePeerDependencies: + - supports-color + - typescript + + applesauce-signers@0.0.0-next-20250206174509(typescript@5.7.3): + dependencies: + '@noble/hashes': 1.7.1 + '@noble/secp256k1': 1.7.1 + '@scure/base': 1.2.4 + applesauce-core: 0.0.0-next-20250206174509(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) @@ -2464,16 +2711,18 @@ snapshots: b4a@1.6.7: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} bare-dgram@1.0.1: dependencies: bare-events: 2.5.4 - udx-native: 1.17.2 + udx-native: 1.17.3 bare-dns@1.0.5: {} - bare-dns@2.0.2: {} + bare-dns@2.0.4: {} bare-events@2.5.4: {} @@ -2481,7 +2730,7 @@ snapshots: dependencies: bare-events: 2.5.4 bare-pipe: 3.3.8 - bare-stream: 2.6.4(bare-events@2.5.4) + bare-stream: 2.6.5(bare-events@2.5.4) bare-tcp: 1.9.1 transitivePeerDependencies: - bare-buffer @@ -2489,29 +2738,29 @@ snapshots: 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 + bare-pipe: 4.0.3 + bare-stream: 2.6.5(bare-events@2.5.4) + bare-tcp: 2.0.3 transitivePeerDependencies: - bare-buffer bare-pipe@3.3.8: dependencies: bare-events: 2.5.4 - bare-stream: 2.6.4(bare-events@2.5.4) + bare-stream: 2.6.5(bare-events@2.5.4) transitivePeerDependencies: - bare-buffer - bare-pipe@4.0.2: + bare-pipe@4.0.3: dependencies: bare-events: 2.5.4 - bare-stream: 2.6.4(bare-events@2.5.4) + bare-stream: 2.6.5(bare-events@2.5.4) transitivePeerDependencies: - bare-buffer - bare-stream@2.6.4(bare-events@2.5.4): + bare-stream@2.6.5(bare-events@2.5.4): dependencies: - streamx: 2.21.1 + streamx: 2.22.0 optionalDependencies: bare-events: 2.5.4 @@ -2519,15 +2768,15 @@ snapshots: dependencies: bare-dns: 1.0.5 bare-events: 2.5.4 - bare-stream: 2.6.4(bare-events@2.5.4) + bare-stream: 2.6.5(bare-events@2.5.4) transitivePeerDependencies: - bare-buffer - bare-tcp@2.0.2: + bare-tcp@2.0.3: dependencies: - bare-dns: 2.0.2 + bare-dns: 2.0.4 bare-events: 2.5.4 - bare-stream: 2.6.4(bare-events@2.5.4) + bare-stream: 2.6.5(bare-events@2.5.4) transitivePeerDependencies: - bare-buffer @@ -2539,10 +2788,10 @@ snapshots: dependencies: is-windows: 1.0.2 - better-sqlite3@11.7.2: + better-sqlite3@11.8.1: dependencies: bindings: 1.5.0 - prebuild-install: 7.1.2 + prebuild-install: 7.1.3 binary-extensions@2.3.0: {} @@ -2570,7 +2819,7 @@ snapshots: hypertrace: 1.4.2 protomux: 3.10.1 sodium-universal: 4.0.1 - streamx: 2.21.1 + streamx: 2.22.0 transitivePeerDependencies: - sodium-javascript @@ -2580,19 +2829,19 @@ snapshots: blossom-client-sdk@0.9.1: dependencies: - '@noble/hashes': 1.7.0 + '@noble/hashes': 1.7.1 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 + '@cashu/cashu-ts': 2.2.0 + '@noble/hashes': 1.7.1 blossom-server-sdk@0.4.0: dependencies: - better-sqlite3: 11.7.2 + better-sqlite3: 11.8.1 debug: 4.4.0(supports-color@5.5.0) mime: 4.0.6 minio: 7.1.3 @@ -2669,6 +2918,8 @@ snapshots: call-bind-apply-helpers: 1.0.1 get-intrinsic: 1.2.7 + character-entities@2.0.2: {} + chardet@0.7.0: {} chokidar@3.6.0: @@ -2746,6 +2997,10 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + decode-uri-component@0.2.2: {} decompress-response@6.0.0: @@ -2768,25 +3023,31 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-indent@6.1.0: {} detect-libc@2.0.3: {} - dht-rpc@6.16.0: + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dht-rpc@6.16.2: 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 + kademlia-routing-table: 1.0.6 nat-sampler: 1.0.1 sodium-universal: 4.0.1 - streamx: 2.21.1 + streamx: 2.22.0 time-ordered-set: 2.0.1 - udx-native: 1.17.2 + udx-native: 1.17.3 transitivePeerDependencies: - sodium-javascript @@ -2827,12 +3088,14 @@ snapshots: es-errors@1.3.0: {} - es-object-atoms@1.1.0: + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} + escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -2889,6 +3152,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + extendable-error@0.1.7: {} external-editor@3.1.0: @@ -2913,7 +3178,7 @@ snapshots: dependencies: strnum: 1.0.5 - fastq@1.18.0: + fastq@1.19.0: dependencies: reusify: 1.0.4 @@ -2946,7 +3211,7 @@ snapshots: optionalDependencies: debug: 4.4.0(supports-color@5.5.0) - for-each@0.3.3: + for-each@0.3.4: dependencies: is-callable: 1.2.7 @@ -2978,7 +3243,7 @@ snapshots: call-bind-apply-helpers: 1.0.1 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.0 + es-object-atoms: 1.1.1 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 @@ -2991,7 +3256,7 @@ snapshots: get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.0 + es-object-atoms: 1.1.1 get-uri@6.0.4: dependencies: @@ -3080,7 +3345,7 @@ snapshots: hyper-address@0.1.3: dependencies: - '@scure/base': 1.2.1 + '@scure/base': 1.2.4 commander: 12.1.0 debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: @@ -3091,7 +3356,7 @@ snapshots: hyper-socks5-proxy@0.1.2: dependencies: '@pondwader/socks5-server': 1.0.10 - '@scure/base': 1.2.1 + '@scure/base': 1.2.4 commander: 12.1.0 debug: 4.4.0(supports-color@5.5.0) hyper-address: 0.1.3 @@ -3122,7 +3387,7 @@ snapshots: bogon: 1.1.0 compact-encoding: 2.16.0 compact-encoding-net: 1.2.0 - dht-rpc: 6.16.0 + dht-rpc: 6.16.2 hypercore-crypto: 3.4.2 hypercore-id-encoding: 1.3.0 noise-curve-ed: 2.0.1 @@ -3131,7 +3396,7 @@ snapshots: safety-catch: 1.0.2 signal-promise: 1.0.3 sodium-universal: 4.0.1 - streamx: 2.21.1 + streamx: 2.22.0 unslab: 1.3.0 xache: 1.2.1 transitivePeerDependencies: @@ -3190,6 +3455,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.3 @@ -3233,7 +3500,7 @@ snapshots: jwa: 2.0.0 safe-buffer: 5.2.1 - kademlia-routing-table@1.0.3: + kademlia-routing-table@1.0.6: dependencies: bare-events: 2.5.4 @@ -3251,12 +3518,59 @@ snapshots: lodash@4.17.21: {} + longest-streak@3.1.0: {} + lowdb@7.0.1: dependencies: steno: 4.0.2 math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@0.3.0: {} merge-descriptors@1.0.3: {} @@ -3265,6 +3579,139 @@ snapshots: methods@1.1.2: {} + micromark-core-commonmark@2.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.4 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.1 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.0.4: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromark@4.0.1: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.0(supports-color@5.5.0) + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.4 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3321,7 +3768,7 @@ snapshots: nanoid@5.0.9: {} - napi-build-utils@1.0.2: {} + napi-build-utils@2.0.0: {} nat-sampler@1.0.1: {} @@ -3329,9 +3776,9 @@ snapshots: netmask@2.0.2: {} - node-abi@3.72.0: + node-abi@3.74.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 node-fetch@2.7.0: dependencies: @@ -3346,7 +3793,7 @@ snapshots: ignore-by-default: 1.0.1 minimatch: 3.1.2 pstree.remy: 1.1.8 - semver: 7.6.3 + semver: 7.7.1 simple-update-notifier: 2.0.0 supports-color: 5.5.0 touch: 3.1.1 @@ -3382,12 +3829,14 @@ snapshots: nostr-wasm: 0.1.0 typescript: 5.7.3 + nostr-typedef@0.9.0: {} + nostr-wasm@0.1.0: optional: true object-assign@4.1.1: {} - object-inspect@1.13.3: {} + object-inspect@1.13.4: {} on-finished@2.4.1: dependencies: @@ -3449,7 +3898,7 @@ snapshots: degenerator: 5.0.1 netmask: 2.0.2 - package-manager-detector@0.2.8: {} + package-manager-detector@0.2.9: {} parseurl@1.3.3: {} @@ -3471,15 +3920,15 @@ snapshots: possible-typed-array-names@1.0.0: {} - prebuild-install@7.1.2: + prebuild-install@7.1.3: 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 + napi-build-utils: 2.0.0 + node-abi: 3.74.0 pump: 3.0.2 rc: 1.2.8 simple-get: 4.0.1 @@ -3568,6 +4017,30 @@ snapshots: regenerator-runtime@0.14.1: {} + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + resolve-from@5.0.0: {} reusify@1.0.4: {} @@ -3578,6 +4051,11 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rx-nostr@3.5.0: + dependencies: + nostr-typedef: 0.9.0 + rxjs: 7.8.1 + rxjs@7.8.1: dependencies: tslib: 2.8.1 @@ -3596,7 +4074,7 @@ snapshots: sax@1.4.1: {} - semver@7.6.3: {} + semver@7.7.1: {} send@0.19.0: dependencies: @@ -3645,27 +4123,27 @@ snapshots: side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 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 + object-inspect: 1.13.4 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 + object-inspect: 1.13.4 side-channel-map: 1.0.1 side-channel@1.1.0: dependencies: es-errors: 1.3.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 side-channel-list: 1.0.0 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -3684,7 +4162,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 slash@3.0.0: {} @@ -3740,10 +4218,9 @@ snapshots: steno@4.0.2: {} - streamx@2.21.1: + streamx@2.22.0: dependencies: fast-fifo: 1.3.2 - queue-tick: 1.0.1 text-decoder: 1.2.3 optionalDependencies: bare-events: 2.5.4 @@ -3811,6 +4288,8 @@ snapshots: tr46@0.0.3: {} + trough@2.2.0: {} + tslib@2.8.1: {} tunnel-agent@0.6.0: @@ -3824,19 +4303,48 @@ snapshots: typescript@5.7.3: {} - udx-native@1.17.2: + udx-native@1.17.3: dependencies: b4a: 1.6.7 bare-events: 2.5.4 node-gyp-build: 4.8.4 - streamx: 2.21.1 + streamx: 2.22.0 undefsafe@2.0.5: {} undici-types@6.20.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unique-names-generator@4.7.1: {} + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + universalify@0.1.2: {} unpipe@1.0.0: {} @@ -3859,6 +4367,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + web-encoding@1.1.5: dependencies: util: 0.12.5 @@ -3887,7 +4405,7 @@ snapshots: available-typed-arrays: 1.0.7 call-bind: 1.0.8 call-bound: 1.0.3 - for-each: 0.3.3 + for-each: 0.3.4 gopd: 1.2.0 has-tostringtag: 1.0.2 @@ -3913,3 +4431,5 @@ snapshots: z32@1.1.0: dependencies: b4a: 1.6.7 + + zwitch@2.0.4: {} diff --git a/src/app/index.ts b/src/app/index.ts index d73a6d3..88694de 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -7,7 +7,7 @@ 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 { SimpleSigner } from "applesauce-signers/signers/simple-signer"; import cors from "cors"; import { logger } from "../logger.js"; diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 0000000..5004f1b --- /dev/null +++ b/src/core.ts @@ -0,0 +1,23 @@ +import { createRxNostr, noopVerifier } from "rx-nostr"; +import { verifyEvent } from "nostr-tools/wasm"; +import { EventStore, QueryStore } from "applesauce-core"; +import { logger } from "./logger.js"; + +const log = logger.extend("rx-nostr"); + +export const rxNostr = createRxNostr({ + verifier: async (event) => { + try { + return verifyEvent(event); + } catch (error) { + return false; + } + }, +}); + +rxNostr.createConnectionStateObservable().subscribe((packet) => { + log(`${packet.state} ${packet.from}`); +}); + +export const eventStore = new EventStore(); +export const queryStore = new QueryStore(eventStore); diff --git a/src/helpers/array.ts b/src/helpers/array.ts new file mode 100644 index 0000000..5cb2383 --- /dev/null +++ b/src/helpers/array.ts @@ -0,0 +1,4 @@ +export function arrayFallback(arr: T[], fallback: T[]): T[] { + if (arr.length === 0) return fallback; + else return arr; +} diff --git a/src/helpers/ip.ts b/src/helpers/ip.ts index e968969..b4e5cd7 100644 --- a/src/helpers/ip.ts +++ b/src/helpers/ip.ts @@ -1,19 +1,19 @@ -import os from 'node:os'; +import os from "node:os"; export function getIPAddresses() { - var ifaces = os.networkInterfaces(); - var addresses: string[] = []; + var ifaces = os.networkInterfaces(); + var addresses: string[] = []; - for (const [name, info] of Object.entries(ifaces)) { - if (!info) continue; + 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; + 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); - } - } + addresses.push(interfaceInfo.address); + } + } - return addresses; + return addresses; } diff --git a/src/helpers/network.ts b/src/helpers/network.ts index 6ebeea6..6b51da8 100644 --- a/src/helpers/network.ts +++ b/src/helpers/network.ts @@ -1,23 +1,23 @@ -import net from 'net'; +import net from "net"; export function testTCPConnection(host: string, port: number, timeout = 5000) { - return new Promise((resolve, reject) => { - const socket = new net.Socket(); + return new Promise((resolve, reject) => { + const socket = new net.Socket(); - const timer = setTimeout(() => { - socket.destroy(); - reject(new Error('Connection timed out')); - }, timeout); + const timer = setTimeout(() => { + socket.destroy(); + reject(new Error("Connection timed out")); + }, timeout); - socket.connect(port, host, () => { - clearTimeout(timer); - socket.destroy(); - resolve(true); - }); + socket.connect(port, host, () => { + clearTimeout(timer); + socket.destroy(); + resolve(true); + }); - socket.on('error', (err) => { - clearTimeout(timer); - reject(err); - }); - }); + socket.on("error", (err) => { + clearTimeout(timer); + reject(err); + }); + }); } diff --git a/src/index.ts b/src/index.ts index 55f473c..4bbe744 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "./polyfill.js"; import process from "node:process"; import path from "node:path"; @@ -7,9 +8,7 @@ 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"; @@ -19,10 +18,6 @@ import { pathExists } from "./helpers/fs.js"; 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); diff --git a/src/modules/address-book.ts b/src/modules/address-book.ts index 15a8d5a..ac3945a 100644 --- a/src/modules/address-book.ts +++ b/src/modules/address-book.ts @@ -1,66 +1,66 @@ -import { NostrEvent, kinds } from 'nostr-tools'; -import _throttle from 'lodash.throttle'; +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'; +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; + log = logger.extend("AddressBook"); + app: App; + loader: PubkeyBatchLoader; - get extraRelays() { - return this.loader.extraRelays; - } - set extraRelays(v: string[]) { - this.loader.extraRelays = v; - } + get extraRelays() { + return this.loader.extraRelays; + } + set extraRelays(v: string[]) { + this.loader.extraRelays = v; + } - constructor(app: App) { - this.app = app; + 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 = 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}`); - }); - } + 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); - } + getMailboxes(pubkey: string) { + return this.loader.getEvent(pubkey); + } - getOutboxes(pubkey: string) { - const mailboxes = this.getMailboxes(pubkey); - return mailboxes && getOutboxes(mailboxes); - } + getOutboxes(pubkey: string) { + const mailboxes = this.getMailboxes(pubkey); + return mailboxes && getOutboxes(mailboxes); + } - getInboxes(pubkey: string) { - const mailboxes = this.getMailboxes(pubkey); - return mailboxes && getInboxes(mailboxes); - } + getInboxes(pubkey: string) { + const mailboxes = this.getMailboxes(pubkey); + return mailboxes && getInboxes(mailboxes); + } - handleEvent(event: NostrEvent) { - this.loader.handleEvent(event); - } + handleEvent(event: NostrEvent) { + this.loader.handleEvent(event); + } - async loadMailboxes(pubkey: string, relays?: string[]) { - return this.loader.getOrLoadEvent(pubkey, relays); - } + 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 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); - } + 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 index 937cf82..b3825c7 100644 --- a/src/modules/cautious-pool.ts +++ b/src/modules/cautious-pool.ts @@ -1,95 +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 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'; +import { logger } from "../logger.js"; export type TestRelay = (relay: AbstractRelay, challenge: string) => boolean; type EventMap = { - challenge: [AbstractRelay, string]; - connected: [AbstractRelay]; - closed: [AbstractRelay]; + challenge: [AbstractRelay, string]; + connected: [AbstractRelay]; + closed: [AbstractRelay]; }; export default class CautiousPool extends SimplePool { - log = logger.extend('CautiousPool'); - isSelf?: TestRelay; - blacklist = new Set(); + log = logger.extend("CautiousPool"); + isSelf?: TestRelay; + blacklist = new Set(); - challenges = new Map(); - authenticated = new Map(); + challenges = new Map(); + authenticated = new Map(); - emitter = new EventEmitter(); - constructor(isSelf?: TestRelay) { - super(); + emitter = new EventEmitter(); + constructor(isSelf?: TestRelay) { + super(); - this.isSelf = isSelf; - } + this.isSelf = isSelf; + } - async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise { - url = normalizeURL(url); + 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'); + 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'); + 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'); + const relay = await super.ensureRelay(url, params); + if (this.checkRelay(relay)) throw new Error("Cant connect to self"); - this.emitter.emit('connected', relay); + 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._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.onnotice = () => {}; - relay.onclose = () => { - this.challenges.delete(relay.url); - this.authenticated.delete(relay.url); - this.emitter.emit('closed', relay); - }; + relay.onclose = () => { + this.challenges.delete(relay.url); + this.authenticated.delete(relay.url); + this.emitter.emit("closed", relay); + }; - return relay; - } + return relay; + } - private checkRelay(relay: AbstractRelay, challenge?: string) { - // @ts-expect-error - challenge = challenge || relay.challenge; + 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; - } - } + 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; - } + return false; + } - isAuthenticated(relay: string | AbstractRelay) { - return !!this.authenticated.get(typeof relay === 'string' ? relay : relay.url); - } + 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; + async authenticate(url: string | AbstractRelay, auth: VerifiedEvent) { + const relay = typeof url === "string" ? await this.ensureRelay(url) : url; - return await relay.auth(async (draft) => auth); - } + return await relay.auth(async (draft) => auth); + } - [Symbol.iterator](): IterableIterator<[string, AbstractRelay]> { - return this.relays[Symbol.iterator](); - } + [Symbol.iterator](): IterableIterator<[string, AbstractRelay]> { + return this.relays[Symbol.iterator](); + } } diff --git a/src/modules/community-multiplexer.ts b/src/modules/community-multiplexer.ts index c2bc5ee..69b51a3 100644 --- a/src/modules/community-multiplexer.ts +++ b/src/modules/community-multiplexer.ts @@ -1,89 +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 { 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'; +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; + log = logger.extend("community-multiplexer"); + db: Database; + eventStore: IEventStore; + pool: SimplePool; + connectionManager: HyperConnectionManager; - communities = new Map(); + communities = new Map(); - constructor(db: Database, eventStore: IEventStore) { - this.db = db; - this.eventStore = eventStore; - this.pool = new SimplePool(); + constructor(db: Database, eventStore: IEventStore) { + this.db = db; + this.eventStore = eventStore; + this.pool = new SimplePool(); - this.connectionManager = new HyperConnectionManager(randomBytes(32).toString('hex')); + this.connectionManager = new HyperConnectionManager(randomBytes(32).toString("hex")); - this.syncCommunityDefinitions(); - } + this.syncCommunityDefinitions(); + } - attachToServer(wss: WebSocketServer) { - wss.on('connection', this.handleConnection.bind(this)); - } + attachToServer(wss: WebSocketServer) { + wss.on("connection", this.handleConnection.bind(this)); + } - handleConnection(ws: WebSocket, req: IncomingMessage) { - if (!req.url) return false; + 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; + 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); + 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; - } - } + // 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(), - }); - } + 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; + 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]; + 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'); + 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); + this.log("Creating community proxy", pubkey); + const community = new CommunityProxy(this.db, definition, this.connectionManager); - community.connect(); - this.communities.set(pubkey, community); + community.connect(); + this.communities.set(pubkey, community); - return community; - } + return community; + } - stop() { - for (const [pubkey, community] of this.communities) { - community.stop(); - } - this.communities.clear(); - this.connectionManager.stop(); - } + stop() { + for (const [pubkey, community] of this.communities) { + community.stop(); + } + this.communities.clear(); + this.connectionManager.stop(); + } } diff --git a/src/modules/contact-book.ts b/src/modules/contact-book.ts index d622ed3..3a5ef62 100644 --- a/src/modules/contact-book.ts +++ b/src/modules/contact-book.ts @@ -1,54 +1,54 @@ -import { NostrEvent, kinds } from 'nostr-tools'; -import _throttle from 'lodash.throttle'; +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'; +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; + log = logger.extend("ContactsBook"); + app: App; + loader: PubkeyBatchLoader; + extraRelays = COMMON_CONTACT_RELAYS; - constructor(app: App) { - this.app = app; + 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 = 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}`); - }); - } + 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); - } + 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 []; - } + 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); - } + handleEvent(event: NostrEvent) { + this.loader.handleEvent(event); + } - async loadContacts(pubkey: string, relays: string[] = []) { - return this.loader.getOrLoadEvent(pubkey, relays); - } + 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 index f9c713c..8e38a56 100644 --- a/src/modules/control/config-actions.ts +++ b/src/modules/control/config-actions.ts @@ -1,49 +1,49 @@ -import { WebSocket } from 'ws'; -import { ConfigMessage, ConfigResponse } from '@satellite-earth/core/types/control-api/config.js'; +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'; +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'; + app: App; + name = "CONFIG"; - private subscribed = new Set(); + private subscribed = new Set(); - constructor(app: App) { - this.app = app; + 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]); - } - }); - } + // 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; + 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]; + case "SET": + const field = message[3]; + const value = message[4]; - this.app.config.setField(field, value); - return true; + this.app.config.setField(field, value); + return true; - default: - return false; - } - } + default: + return false; + } + } - send(sock: WebSocket | NodeJS.Process, response: ConfigResponse) { - sock.send?.(JSON.stringify(response)); - } + 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 index 7191524..2eeafd6 100644 --- a/src/modules/control/control-api.ts +++ b/src/modules/control/control-api.ts @@ -1,130 +1,130 @@ -import { WebSocket, WebSocketServer } from 'ws'; -import { type IncomingMessage } from 'http'; -import { ControlResponse } from '@satellite-earth/core/types/control-api/index.js'; +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'; +import type App from "../../app/index.js"; +import { logger } from "../../logger.js"; -export type ControlMessage = ['CONTROL', string, string, ...any[]]; +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; + 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(); + app: App; + auth?: string; + log = logger.extend("ControlApi"); + handlers = new Map(); - authenticatedConnections = new Set(); + authenticatedConnections = new Set(); - constructor(app: App, auth?: string) { - this.app = app; - this.auth = auth; - } + 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); - } + 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)); - } + /** 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); - }); + 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); - } + for (const [id, handler] of this.handlers) { + handler.handleConnection?.(ws); + } - ws.once('close', () => this.handleDisconnect(ws)); - } - handleDisconnect(ws: WebSocket) { - this.authenticatedConnections.delete(ws); + ws.once("close", () => this.handleDisconnect(ws)); + } + handleDisconnect(ws: WebSocket) { + this.authenticatedConnections.delete(ws); - for (const [id, handler] of this.handlers) { - handler.handleDisconnect?.(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); - } - }); + 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); - } - } + 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[]; + /** 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 - } - } + 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; - } + /** 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); - } + 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; - } + this.log("Failed to handle Control message", message); + return false; + } - send(sock: WebSocket | NodeJS.Process, response: ControlResponse) { - sock.send?.(JSON.stringify(response)); - } + send(sock: WebSocket | NodeJS.Process, response: ControlResponse) { + sock.send?.(JSON.stringify(response)); + } } diff --git a/src/modules/control/decryption-cache.ts b/src/modules/control/decryption-cache.ts index 8238d6b..cd6e3c4 100644 --- a/src/modules/control/decryption-cache.ts +++ b/src/modules/control/decryption-cache.ts @@ -1,50 +1,50 @@ -import { WebSocket } from 'ws'; +import { WebSocket } from "ws"; import { - DecryptionCacheMessage, - DecryptionCacheResponse, -} from '@satellite-earth/core/types/control-api/decryption-cache.js'; + 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'; +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'; + app: App; + name = "DECRYPTION-CACHE"; - constructor(app: App) { - this.app = app; - } + 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; + 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-PUBKEY": + this.app.decryptionCache.clearPubkey(message[3]); + return true; - case 'CLEAR': - this.app.decryptionCache.clearAll(); - 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; + 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; - } - } + default: + return false; + } + } - send(sock: WebSocket | NodeJS.Process, response: DecryptionCacheResponse) { - sock.send?.(JSON.stringify(response)); - } + 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 index 29b2bc5..d5c5a80 100644 --- a/src/modules/control/dm-actions.ts +++ b/src/modules/control/dm-actions.ts @@ -1,31 +1,31 @@ -import { WebSocket } from 'ws'; -import { DirectMessageMessage } from '@satellite-earth/core/types/control-api/direct-messages.js'; +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'; +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'; + app: App; + name = "DM"; - constructor(app: App) { - this.app = app; - } + 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; + 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; + case "CLOSE": + this.app.directMessageManager.closeConversation(message[3], message[4]); + return true; - default: - return false; - } - } + default: + return false; + } + } } diff --git a/src/modules/control/logs-actions.ts b/src/modules/control/logs-actions.ts index 743e63a..ee4bd26 100644 --- a/src/modules/control/logs-actions.ts +++ b/src/modules/control/logs-actions.ts @@ -1,27 +1,27 @@ -import { WebSocket } from 'ws'; -import { LogsMessage } from '@satellite-earth/core/types/control-api/logs.js'; +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'; +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'; + app: App; + name = "LOGS"; - constructor(app: App) { - this.app = app; - } + 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; + 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; - } - } + default: + return false; + } + } } diff --git a/src/modules/control/notification-actions.ts b/src/modules/control/notification-actions.ts index 315074c..22b92d5 100644 --- a/src/modules/control/notification-actions.ts +++ b/src/modules/control/notification-actions.ts @@ -1,43 +1,43 @@ -import { WebSocket } from 'ws'; -import { NotificationsMessage, NotificationsResponse } from '@satellite-earth/core/types/control-api/notifications.js'; +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'; +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'; + app: App; + name = "NOTIFICATIONS"; - constructor(app: App) { - this.app = app; - } + 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; + 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 "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 "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; + 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)); - } + 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 index e297ced..d519d05 100644 --- a/src/modules/control/receiver-actions.ts +++ b/src/modules/control/receiver-actions.ts @@ -1,29 +1,29 @@ -import { WebSocket } from 'ws'; -import { ReceiverMessage } from '@satellite-earth/core/types/control-api/receiver.js'; +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'; +import type App from "../../app/index.js"; +import { type ControlMessageHandler } from "./control-api.js"; export default class ReceiverActions implements ControlMessageHandler { - app: App; - name = 'RECEIVER'; + 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; + 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; + case "STOP": + this.app.receiver.stop(); + return true; - default: - return false; - } - } + default: + return false; + } + } } diff --git a/src/modules/control/remote-auth-actions.ts b/src/modules/control/remote-auth-actions.ts index 3065c2f..0689789 100644 --- a/src/modules/control/remote-auth-actions.ts +++ b/src/modules/control/remote-auth-actions.ts @@ -1,69 +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 { 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'; +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'; + app: App; + name = "REMOTE-AUTH"; - private subscribed = new Set(); + private subscribed = new Set(); - constructor(app: App) { - this.app = app; + 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), - ]); - } - }); - } + // 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); + 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]); - } - } - } + 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; - } - } + 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)); - } + 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 index e0bfb42..0358b04 100644 --- a/src/modules/control/report-actions.ts +++ b/src/modules/control/report-actions.ts @@ -1,93 +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 { 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'; +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'); + app: App; + name = "REPORT"; + log = logger.extend("ReportActions"); - types: { - [k in keyof ReportArguments]?: typeof Report; - } = REPORT_CLASSES; + types: { + [k in keyof ReportArguments]?: typeof Report; + } = REPORT_CLASSES; - private reports = new Map>>(); + private reports = new Map>>(); - constructor(app: App) { - this.app = app; - } + 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; - } + 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); + 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) { + for (const [id, report] of reports) report.close(); - if (reports.size) this.log(`Closed ${reports.size} reports for disconnected socket`); - this.reports.delete(ws); - } - } + 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]; + // 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); + 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)); + this.log(`Creating ${type} ${id} report with args`, JSON.stringify(args)); - report = new ReportClass(id, this.app, sock); - reports.set(id, report); - } + 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; - } - } + 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(); - } - } - } + 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 index 6cb890a..42863d3 100644 --- a/src/modules/control/scrapper-actions.ts +++ b/src/modules/control/scrapper-actions.ts @@ -1,37 +1,37 @@ -import { WebSocket } from 'ws'; -import { ScrapperMessage } from '@satellite-earth/core/types/control-api/scrapper.js'; +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'; +import type App from "../../app/index.js"; +import { type ControlMessageHandler } from "./control-api.js"; export default class ScrapperActions implements ControlMessageHandler { - app: App; - name = 'SCRAPPER'; + 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; + 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 "STOP": + this.app.scrapper.stop(); + return true; - case 'ADD-PUBKEY': - this.app.scrapper.addPubkey(message[3]); - return true; + case "ADD-PUBKEY": + this.app.scrapper.addPubkey(message[3]); + return true; - case 'REMOVE-PUBKEY': - this.app.scrapper.removePubkey(message[3]); - return true; + case "REMOVE-PUBKEY": + this.app.scrapper.removePubkey(message[3]); + return true; - default: - return false; - } - } + default: + return false; + } + } } diff --git a/src/modules/decryption-cache/decryption-cache.ts b/src/modules/decryption-cache/decryption-cache.ts index 8333fed..5b54989 100644 --- a/src/modules/decryption-cache/decryption-cache.ts +++ b/src/modules/decryption-cache/decryption-cache.ts @@ -1,153 +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 { 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'; +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'); +const migrations = new MigrationSet("decryption-cache"); // Version 1 migrations.addScript(1, async (db, log) => { - db.prepare( - ` + db.prepare( + ` CREATE TABLE "decryption_cache" ( "event" TEXT(64) NOT NULL, "content" TEXT NOT NULL, PRIMARY KEY("event") ); `, - ).run(); + ).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 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 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( - ` + ).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(); + ).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`); + // 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]; + cache: [string, string]; }; export default class DecryptionCache extends EventEmitter { - database: Database; - log = logger.extend('DecryptionCache'); + database: Database; + log = logger.extend("DecryptionCache"); - constructor(database: Database) { - super(); - this.database = database; - } + constructor(database: Database) { + super(); + this.database = database; + } - setup() { - return migrations.run(this.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); + /** 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}`); + if (result.changes > 0) { + this.log(`Saved content for ${id}`); - this.emit('cache', id, plaintext); - } - } + 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`) - } + /** 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(); - } + /** 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[] = []; + 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 + 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); + 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()); - } + // 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 ')}`; - } + if (andConditions.length > 0) { + sql += ` WHERE ${andConditions.join(" AND ")}`; + } - switch (filter?.order) { - case 'rank': - sql += ' ORDER BY rank'; - break; + switch (filter?.order) { + case "rank": + sql += " ORDER BY rank"; + break; - case 'created_at': - default: - sql += ' ORDER BY events.created_at DESC'; - 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 })); - } + 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); + 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); - } + 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 index 056c3d7..4b9d099 100644 --- a/src/modules/direct-message-manager.ts +++ b/src/modules/direct-message-manager.ts @@ -1,168 +1,169 @@ -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 { NostrEvent, kinds } from "nostr-tools"; +import { SubCloser } from "nostr-tools/abstract-pool"; +import { Subscription } from "nostr-tools/abstract-relay"; +import { getInboxes } from "applesauce-core/helpers"; +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'; +import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js"; +import { logger } from "../logger.js"; +import type App from "../app/index.js"; +import { arrayFallback } from "../helpers/array.js"; type EventMap = { - open: [string, string]; - close: [string, string]; - message: [NostrEvent]; + 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; + log = logger.extend("DirectMessageManager"); + app: App; - private explicitRelays: string[] = []; + private explicitRelays: string[] = []; - constructor(app: App) { - super(); - this.app = app; + 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)); - }); + // 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); - }); - } + // 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; + /** 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; + const addressedTo = event.tags.find((t) => t[0] === "p")?.[1]; + if (!addressedTo) return; - // get users inboxes - let relays = await this.app.addressBook.loadInboxes(addressedTo); + // 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 (!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 (appRelays) relays = appRelays.filter((r) => r.write).map((r) => r.url); + } + } - if (!relays || relays.length === 0) { - // use fallback relays - relays = this.explicitRelays; - } + 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)); + this.log(`Forwarding message to ${relays.length} relays`); + const results = await Promise.allSettled(this.app.pool.publish(relays, event)); - return results; - } + return results; + } - private getConversationKey(a: string, b: string) { - if (a < b) return a + ':' + b; - else return b + ':' + a; - } + 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; + 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; - } + 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(); + const relays = arrayFallback(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); - } - }, - }); + 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); - }; + 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(); - } - } - } + 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); + subscriptions = new Map(); + async openConversation(a: string, b: string) { + const key = this.getConversationKey(a, b); - if (this.subscriptions.has(key)) return; + if (this.subscriptions.has(key)) return; - const aMailboxes = await this.app.addressBook.loadMailboxes(a); - const bMailboxes = await this.app.addressBook.loadMailboxes(b); + 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; + // 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 ? arrayFallback(getInboxes(aMailboxes), this.explicitRelays) : this.explicitRelays; + const bInboxes = bMailboxes ? arrayFallback(getInboxes(bMailboxes), this.explicitRelays) : this.explicitRelays; - const relays = new Set([...aInboxes, ...bInboxes]); + 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`); - }, - }, - ); + 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); + 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); - } - } + 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 index 41b9160..bd68ff3 100644 --- a/src/modules/gossip.ts +++ b/src/modules/gossip.ts @@ -1,132 +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 { SimpleSigner } from "applesauce-signers/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'; +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(), - }; + 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; + 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[] = []; + 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; - } + 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(); + async gossip() { + const pubkey = await this.signer.getPublicKey(); - if (this.broadcastRelays.length === 0) return; + 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.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.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')), - ); - } - } + 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(); + private async update() { + if (!this.running) return; + await this.gossip(); - setTimeout(this.update.bind(this), this.interval); - } + setTimeout(this.update.bind(this), this.interval); + } - start() { - if (this.running) return; - this.running = true; + start() { + if (this.running) return; + this.running = true; - this.log(`Starting gossip on ${this.broadcastRelays.join(', ')}`); - setTimeout(this.update.bind(this), 5000); - } + this.log(`Starting gossip on ${this.broadcastRelays.join(", ")}`); + setTimeout(this.update.bind(this), 5000); + } - stop() { - this.log('Stopping gossip'); - this.running = false; - } + stop() { + this.log("Stopping gossip"); + this.running = false; + } - private lookups = new Map(); - async lookup(pubkey: string) { - const last = this.lookups.get(pubkey); + private lookups = new Map(); + async lookup(pubkey: string) { + const last = this.lookups.get(pubkey); - const filter = { authors: [pubkey], '#p': [pubkey], kinds: [30166] }; + 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')); + // 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 sub = this.pool.subscribeMany(this.broadcastRelays, [filter], { + onevent: (event) => this.eventStore.addEvent(event), + oneose: () => { + sub.close(); + res(); + }, + }); + }); + } - const events = this.eventStore.getEventsForFilters([filter]); + 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; - } + 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 index bcb624f..26ed9be 100644 --- a/src/modules/graph/index.ts +++ b/src/modules/graph/index.ts @@ -1,105 +1,105 @@ -import { NostrEvent, kinds } from 'nostr-tools'; -import App from '../../app/index.js'; +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; + contacts: Record }> = {}; + app: App; - constructor(app: App) { - this.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); - } - } + 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); - } - } + add(event: NostrEvent) { + if (event.kind === kinds.Contacts) { + this.addContacts(event); + } + } - addContacts(event: NostrEvent) { - const existing = this.contacts[event.pubkey]; + 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])); + // 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, - }; - } - } + this.contacts[event.pubkey] = { + created_at: event.created_at, + set: following, + }; + } + } - getNodes(roots: string[] = []): Node[] { - const u: Record = {}; + getNodes(roots: string[] = []): Node[] { + const u: Record = {}; - // Init u with root pubkeys - for (let p of roots) { - u[p] = { z: 0, n: 1 }; - } + // 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; - } + 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; - } + // 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; - } + if (!u[c]) { + u[c] = { z, n: 1 }; + } else { + if (u[c].z > z) { + return; + } - u[c].n++; - } - }); - } - }; + u[c].n++; + } + }); + } + }; - // Populate u with all the pubkeys that - // are directly followed by root pubkey - populate(roots, 1); + // 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, - ); + // 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; - }); - } + // 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 index dde1f0f..d885097 100644 --- a/src/modules/hyper-connection-manager.ts +++ b/src/modules/hyper-connection-manager.ts @@ -1,65 +1,65 @@ -import net from 'net'; -import HyperDHT from 'hyperdht'; -import { pipeline } from 'streamx'; -import { logger } from '../logger.js'; +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; + log = logger.extend(`hyper-connection-manager`); + sockets = new Map(); + servers = new Map(); + node: HyperDHT; - lastPort = START_PORT; + lastPort = START_PORT; - constructor(privateKey: string) { - this.node = new HyperDHT({ - keyPair: HyperDHT.keyPair(Buffer.from(privateKey, 'hex')), - }); - } + 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, - }); + 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); + // @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); - }); - }); + 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); + 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); - }); - }); - } + 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); + async getLocalAddress(pubkey: string) { + let server = this.servers.get(pubkey); + if (!server) server = await this.bind(pubkey); - return server!.address() as net.AddressInfo; - } + return server!.address() as net.AddressInfo; + } - stop() { - for (const [pubkey, server] of this.servers) { - server.close(); - } - this.servers.clear(); - } + 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 index f5f3cfe..49bddfa 100644 --- a/src/modules/labeled-event-store.ts +++ b/src/modules/labeled-event-store.ts @@ -1,79 +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'; +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(', ')})`; + return `(${params.map(() => `?`).join(", ")})`; } -const migrations = new MigrationSet('labeled-event-store'); +const migrations = new MigrationSet("labeled-event-store"); // Version 1 migrations.addScript(1, async (db, log) => { - db.prepare( - ` + db.prepare( + ` CREATE TABLE IF NOT EXISTS event_labels ( id INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT(64) REFERENCES events(id), label TEXT ) `, - ).run(); + ).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(); + 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; + label: string; + readAll = false; - constructor(db: Database, label: string) { - super(db); - this.label = label; + constructor(db: Database, label: string) { + super(db); + this.label = label; - this.log = logger.extend(`event-store:` + label); - } + this.log = logger.extend(`event-store:` + label); + } - async setup() { - await super.setup(); - await migrations.run(this.db); - } + async setup() { + await super.setup(); + await migrations.run(this.db); + } - override buildConditionsForFilters(filter: Filter) { - const parts = super.buildConditionsForFilters(filter); + 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; - } + 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; - } + return parts; + } - addEvent(event: NostrEvent) { - const inserted = super.addEvent(event); + 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); + 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; - } + return inserted; + } - removeEvents(ids: string[]) { - this.db.prepare(`DELETE FROM event_labels WHERE event IN ${mapParams(ids)}`).run(...ids); - return super.removeEvents(ids); - } + 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); - } + 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 index 34a19e6..2ce8897 100644 --- a/src/modules/log-store/log-store.ts +++ b/src/modules/log-store/log-store.ts @@ -1,32 +1,32 @@ -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 { 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'; +import { logger } from "../../logger.js"; type EventMap = { - log: [LogEntry]; - clear: [string | undefined]; + log: [LogEntry]; + clear: [string | undefined]; }; export type LogEntry = { - id: string; - service: string; - timestamp: number; - message: string; + id: string; + service: string; + timestamp: number; + message: string; }; export type DatabaseLogEntry = LogEntry & { - id: number | bigint; + id: number | bigint; }; -const migrations = new MigrationSet('log-store'); +const migrations = new MigrationSet("log-store"); // version 1 migrations.addScript(1, async (db, log) => { - db.prepare( - ` + db.prepare( + ` CREATE TABLE IF NOT EXISTS "logs" ( "id" TEXT NOT NULL UNIQUE, "timestamp" INTEGER NOT NULL, @@ -35,129 +35,129 @@ migrations.addScript(1, async (db, log) => { PRIMARY KEY("id") ); `, - ).run(); - log('Created logs table'); + ).run(); + log("Created logs table"); - db.prepare('CREATE INDEX IF NOT EXISTS logs_service ON logs(service)'); - log('Created logs service index'); + 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; + database: SQLDatabase; + debug: Debugger; - constructor(database: SQLDatabase) { - super(); - this.database = database; - this.debug = logger; - } + constructor(database: SQLDatabase) { + super(); + this.database = database; + this.debug = logger; + } - async setup() { - return await migrations.run(this.database); - } + 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, - }; + 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); + this.queue.push(entry); + this.emit("log", entry); - if (!this.running) this.write(); - } + if (!this.running) this.write(); + } - running = false; - queue: LogEntry[] = []; - private write() { - if (this.running) return; - this.running = true; + running = false; + queue: LogEntry[] = []; + private write() { + if (this.running) return; + this.running = true; - const BATCH_SIZE = 5000; + const BATCH_SIZE = 5000; - const inserted: (number | bigint)[] = []; - const failed: LogEntry[] = []; + 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); + 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); - } + inserted.push(lastInsertRowid); + } catch (error) { + failed.push(entry); + } - if (++i >= BATCH_SIZE) break; - } - })(); + if (++i >= BATCH_SIZE) break; + } + })(); - for (const entry of failed) { - // Don't know what to do here... - } + 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; - } + 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)[] = []; + getLogs(filter?: { service?: string; since?: number; until?: number; limit?: number }) { + const conditions: string[] = []; + const parameters: (string | number)[] = []; - let sql = `SELECT * FROM logs`; + 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?.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); - } + 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)[] = []; + clearLogs(filter?: { service?: string; since?: number; until?: number }) { + const conditions: string[] = []; + const parameters: (string | number)[] = []; - let sql = `DELETE FROM logs`; + 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 ')}`; + 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); - } + 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 index 276b2eb..448ee36 100644 --- a/src/modules/network/inbound/hyper.ts +++ b/src/modules/network/inbound/hyper.ts @@ -1,71 +1,71 @@ -import HolesailServer from 'holesail-server'; -import { encodeAddress } from 'hyper-address'; -import { hexToBytes } from '@noble/hashes/utils'; -import { AddressInfo } from 'net'; +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'; +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'); + app: App; + hyper?: HolesailServer; + log = logger.extend("Network:Inbound:Hyper"); - get available() { - return true; - } - running = false; - error?: Error; - address?: string; + get available() { + return true; + } + running = false; + error?: Error; + address?: string; - constructor(app: App) { - this.app = app; - } + constructor(app: App) { + this.app = app; + } - async start(address: AddressInfo) { - try { - this.running = true; - this.error = undefined; + async start(address: AddressInfo) { + try { + this.running = true; + this.error = undefined; - this.log(`Importing and starting hyperdht node`); + this.log(`Importing and starting hyperdht node`); - const { default: HolesailServer } = await import('holesail-server'); - const { getOrCreateNode } = await import('../../../sidecars/hyperdht.js'); + const { default: HolesailServer } = await import("holesail-server"); + const { getOrCreateNode } = await import("../../../sidecars/hyperdht.js"); - const hyper = (this.hyper = new HolesailServer()); - hyper.dht = getOrCreateNode(); + 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; + 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; - } - } + 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; - } + 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 index 67b5400..a98f976 100644 --- a/src/modules/network/inbound/i2p.ts +++ b/src/modules/network/inbound/i2p.ts @@ -1,76 +1,76 @@ -import type { AddressInfo } from 'net'; -import type { I2pSamStream } from '@diva.exchange/i2p-sam'; +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'; +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'); + app: App; + log = logger.extend("Network:Inbound:I2P"); - available = !!I2P_SAM_ADDRESS; - running = false; - address?: string; - error?: Error; + available = !!I2P_SAM_ADDRESS; + running = false; + address?: string; + error?: Error; - private forward?: I2pSamStream; + private forward?: I2pSamStream; - constructor(app: App) { - this.app = app; - } + constructor(app: App) { + this.app = app; + } - async start(address: AddressInfo) { - try { - if (this.running) return; - this.running = true; + 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}`); + 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'); + 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'); + // 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.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.address = "http://" + this.forward.getB32Address(); - this.log(`Listening on ${this.address}`); + 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; - } - } + // 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; + async stop() { + if (!this.running) return; + this.running = false; - if (this.forward) { - this.log('Closing forwarding stream'); - this.forward.close(); - this.forward = undefined; - } - } + 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 index c1c8261..27190c7 100644 --- a/src/modules/network/inbound/index.ts +++ b/src/modules/network/inbound/index.ts @@ -1,79 +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'; +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; + 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; + running = false; + get addresses() { + const ip = getIPAddresses(); + const hyper = this.hyper.address; + const tor = this.tor.address; - return [...(ip ?? []), ...(tor ?? []), ...(hyper ?? [])]; - } + return [...(ip ?? []), ...(tor ?? []), ...(hyper ?? [])]; + } - constructor(app: App) { - this.app = app; + constructor(app: App) { + this.app = app; - this.hyper = new HyperInbound(app); - this.tor = new TorInbound(app); - this.i2p = new I2PInbound(app); + this.hyper = new HyperInbound(app); + this.tor = new TorInbound(app); + this.i2p = new I2PInbound(app); - this.listenToAppConfig(app.config); - } + this.listenToAppConfig(app.config); + } - private getAddress() { - const address = this.app.server.address(); + 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'); + if (typeof address === "string" || address === null) + throw new Error("External servers started when server does not have an address"); - return address; - } + return address; + } - private update(config = this.app.config.data) { - if (!this.running) return; - const address = this.getAddress(); + 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.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.tor.available) { + if (!this.tor.running) this.tor.start(address); + } - if (this.i2p.available) { - if (!this.i2p.running) this.i2p.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)); - } + /** 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(); - } + start() { + this.running = true; + this.update(); + } - async stop() { - this.running = false; - await this.hyper.stop(); - await this.tor.stop(); - await this.i2p.stop(); - } + 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 index cf008cc..d2f6d70 100644 --- a/src/modules/network/inbound/tor.ts +++ b/src/modules/network/inbound/tor.ts @@ -1,29 +1,29 @@ -import { AddressInfo } from 'net'; +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'; +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'); + app: App; + log = logger.extend("Network:Inbound:Tor"); - readonly available = !!TOR_ADDRESS; - readonly running = !!TOR_ADDRESS; - readonly address = TOR_ADDRESS; - error?: Error; + readonly available = !!TOR_ADDRESS; + readonly running = !!TOR_ADDRESS; + readonly address = TOR_ADDRESS; + error?: Error; - constructor(app: App) { - this.app = app; - } + constructor(app: App) { + this.app = app; + } - async start(address: AddressInfo) { - // not implemented yet - if (TOR_ADDRESS) this.log(`Listening on ${TOR_ADDRESS}`); - } + async start(address: AddressInfo) { + // not implemented yet + if (TOR_ADDRESS) this.log(`Listening on ${TOR_ADDRESS}`); + } - async stop() { - // not implemented yet - } + async stop() { + // not implemented yet + } } diff --git a/src/modules/network/interfaces.ts b/src/modules/network/interfaces.ts index 355dd49..18c5f9e 100644 --- a/src/modules/network/interfaces.ts +++ b/src/modules/network/interfaces.ts @@ -1,22 +1,22 @@ -import { AddressInfo } from 'net'; +import { AddressInfo } from "net"; export interface InboundInterface { - available: boolean; - running: boolean; - error?: Error; + available: boolean; + running: boolean; + error?: Error; - address?: string; - start(address: AddressInfo): Promise; - stop(): Promise; + address?: string; + start(address: AddressInfo): Promise; + stop(): Promise; } export interface OutboundInterface { - available: boolean; - running: boolean; - error?: Error; + available: boolean; + running: boolean; + error?: Error; - type: 'SOCKS5' | 'HTTP'; - address?: string; - start(): Promise; - stop(): Promise; + 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 index 3aa6826..398ae89 100644 --- a/src/modules/network/outbound/hyper.ts +++ b/src/modules/network/outbound/hyper.ts @@ -1,57 +1,57 @@ -import type { createProxy } from 'hyper-socks5-proxy'; -import getPort from 'get-port'; -import EventEmitter from 'events'; +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'; +import { logger } from "../../../logger.js"; +import { OutboundInterface } from "../interfaces.js"; type EventMap = { - started: []; - stopped: []; + started: []; + stopped: []; }; export default class HyperOutbound extends EventEmitter implements OutboundInterface { - log = logger.extend('Network:Outbound:Hyper'); - private port?: number; - private proxy?: ReturnType; + 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; - } + running = false; + error?: Error; + readonly type = "SOCKS5"; + address?: string; + get available() { + return true; + } - async start() { - if (this.running) return; - this.running = 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'); + 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.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; - } - } + 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; + 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'); - } + 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 index 30b9d0e..2d332ca 100644 --- a/src/modules/network/outbound/i2p.ts +++ b/src/modules/network/outbound/i2p.ts @@ -1,31 +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'; +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'); + log = logger.extend("Network:Outbound:I2P"); - running = false; - error?: Error; - readonly type = I2P_PROXY_TYPE; - readonly address = I2P_PROXY; - readonly available = !!I2P_PROXY; + 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; + 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; - } - } + 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() {} + async stop() {} } diff --git a/src/modules/network/outbound/index.ts b/src/modules/network/outbound/index.ts index 0ec708c..2dc3a4c 100644 --- a/src/modules/network/outbound/index.ts +++ b/src/modules/network/outbound/index.ts @@ -1,132 +1,132 @@ -import { PacProxyAgent } from 'pac-proxy-agent'; -import _throttle from 'lodash.throttle'; +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'; +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; + log = logger.extend("Network:Outbound"); + hyper: HyperOutbound; + tor: TorOutbound; + i2p: I2POutbound; - running = false; - agent: PacProxyAgent; + running = false; + agent: PacProxyAgent; - enableHyperConnections = false; - enableTorConnections = false; - enableI2PConnections = false; - routeAllTrafficThroughTor = false; + enableHyperConnections = false; + enableTorConnections = false; + enableI2PConnections = false; + routeAllTrafficThroughTor = false; - constructor() { - this.hyper = new HyperOutbound(); - this.tor = new TorOutbound(); - this.i2p = new I2POutbound(); + constructor() { + this.hyper = new HyperOutbound(); + this.tor = new TorOutbound(); + this.i2p = new I2POutbound(); - this.agent = new PacProxyAgent(this.buildPacURI(), { fallbackToDirect: true }); - } + this.agent = new PacProxyAgent(this.buildPacURI(), { fallbackToDirect: true }); + } - private buildPacURI() { - const statements: string[] = []; + private buildPacURI() { + const statements: string[] = []; - if (this.i2p.available && this.enableI2PConnections) { - statements.push( - ` + 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 (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 (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";'); - } + 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 = ` + const PACFile = ` // SPDX-License-Identifier: CC0-1.0 function FindProxyForURL(url, host) { - ${statements.join('\n')} + ${statements.join("\n")} } `.trim(); - return 'pac+data:application/x-ns-proxy-autoconfig;base64,' + btoa(PACFile); - } + 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, '')); + 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; - } + // forces the agent to refetch the resolver and pac file + this.agent.resolverPromise = undefined; + } - updateAgentThrottle: () => void = _throttle(this.updateAgent.bind(this), 100); + 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; + /** 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.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.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(); - } + if (this.i2p.available && this.enableI2PConnections !== this.i2p.running) { + if (this.enableI2PConnections) this.i2p.start(); + else this.i2p.stop(); + } - this.updateAgentThrottle(); - }); - } + this.updateAgentThrottle(); + }); + } - async stop() { - await this.hyper.stop(); - await this.tor.stop(); - } + async stop() { + await this.hyper.stop(); + await this.tor.stop(); + } } const outboundNetwork = new OutboundNetworkManager(); diff --git a/src/modules/network/outbound/tor.ts b/src/modules/network/outbound/tor.ts index 3b03a63..ab7a723 100644 --- a/src/modules/network/outbound/tor.ts +++ b/src/modules/network/outbound/tor.ts @@ -1,31 +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'; +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'); + log = logger.extend("Network:Outbound:Tor"); - running = false; - error?: Error; - readonly type = TOR_PROXY_TYPE; - readonly address = TOR_PROXY; - readonly available = !!TOR_PROXY; + 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; + 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; - } - } + 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() {} + async stop() {} } diff --git a/src/modules/network/outbound/websocket.ts b/src/modules/network/outbound/websocket.ts index 0b5d3bc..a8de87b 100644 --- a/src/modules/network/outbound/websocket.ts +++ b/src/modules/network/outbound/websocket.ts @@ -1,11 +1,11 @@ -import { ClientRequestArgs } from 'http'; -import { ClientOptions, WebSocket } from 'ws'; +import { ClientRequestArgs } from "http"; +import { ClientOptions, WebSocket } from "ws"; -import outboundNetwork from './index.js'; +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 }); - } + 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 index bbcd3a2..0b11d10 100644 --- a/src/modules/notifications/notifications-manager.ts +++ b/src/modules/notifications/notifications-manager.ts @@ -1,155 +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 { 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'; +import { logger } from "../../logger.js"; +import App from "../../app/index.js"; export type NotificationsManagerState = { - channels: NotificationChannel[]; + channels: NotificationChannel[]; }; type EventMap = { - addChannel: [NotificationChannel]; - updateChannel: [NotificationChannel]; - removeChannel: [NotificationChannel]; + addChannel: [NotificationChannel]; + updateChannel: [NotificationChannel]; + removeChannel: [NotificationChannel]; }; export default class NotificationsManager extends EventEmitter { - log = logger.extend('Notifications'); - app: App; - lastRead: number = dayjs().unix(); + log = logger.extend("Notifications"); + app: App; + lastRead: number = dayjs().unix(); - webPushKeys: webPush.VapidKeys = webPush.generateVAPIDKeys(); + webPushKeys: webPush.VapidKeys = webPush.generateVAPIDKeys(); - state: NotificationsManagerState = { channels: [] }; + state: NotificationsManagerState = { channels: [] }; - get channels() { - return this.state.channels; - } + get channels() { + return this.state.channels; + } - constructor(app: App) { - super(); - this.app = app; - } + constructor(app: App) { + super(); + this.app = app; + } - async setup() { - this.state = ( - await this.app.state.getMutableState('notification-manager', { channels: [] }) - ).proxy; - } + 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); - } - } + 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; + /** 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; - } + 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); + /** 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)}`, - }; - } - } + 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; + 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`); + 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, - }; + 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; + 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, - }; + 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; + 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); - } - } - } + 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 index c3b3f48..4131f98 100644 --- a/src/modules/profile-book.ts +++ b/src/modules/profile-book.ts @@ -1,40 +1,40 @@ -import { NostrEvent, kinds } from 'nostr-tools'; -import _throttle from 'lodash.throttle'; +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'; +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; + log = logger.extend("ProfileBook"); + app: App; + loader: PubkeyBatchLoader; + extraRelays = COMMON_CONTACT_RELAYS; - constructor(app: App) { - this.app = app; + 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 = 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}`); - }); - } + 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); - } + getProfile(pubkey: string) { + return this.loader.getEvent(pubkey); + } - handleEvent(event: NostrEvent) { - this.loader.handleEvent(event); - } + handleEvent(event: NostrEvent) { + this.loader.handleEvent(event); + } - async loadProfile(pubkey: string, relays: string[] = []) { - return this.loader.getOrLoadEvent(pubkey, relays); - } + async loadProfile(pubkey: string, relays: string[] = []) { + return this.loader.getOrLoadEvent(pubkey, relays); + } } diff --git a/src/modules/receiver/index.ts b/src/modules/receiver/index.ts index 8effeb0..1296330 100644 --- a/src/modules/receiver/index.ts +++ b/src/modules/receiver/index.ts @@ -1,259 +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 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'; +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; + 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); - }, - }); - }); + 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]; + started: [Receiver]; + stopped: [Receiver]; + status: [string]; + rebuild: []; + subscribed: [string, string[]]; + closed: [string, string[]]; + error: [Error]; + event: [NostrEvent]; }; -type ReceiverStatus = 'running' | 'starting' | 'errored' | 'stopped'; +type ReceiverStatus = "running" | "starting" | "errored" | "stopped"; export default class Receiver extends EventEmitter { - log = logger.extend('Receiver'); + log = logger.extend("Receiver"); - _status: ReceiverStatus = 'stopped'; - get status() { - return this._status; - } - set status(v: ReceiverStatus) { - this._status = v; - this.emit('status', v); - } + _status: ReceiverStatus = "stopped"; + get status() { + return this._status; + } + set status(v: ReceiverStatus) { + this._status = v; + this.emit("status", v); + } - starting = true; - startupError?: Error; + starting = true; + startupError?: Error; - app: App; - pool: SimplePool; + app: App; + pool: SimplePool; - subscriptions = new Map(); + subscriptions = new Map(); - constructor(app: App, pool?: SimplePool) { - super(); - this.app = app; - this.pool = pool || app.pool; - } + 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()); + // 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()); + // 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'); + 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); + 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.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(); + 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); + // 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); + const people = getPubkeysFromList(contacts); - this.log(`Found ${people.length} contacts`); + this.log(`Found ${people.length} contacts`); - let usersWithMailboxes = 0; - let usersWithContactRelays = 0; - let usersWithFallbackRelays = 0; + 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); + // 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); + 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 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++; + 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); - }), - ); + // 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`, - ); - } + this.log( + `Found ${usersWithMailboxes} users with mailboxes, ${usersWithContactRelays} user with relays in contact list, and ${usersWithFallbackRelays} using fallback relays`, + ); + } - buildMap() { - this.map.clear(); + 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); + // 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); - } + // 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'); + this.emit("rebuild"); - return this.map; - } + return this.map; + } - private handleEvent(event: NostrEvent) { - this.emit('event', event); - } + private handleEvent(event: NostrEvent) { + this.emit("event", event); + } - async updateRelaySubscription(url: string) { - const pubkeys = this.map.get(url); - if (pubkeys.size === 0) return; + 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 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); - }, - }); + 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)); + 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`); - } - } - } + 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[] = []; + 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 - }); + 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); - } + promises.push(p); + } - return Promise.all(promises); - } + return Promise.all(promises); + } - async start() { - if (this.status === 'running' || this.status === 'starting') return; + async start() { + if (this.status === "running" || this.status === "starting") return; - try { - this.log('Starting'); - this.startupError = undefined; - this.status = 'starting'; + try { + this.log("Starting"); + this.startupError = undefined; + this.status = "starting"; - await this.fetchData(); - this.buildMap(); - await this.ensureSubscriptions(); + 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); - } - } - } + 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; + /** stop receiving events and disconnect from all relays */ + stop() { + if (this.status === "stopped") return; - this.status = 'stopped'; + this.status = "stopped"; - for (const [relay, sub] of this.subscriptions) sub.close(); - this.subscriptions.clear(); + for (const [relay, sub] of this.subscriptions) sub.close(); + this.subscriptions.clear(); - this.log('Stopped'); - this.emit('stopped', this); - } + this.log("Stopped"); + this.emit("stopped", this); + } - destroy() { - this.stop(); - this.removeAllListeners(); - } + destroy() { + this.stop(); + this.removeAllListeners(); + } } diff --git a/src/modules/reports/reports/conversations.ts b/src/modules/reports/reports/conversations.ts index 32c092e..bd5a61e 100644 --- a/src/modules/reports/reports/conversations.ts +++ b/src/modules/reports/reports/conversations.ts @@ -1,115 +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 { 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'; +import Report from "../report.js"; -export default class ConversationsReport extends Report<'CONVERSATIONS'> { - readonly type = 'CONVERSATIONS'; +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 }>( - ` + 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); + ) + .get(self, other); - const received = this.app.database.db - .prepare<[string, string], { pubkey: string; count: number; lastMessage: number }>( - ` + 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); + ) + .get(self, other); - const result: ReportResults['CONVERSATIONS'] = { - pubkey: other, - count: (received?.count ?? 0) + (sent?.count ?? 0), - sent: 0, - received: 0, - }; + 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; - } + 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 }>( - ` + 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); + ) + .all(self); - const received = this.app.database.db - .prepare<[string], { pubkey: string; count: number; lastMessage: number }>( - ` + 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); + ) + .all(self); - const results = new SuperMap((pubkey) => ({ - pubkey, - count: sent.length + received.length, - sent: 0, - received: 0, - })); + 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; - } + 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), - ); - } + 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; + 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; + const self = args.pubkey; - // get the latest stats from the database - this.getConversationResult(self, self === from ? to : from).then((result) => this.send(result)); - }; + // 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); - } + 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); + async execute(args: ReportArguments["CONVERSATIONS"]) { + const results = await this.getAllConversationResults(args.pubkey); - for (const result of results) { - this.send(result); - } - } + 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 index c166bd1..53ce298 100644 --- a/src/modules/reports/reports/dm-search.ts +++ b/src/modules/reports/reports/dm-search.ts @@ -1,11 +1,11 @@ -import { ReportArguments } from '@satellite-earth/core/types'; -import Report from '../report.js'; +import { ReportArguments } from "@satellite-earth/core/types"; +import Report from "../report.js"; -export default class DMSearchReport extends Report<'DM_SEARCH'> { - readonly type = 'DM_SEARCH'; +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); - } + 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 index dc3de88..e8f6e14 100644 --- a/src/modules/reports/reports/events-summary.ts +++ b/src/modules/reports/reports/events-summary.ts @@ -1,12 +1,12 @@ -import { ReportArguments } from '@satellite-earth/core/types'; -import { EventRow, parseEventRow } from '@satellite-earth/core'; -import Report from '../report.js'; +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'; +export default class EventsSummaryReport extends Report<"EVENTS_SUMMARY"> { + readonly type = "EVENTS_SUMMARY"; - async execute(args: ReportArguments['EVENTS_SUMMARY']): Promise { - let sql = ` + async execute(args: ReportArguments["EVENTS_SUMMARY"]): Promise { + let sql = ` SELECT events.*, COUNT(l.id) AS reactions, @@ -20,50 +20,50 @@ export default class EventsSummaryReport extends Report<'EVENTS_SUMMARY'> { LEFT JOIN events AS r ON r.id = tags.e AND r.kind = 1 `; - const params: any[] = []; - const conditions: string[] = []; + 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 (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`; - } + if (conditions.length > 0) { + sql += ` WHERE ${conditions.join(" AND ")}\n`; + } - sql += ' GROUP BY events.id\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; - } + 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); + let limit = args.limit || 100; + sql += ` LIMIT ?`; + params.push(limit); - const rows = await this.app.database.db - .prepare(sql) - .all(...params); + const rows = await this.app.database.db + .prepare(sql) + .all(...params); - const results = rows.map((row) => { - const event = parseEventRow(row); + const results = rows.map((row) => { + const event = parseEventRow(row); - return { event, reactions: row.reactions, shares: row.shares, replies: row.replies }; - }); + return { event, reactions: row.reactions, shares: row.shares, replies: row.replies }; + }); - for (const result of results) { - this.send(result); - } - } + for (const result of results) { + this.send(result); + } + } } diff --git a/src/modules/reports/reports/index.ts b/src/modules/reports/reports/index.ts index 3e609ba..75387f3 100644 --- a/src/modules/reports/reports/index.ts +++ b/src/modules/reports/reports/index.ts @@ -1,30 +1,30 @@ -import { ReportArguments } from '@satellite-earth/core/types'; -import Report from '../report.js'; +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'; +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; + [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, + 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 index 7a6fefa..fc8ca3b 100644 --- a/src/modules/reports/reports/logs.ts +++ b/src/modules/reports/reports/logs.ts @@ -1,23 +1,23 @@ -import { ReportArguments } from '@satellite-earth/core/types'; +import { ReportArguments } from "@satellite-earth/core/types"; -import { LogEntry } from '../../log-store/log-store.js'; -import Report from '../report.js'; +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'; +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); - }; + 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); - } + 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); - } + 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 index 4d1ee28..fb2f067 100644 --- a/src/modules/reports/reports/network-status.ts +++ b/src/modules/reports/reports/network-status.ts @@ -1,69 +1,69 @@ -import Report from '../report.js'; +import Report from "../report.js"; -export default class NetworkStatusReport extends Report<'NETWORK_STATUS'> { - readonly type = 'NETWORK_STATUS'; +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; + 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, - }, - }, - }); - } + 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); + 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); + // NOTE: set and interval since there are not events to listen to yet + const i = setInterval(listener, 1000); - return () => clearInterval(i); - } + return () => clearInterval(i); + } - async execute(args: {}): Promise { - this.update(); - } + async execute(args: {}): Promise { + this.update(); + } } diff --git a/src/modules/reports/reports/notification-channels.ts b/src/modules/reports/reports/notification-channels.ts index 4cd7466..f4f122a 100644 --- a/src/modules/reports/reports/notification-channels.ts +++ b/src/modules/reports/reports/notification-channels.ts @@ -1,29 +1,29 @@ -import { NotificationChannel } from '@satellite-earth/core/types/control-api/notifications.js'; -import Report from '../report.js'; +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'; +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]); - }; + 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); + 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); - }; - } + 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); - } - } + 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 index c011e00..d5531b8 100644 --- a/src/modules/reports/reports/overview.ts +++ b/src/modules/reports/reports/overview.ts @@ -1,40 +1,40 @@ -import { NostrEvent } from 'nostr-tools'; -import { ReportArguments } from '@satellite-earth/core/types'; +import { NostrEvent } from "nostr-tools"; +import { ReportArguments } from "@satellite-earth/core/types"; -import Report from '../report.js'; +import Report from "../report.js"; -export default class OverviewReport extends Report<'OVERVIEW'> { - readonly type = 'OVERVIEW'; +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); + 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); - }; + if (result) this.send(result); + }; - this.app.eventStore.on('event:inserted', listener); - return () => { - this.app.eventStore.off('event:inserted', listener); - }; - } + 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(); + 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); - } - } + 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 index 33937c6..3d51e36 100644 --- a/src/modules/reports/reports/receiver-status.ts +++ b/src/modules/reports/reports/receiver-status.ts @@ -1,38 +1,38 @@ -import Report from '../report.js'; +import Report from "../report.js"; -export default class ReceiverStatusReport extends Report<'RECEIVER_STATUS'> { - readonly type = 'RECEIVER_STATUS'; +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, - })), - }); - } + 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); + 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); + 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); - }; - } + 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(); - } + async execute(args: {}): Promise { + this.update(); + } } diff --git a/src/modules/reports/reports/scrapper-status.ts b/src/modules/reports/reports/scrapper-status.ts index 6611c4e..8887813 100644 --- a/src/modules/reports/reports/scrapper-status.ts +++ b/src/modules/reports/reports/scrapper-status.ts @@ -1,55 +1,55 @@ -import { NostrEvent } from 'nostr-tools'; -import _throttle from 'lodash.throttle'; -import Report from '../report.js'; +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'; +export default class ScrapperStatusReport extends Report<"SCRAPPER_STATUS"> { + readonly type = "SCRAPPER_STATUS"; - eventsPerSecond: number[] = [0]; + eventsPerSecond: number[] = [0]; - update() { - const averageEventsPerSecond = this.eventsPerSecond.reduce((m, v) => m + v, 0) / this.eventsPerSecond.length; - const pubkeys = this.app.scrapper.state.pubkeys; + 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++; - } - } + 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, - }); - } + this.send({ + running: this.app.scrapper.running, + eventsPerSecond: averageEventsPerSecond, + activeSubscriptions, + pubkeys, + }); + } - async setup() { - const onEvent = (event: NostrEvent) => { - this.eventsPerSecond[0]++; - }; + async setup() { + const onEvent = (event: NostrEvent) => { + this.eventsPerSecond[0]++; + }; - this.app.scrapper.on('event', onEvent); + this.app.scrapper.on("event", onEvent); - const tick = setInterval(() => { - // start a new second - this.eventsPerSecond.unshift(0); + const tick = setInterval(() => { + // start a new second + this.eventsPerSecond.unshift(0); - // limit to 60 seconds - while (this.eventsPerSecond.length > 60) this.eventsPerSecond.pop(); + // limit to 60 seconds + while (this.eventsPerSecond.length > 60) this.eventsPerSecond.pop(); - this.update(); - }, 1000); + this.update(); + }, 1000); - return () => { - this.app.scrapper.off('event', onEvent); - clearInterval(tick); - }; - } + return () => { + this.app.scrapper.off("event", onEvent); + clearInterval(tick); + }; + } - async execute(args: {}): Promise { - this.update(); - } + async execute(args: {}): Promise { + this.update(); + } } diff --git a/src/modules/reports/reports/services.ts b/src/modules/reports/reports/services.ts index ab24fa5..57c6f04 100644 --- a/src/modules/reports/reports/services.ts +++ b/src/modules/reports/reports/services.ts @@ -1,12 +1,12 @@ -import Report from '../report.js'; +import Report from "../report.js"; -export default class ServicesReport extends Report<'SERVICES'> { - readonly type = 'SERVICES'; +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); - } + 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/pubkey-relay-scrapper.ts b/src/modules/scrapper/pubkey-relay-scrapper.ts index fc28c00..8a66b0e 100644 --- a/src/modules/scrapper/pubkey-relay-scrapper.ts +++ b/src/modules/scrapper/pubkey-relay-scrapper.ts @@ -1,115 +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 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'; +import { logger } from "../../logger.js"; function stripProtocol(url: string) { - return url.replace(/^\w+\:\/\//, ''); + return url.replace(/^\w+\:\/\//, ""); } const DEFAULT_LIMIT = 1000; export type PubkeyRelayScrapperState = { - cursor?: number; - complete?: boolean; + cursor?: number; + complete?: boolean; }; type EventMap = { - event: [NostrEvent]; - chunk: [{ count: number; cursor: number }]; + event: [NostrEvent]; + chunk: [{ count: number; cursor: number }]; }; export default class PubkeyRelayScrapper extends EventEmitter { - pubkey: string; - relay: AbstractRelay; - log: Debugger; + pubkey: string; + relay: AbstractRelay; + log: Debugger; - running = false; - error?: Error; - state: PubkeyRelayScrapperState = {}; + 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; - } + 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; + private subscription?: Subscription; - constructor(pubkey: string, relay: AbstractRelay, state?: PubkeyRelayScrapperState) { - super(); + constructor(pubkey: string, relay: AbstractRelay, state?: PubkeyRelayScrapperState) { + super(); - this.pubkey = pubkey; - this.relay = relay; - if (state) this.state = state; + this.pubkey = pubkey; + this.relay = relay; + if (state) this.state = state; - this.log = logger.extend('scrapper:' + pubkey + ':' + stripProtocol(relay.url)); - } + 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; + 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; + this.running = true; - // wait for relay connection - await this.relay.connect(); + // 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})`); + 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++; + // 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; + 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; + // 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.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.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); + 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(); - }, - }); - }); - } + rej(this.error); + } + res(); + }, + }); + }); + } } diff --git a/src/modules/scrapper/pubkey-scrapper.ts b/src/modules/scrapper/pubkey-scrapper.ts index c73e8f6..f08ddd5 100644 --- a/src/modules/scrapper/pubkey-scrapper.ts +++ b/src/modules/scrapper/pubkey-scrapper.ts @@ -1,83 +1,83 @@ -import App from '../../app/index.js'; -import { NostrEvent } from 'nostr-tools'; -import { EventEmitter } from 'events'; -import { Debugger } from 'debug'; +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'; +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]; + event: [NostrEvent]; }; export default class PubkeyScrapper extends EventEmitter { - app: App; - pubkey: string; - additionalRelays: string[] = []; - log: Debugger; + app: App; + pubkey: string; + additionalRelays: string[] = []; + log: Debugger; - private failed = new Set(); - relayScrappers = new Map(); + private failed = new Set(); + relayScrappers = new Map(); - constructor(app: App, pubkey: string) { - super(); - this.app = app; - this.pubkey = pubkey; + constructor(app: App, pubkey: string) { + super(); + this.app = app; + this.pubkey = pubkey; - this.log = logger.extend('scrapper:' + this.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); + async ensureData() { + // get mailboxes + this.app.profileBook.loadProfile(this.pubkey); + const mailboxes = await this.app.addressBook.loadMailboxes(this.pubkey); - return { mailboxes }; - } + return { mailboxes }; + } - async loadNext() { - const { mailboxes } = await this.ensureData(); + async loadNext() { + const { mailboxes } = await this.ensureData(); - const outboxes = getOutboxes(mailboxes); + const outboxes = getOutboxes(mailboxes); - const relays = [...outboxes, ...this.additionalRelays]; - const scrappers: PubkeyRelayScrapper[] = []; - for (const url of relays) { - if (this.failed.has(url)) continue; + 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)); + 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; + // 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); - } + 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); - } - } + 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); + // 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(); - } - } + const next = incomplete[0]; + if (next) { + await next.loadNext(); + } + } } diff --git a/src/modules/secrets-manager.ts b/src/modules/secrets-manager.ts index 19e4b6d..5eb9f29 100644 --- a/src/modules/secrets-manager.ts +++ b/src/modules/secrets-manager.ts @@ -1,125 +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 _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'; +import { logger } from "../logger.js"; type Secrets = { - nostrKey: Uint8Array; - vapidPrivateKey: string; - vapidPublicKey: string; - hyperKey: Buffer; - i2pPrivateKey?: string; - i2pPublicKey?: string; + 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; + 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: []; + /** 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; + log = logger.extend("SecretsManager"); + protected secrets?: Secrets; + path: string; - constructor(path: string) { - super(); - this.path = path; - } + 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; + 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(); - } + this.emit("changed", secret, value); + this.emit("updated"); + this.write(); + } - read() { - this.log('Loading secrets'); + read() { + this.log("Loading secrets"); - let json: Record = {}; + let json: Record = {}; - try { - json = JSON.parse(fs.readFileSync(this.path, { encoding: 'utf-8' })); - } catch (error) {} + try { + json = JSON.parse(fs.readFileSync(this.path, { encoding: "utf-8" })); + } catch (error) {} - let changed = false; + let changed = false; - const secrets = {} as Secrets; + 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.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.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'); + 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; + secrets.i2pPrivateKey = json.i2pPrivateKey; + secrets.i2pPublicKey = json.i2pPublicKey; - this.secrets = secrets; + this.secrets = secrets; - this.emit('loaded'); - this.emit('updated'); + this.emit("loaded"); + this.emit("updated"); - if (changed) this.write(); - } - write() { - if (!this.secrets) throw new Error('Secrets not loaded'); + if (changed) this.write(); + } + write() { + if (!this.secrets) throw new Error("Secrets not loaded"); - this.log('Saving'); + 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, - }; + 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' }); + fs.writeFileSync(this.path, JSON.stringify(json, null, 2), { encoding: "utf-8" }); - this.emit('saved'); - } + this.emit("saved"); + } } diff --git a/src/modules/state/application-state-manager.ts b/src/modules/state/application-state-manager.ts index c4e2b42..f2ca840 100644 --- a/src/modules/state/application-state-manager.ts +++ b/src/modules/state/application-state-manager.ts @@ -1,49 +1,49 @@ -import { MigrationSet } from '@satellite-earth/core/sqlite'; -import { Database } from 'better-sqlite3'; +import { MigrationSet } from "@satellite-earth/core/sqlite"; +import { Database } from "better-sqlite3"; -import { MutableState } from './mutable-state.js'; +import { MutableState } from "./mutable-state.js"; -const migrations = new MigrationSet('application-state'); +const migrations = new MigrationSet("application-state"); migrations.addScript(1, async (db, log) => { - db.prepare( - ` + db.prepare( + ` CREATE TABLE "application_state" ( "id" TEXT NOT NULL, "state" TEXT, PRIMARY KEY("id") ); `, - ).run(); + ).run(); - log('Created application state table'); + log("Created application state table"); }); export default class ApplicationStateManager { - private mutableState = new Map>(); + private mutableState = new Map>(); - database: Database; - constructor(database: Database) { - this.database = database; - } + database: Database; + constructor(database: Database) { + this.database = database; + } - async setup() { - await migrations.run(this.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; + 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; - } + 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(); - } - } + 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 index 78a62ce..f23302a 100644 --- a/src/modules/state/mutable-state.ts +++ b/src/modules/state/mutable-state.ts @@ -1,91 +1,91 @@ -import { EventEmitter } from 'events'; -import { Database } from 'better-sqlite3'; -import _throttle from 'lodash.throttle'; -import { Debugger } from 'debug'; +import { EventEmitter } from "events"; +import { Database } from "better-sqlite3"; +import _throttle from "lodash.throttle"; +import { Debugger } from "debug"; -import { logger } from '../../logger.js'; +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]; + /** 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; + state?: T; + log: Debugger; - private _proxy?: T; + 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; - } + /** 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; + 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(); - } + 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; + 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; - }, - })); - } + 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); + 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); + 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'); - } + 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}`); + if (!this.state) throw new Error(`Missing initial state for ${this.key}`); - this.createProxy(); + this.createProxy(); - if (this.state) { - this.emit('loaded', this.state); - this.emit('updated', this.state); - } - } - async save() { - if (!this.state) return; + 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)); + 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); - } + this.log("Saved"); + this.emit("saved", this.state); + } } diff --git a/src/modules/switchboard/switchboard.ts b/src/modules/switchboard/switchboard.ts index a3f99d4..c7108e5 100644 --- a/src/modules/switchboard/switchboard.ts +++ b/src/modules/switchboard/switchboard.ts @@ -1,92 +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'; +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'); + private app: App; + private log = logger.extend("Switchboard"); - constructor(app: App) { - this.app = app; - } + constructor(app: App) { + this.app = app; + } - public handleConnection(downstream: WebSocket, req: IncomingMessage) { - let upstream: WebSocket | undefined; + 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')); + 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 (!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 (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; - } + if (addresses.length === 0) { + downstream.send(JSON.stringify(["PROXY", "ERROR", "Lookup failed"])); + return; + } - this.app.relay.disconnectSocket(downstream); - downstream.send(JSON.stringify(['PROXY', 'CONNECTING'])); + 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); + 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)); - }); + // wait for connection + await new Promise((res, rej) => { + upstream?.once("open", () => res()); + upstream?.once("error", (error) => rej(error)); + }); - this.log(`Proxy connection to ${address}`); + this.log(`Proxy connection to ${address}`); - // clear last error - error = undefined; + // clear last error + error = undefined; - // Forward from client to target relay - downstream.on('message', (message, isBinary) => { - upstream?.send(message, { binary: isBinary }); - }); + // 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 }); - }); + // 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()); + // connect the close events + upstream.on("close", () => downstream.close()); + downstream.on("close", () => upstream?.close()); - // tell downstream its connected - downstream.send(JSON.stringify(['PROXY', 'CONNECTED'])); + // 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; - } - } + // 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); + // 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); - } + this.app.relay.handleConnection(downstream, req); + } } diff --git a/src/polyfill.ts b/src/polyfill.ts new file mode 100644 index 0000000..1eec21a --- /dev/null +++ b/src/polyfill.ts @@ -0,0 +1,6 @@ +import { useWebSocketImplementation } from "nostr-tools/relay"; +import OutboundProxyWebSocket from "./modules/network/outbound/websocket.js"; + +// @ts-expect-error +global.WebSocket = OutboundProxyWebSocket; +useWebSocketImplementation(OutboundProxyWebSocket); diff --git a/src/sidecars/hyperdht.ts b/src/sidecars/hyperdht.ts index 42f8f45..aa9be0b 100644 --- a/src/sidecars/hyperdht.ts +++ b/src/sidecars/hyperdht.ts @@ -1,19 +1,19 @@ -import HyperDHT from 'hyperdht'; -import { logger } from '../logger.js'; +import HyperDHT from "hyperdht"; +import { logger } from "../logger.js"; -const log = logger.extend('HyperDHT'); +const log = logger.extend("HyperDHT"); let node: HyperDHT | undefined; export function getOrCreateNode() { - if (node) return node; + if (node) return node; - log('Creating HyperDHT Node'); - return (node = new HyperDHT()); + log("Creating HyperDHT Node"); + return (node = new HyperDHT()); } export function destroyNode() { - if (node) { - node.destroy(); - node = undefined; - } + if (node) { + node.destroy(); + node = undefined; + } } diff --git a/src/types/holesail-server.d.ts b/src/types/holesail-server.d.ts index 59f7917..4d35be4 100644 --- a/src/types/holesail-server.d.ts +++ b/src/types/holesail-server.d.ts @@ -1,24 +1,24 @@ -declare module 'holesail-server' { - import HyperDHT, { KeyPair, Server } from 'hyperdht'; +declare module "holesail-server" { + import HyperDHT, { KeyPair, Server } from "hyperdht"; - type ServeArgs = { - secure?: boolean; - buffSeed?: Buffer; - port?: number; - address?: string; - }; + 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; + 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; - } + 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 index b7e0465..e00cf98 100644 --- a/src/types/hyperdht.d.ts +++ b/src/types/hyperdht.d.ts @@ -1,38 +1,38 @@ -declare module 'hyperdht' { - import type { Socket } from 'net'; - import type EventEmitter from 'events'; +declare module "hyperdht" { + import type { Socket } from "net"; + import type EventEmitter from "events"; - class NoiseStreamSocket extends Socket { - remotePublicKey: Buffer; - } + class NoiseStreamSocket extends Socket { + remotePublicKey: Buffer; + } - export class Server extends EventEmitter<{ - listening: []; - connection: [NoiseStreamSocket]; - }> { - address(): { host: string; port: string; publicKey: Buffer } | null; + export class Server extends EventEmitter<{ + listening: []; + connection: [NoiseStreamSocket]; + }> { + address(): { host: string; port: string; publicKey: Buffer } | null; - listen(keyPair: KeyPair): Promise; - } + listen(keyPair: KeyPair): Promise; + } - type KeyPair = { - publicKey: Buffer; - secretKey: Buffer; - }; + type KeyPair = { + publicKey: Buffer; + secretKey: Buffer; + }; - export default class HyperDHT { - constructor(opts?: { keyPair: KeyPair; bootstrap?: string[] }); + 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; + 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; + connect(host: Buffer, opts?: { reusableSocket: boolean }): Socket; - static keyPair(seed?: Buffer): KeyPair; - } + static keyPair(seed?: Buffer): KeyPair; + } } diff --git a/src/types/streamx.d.ts b/src/types/streamx.d.ts index c053c9d..eb34fd3 100644 --- a/src/types/streamx.d.ts +++ b/src/types/streamx.d.ts @@ -1,5 +1,5 @@ -declare module 'streamx' { - import { Duplex, Stream } from 'stream'; +declare module "streamx" { + import { Duplex, Stream } from "stream"; - export function pipeline(...streams: Stream[]): Duplex; + export function pipeline(...streams: Stream[]): Duplex; }