Code cleanup/rewrite

💥 Breaking Changes 💥

- Ditched Bun for Deno 🦕: Migrated from Bun to Deno due to recurring memory leaks and crashes on our test server.
- SQL Simplification 📈: Removed Prisma and now using Libsql alone
- Hono Takes the Stage: Switched from Elysia to Hono, a cleaner and more compatible framework that plays nice with Deno.

🧹 Code Cleanup 💪

Removed unnecessary clutter and streamlined the codebase for better readability and maintainability.
This commit is contained in:
Danny Morabito 2024-12-03 18:33:15 +01:00
parent 095791f44f
commit bec36602b5
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
14 changed files with 1235 additions and 431 deletions

BIN
bun.lockb

Binary file not shown.

19
deno.json Normal file
View file

@ -0,0 +1,19 @@
{
"nodeModulesDir": "auto",
"tasks": {
"dev": "deno run --allow-all --watch src/index.ts"
},
"imports": {
"@cashu/cashu-ts": "npm:@cashu/cashu-ts@^2.0.0",
"@libsql/client": "npm:@libsql/client",
"@nostr-dev-kit/ndk": "npm:@nostr-dev-kit/ndk@^2.10.7",
"@std/bytes": "jsr:@std/bytes@^1.0.4",
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
"@std/encoding": "jsr:@std/encoding@^1.0.5",
"nostr-tools": "jsr:@nostr/tools@^2.10.4",
"@std/assert": "jsr:@std/assert@1",
"@arx/utils": "https://git.arx-ccn.com/Arx/ts-utils/raw/commit/c1d309ba097ada64cd072ec1e3e97edaaf8773b6/src/index.ts",
"smtp-server": "npm:smtp-server@^3.13.6",
"winston": "npm:winston@^3.17.0"
}
}

733
deno.lock Normal file
View file

@ -0,0 +1,733 @@
{
"version": "4",
"specifiers": {
"jsr:@nostr/tools@^2.10.4": "2.10.4",
"jsr:@std/assert@1": "1.0.8",
"jsr:@std/bytes@^1.0.4": "1.0.4",
"jsr:@std/dotenv@~0.225.2": "0.225.2",
"jsr:@std/encoding@^1.0.5": "1.0.5",
"jsr:@std/internal@^1.0.5": "1.0.5",
"npm:@cashu/cashu-ts@2": "2.0.0",
"npm:@libsql/client@*": "0.14.0",
"npm:@noble/ciphers@~0.5.1": "0.5.3",
"npm:@noble/curves@1.2.0": "1.2.0",
"npm:@noble/hashes@1.3.1": "1.3.1",
"npm:@nostr-dev-kit/ndk@^2.10.7": "2.10.7",
"npm:@scure/base@1.1.1": "1.1.1",
"npm:@scure/bip32@1.3.1": "1.3.1",
"npm:@scure/bip39@1.2.1": "1.2.1",
"npm:nostr-wasm@0.1.0": "0.1.0",
"npm:smtp-server@^3.13.6": "3.13.6",
"npm:winston@^3.17.0": "3.17.0"
},
"jsr": {
"@nostr/tools@2.10.4": {
"integrity": "7fda015c96b4f674727843aecb990e2af1989e4724588415ccf6f69066abfd4f",
"dependencies": [
"npm:@noble/ciphers",
"npm:@noble/curves",
"npm:@noble/hashes",
"npm:@scure/base",
"npm:@scure/bip32",
"npm:@scure/bip39",
"npm:nostr-wasm"
]
},
"@std/assert@1.0.8": {
"integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/bytes@1.0.4": {
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
},
"@std/dotenv@0.225.2": {
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
},
"@std/encoding@1.0.5": {
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
},
"@std/internal@1.0.5": {
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
}
},
"npm": {
"@cashu/cashu-ts@2.0.0": {
"integrity": "sha512-neVWZGviQGFf2RlpVpEerf8zQZDR4HvzmDj58gsae1gOQxzaZoU9BdAyRjVpcvz/dPTYQKZii9mUTAgR+fof2w==",
"dependencies": [
"@cashu/crypto",
"@noble/curves@1.7.0",
"@noble/hashes@1.6.1",
"@scure/bip32@1.6.0",
"buffer"
]
},
"@cashu/crypto@0.3.4": {
"integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==",
"dependencies": [
"@noble/curves@1.7.0",
"@noble/hashes@1.6.1",
"@scure/bip32@1.6.0",
"@scure/bip39@1.5.0",
"buffer"
]
},
"@colors/colors@1.6.0": {
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="
},
"@dabh/diagnostics@2.0.3": {
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"dependencies": [
"colorspace",
"enabled",
"kuler"
]
},
"@libsql/client@0.14.0": {
"integrity": "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==",
"dependencies": [
"@libsql/core",
"@libsql/hrana-client",
"js-base64",
"libsql",
"promise-limit"
]
},
"@libsql/core@0.14.0": {
"integrity": "sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==",
"dependencies": [
"js-base64"
]
},
"@libsql/darwin-arm64@0.4.7": {
"integrity": "sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg=="
},
"@libsql/darwin-x64@0.4.7": {
"integrity": "sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA=="
},
"@libsql/hrana-client@0.7.0": {
"integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==",
"dependencies": [
"@libsql/isomorphic-fetch",
"@libsql/isomorphic-ws",
"js-base64",
"node-fetch"
]
},
"@libsql/isomorphic-fetch@0.3.1": {
"integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw=="
},
"@libsql/isomorphic-ws@0.1.5": {
"integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==",
"dependencies": [
"@types/ws",
"ws"
]
},
"@libsql/linux-arm64-gnu@0.4.7": {
"integrity": "sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA=="
},
"@libsql/linux-arm64-musl@0.4.7": {
"integrity": "sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw=="
},
"@libsql/linux-x64-gnu@0.4.7": {
"integrity": "sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ=="
},
"@libsql/linux-x64-musl@0.4.7": {
"integrity": "sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA=="
},
"@libsql/win32-x64-msvc@0.4.7": {
"integrity": "sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw=="
},
"@neon-rs/load@0.0.4": {
"integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="
},
"@noble/ciphers@0.5.3": {
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="
},
"@noble/curves@1.1.0": {
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"dependencies": [
"@noble/hashes@1.3.1"
]
},
"@noble/curves@1.2.0": {
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": [
"@noble/hashes@1.3.2"
]
},
"@noble/curves@1.7.0": {
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
"dependencies": [
"@noble/hashes@1.6.0"
]
},
"@noble/hashes@1.3.1": {
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
},
"@noble/hashes@1.3.2": {
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
},
"@noble/hashes@1.6.0": {
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="
},
"@noble/hashes@1.6.1": {
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="
},
"@noble/secp256k1@2.1.0": {
"integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw=="
},
"@nostr-dev-kit/ndk@2.10.7": {
"integrity": "sha512-cylva8jsaAGMijxAI32CnJWlzvwD4sWyl86/+RMS6xpZn4MIgeVUfBFc/pYkcfZzDP3v1Z9mIPsuiICRyvu9yQ==",
"dependencies": [
"@noble/curves@1.7.0",
"@noble/hashes@1.6.1",
"@noble/secp256k1",
"@scure/base@1.2.1",
"debug@4.3.7",
"light-bolt11-decoder",
"nostr-tools",
"tseep",
"typescript-lru-cache",
"utf8-buffer",
"websocket-polyfill"
]
},
"@scure/base@1.1.1": {
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
},
"@scure/base@1.2.1": {
"integrity": "sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ=="
},
"@scure/bip32@1.3.1": {
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": [
"@noble/curves@1.1.0",
"@noble/hashes@1.3.2",
"@scure/base@1.1.1"
]
},
"@scure/bip32@1.6.0": {
"integrity": "sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA==",
"dependencies": [
"@noble/curves@1.7.0",
"@noble/hashes@1.6.1",
"@scure/base@1.2.1"
]
},
"@scure/bip39@1.2.1": {
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": [
"@noble/hashes@1.3.2",
"@scure/base@1.1.1"
]
},
"@scure/bip39@1.5.0": {
"integrity": "sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A==",
"dependencies": [
"@noble/hashes@1.6.1",
"@scure/base@1.2.1"
]
},
"@types/node@22.5.4": {
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dependencies": [
"undici-types"
]
},
"@types/triple-beam@1.3.5": {
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
},
"@types/ws@8.5.13": {
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"dependencies": [
"@types/node"
]
},
"async@3.2.6": {
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
},
"base32.js@0.1.0": {
"integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ=="
},
"base64-js@1.5.1": {
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"buffer@6.0.3": {
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"dependencies": [
"base64-js",
"ieee754"
]
},
"bufferutil@4.0.8": {
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"dependencies": [
"node-gyp-build"
]
},
"color-convert@1.9.3": {
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": [
"color-name"
]
},
"color-name@1.1.3": {
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"color-string@1.9.1": {
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": [
"color-name",
"simple-swizzle"
]
},
"color@3.2.1": {
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dependencies": [
"color-convert",
"color-string"
]
},
"colorspace@1.1.4": {
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"dependencies": [
"color",
"text-hex"
]
},
"d@1.0.2": {
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"dependencies": [
"es5-ext",
"type"
]
},
"data-uri-to-buffer@4.0.1": {
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
},
"debug@2.6.9": {
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": [
"ms@2.0.0"
]
},
"debug@4.3.7": {
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": [
"ms@2.1.3"
]
},
"detect-libc@2.0.2": {
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="
},
"enabled@2.0.0": {
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"es5-ext@0.10.64": {
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"dependencies": [
"es6-iterator",
"es6-symbol",
"esniff",
"next-tick"
]
},
"es6-iterator@2.0.3": {
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"dependencies": [
"d",
"es5-ext",
"es6-symbol"
]
},
"es6-symbol@3.1.4": {
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"dependencies": [
"d",
"ext"
]
},
"esniff@2.0.1": {
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"dependencies": [
"d",
"es5-ext",
"event-emitter",
"type"
]
},
"event-emitter@0.3.5": {
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"dependencies": [
"d",
"es5-ext"
]
},
"ext@1.7.0": {
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"dependencies": [
"type"
]
},
"fecha@4.2.3": {
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"fetch-blob@3.2.0": {
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"dependencies": [
"node-domexception",
"web-streams-polyfill"
]
},
"fn.name@1.1.0": {
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"formdata-polyfill@4.0.10": {
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": [
"fetch-blob"
]
},
"ieee754@1.2.1": {
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"inherits@2.0.4": {
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ipv6-normalize@1.0.1": {
"integrity": "sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA=="
},
"is-arrayish@0.3.2": {
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"is-stream@2.0.1": {
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
},
"is-typedarray@1.0.0": {
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
},
"js-base64@3.7.7": {
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="
},
"kuler@2.0.0": {
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"libsql@0.4.7": {
"integrity": "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==",
"dependencies": [
"@libsql/darwin-arm64",
"@libsql/darwin-x64",
"@libsql/linux-arm64-gnu",
"@libsql/linux-arm64-musl",
"@libsql/linux-x64-gnu",
"@libsql/linux-x64-musl",
"@libsql/win32-x64-msvc",
"@neon-rs/load",
"detect-libc"
]
},
"light-bolt11-decoder@3.2.0": {
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
"dependencies": [
"@scure/base@1.1.1"
]
},
"logform@2.7.0": {
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
"dependencies": [
"@colors/colors",
"@types/triple-beam",
"fecha",
"ms@2.1.3",
"safe-stable-stringify",
"triple-beam"
]
},
"ms@2.0.0": {
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"next-tick@1.1.0": {
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"node-domexception@1.0.0": {
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
},
"node-fetch@3.3.2": {
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dependencies": [
"data-uri-to-buffer",
"fetch-blob",
"formdata-polyfill"
]
},
"node-gyp-build@4.8.4": {
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="
},
"nodemailer@6.9.15": {
"integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ=="
},
"nostr-tools@2.10.4": {
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
"dependencies": [
"@noble/ciphers",
"@noble/curves@1.2.0",
"@noble/hashes@1.3.1",
"@scure/base@1.1.1",
"@scure/bip32@1.3.1",
"@scure/bip39@1.2.1",
"nostr-wasm"
]
},
"nostr-wasm@0.1.0": {
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="
},
"one-time@1.0.0": {
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"dependencies": [
"fn.name"
]
},
"promise-limit@2.7.0": {
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="
},
"punycode.js@2.3.1": {
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="
},
"readable-stream@3.6.2": {
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": [
"inherits",
"string_decoder",
"util-deprecate"
]
},
"safe-buffer@5.2.1": {
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safe-stable-stringify@2.5.0": {
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
},
"simple-swizzle@0.2.2": {
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": [
"is-arrayish"
]
},
"smtp-server@3.13.6": {
"integrity": "sha512-dqbSPKn3PCq3Gp5hxBM99u7PET7cQSAWrauhtArJbc+zrf5xNEOjm9+Ob3lySySrRoIEvNE0dz+w2H/xWFJNRw==",
"dependencies": [
"base32.js",
"ipv6-normalize",
"nodemailer",
"punycode.js"
]
},
"stack-trace@0.0.10": {
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="
},
"string_decoder@1.3.0": {
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": [
"safe-buffer"
]
},
"text-hex@1.0.0": {
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"triple-beam@1.4.1": {
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="
},
"tseep@1.3.1": {
"integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ=="
},
"tstl@2.5.16": {
"integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw=="
},
"type@2.7.3": {
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
},
"typedarray-to-buffer@3.1.5": {
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"dependencies": [
"is-typedarray"
]
},
"typescript-lru-cache@2.0.0": {
"integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA=="
},
"undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"utf-8-validate@5.0.10": {
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"dependencies": [
"node-gyp-build"
]
},
"utf8-buffer@1.0.0": {
"integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg=="
},
"util-deprecate@1.0.2": {
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"web-streams-polyfill@3.3.3": {
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
},
"websocket-polyfill@0.0.3": {
"integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==",
"dependencies": [
"tstl",
"websocket"
]
},
"websocket@1.0.35": {
"integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
"dependencies": [
"bufferutil",
"debug@2.6.9",
"es5-ext",
"typedarray-to-buffer",
"utf-8-validate",
"yaeti"
]
},
"winston-transport@4.9.0": {
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
"dependencies": [
"logform",
"readable-stream",
"triple-beam"
]
},
"winston@3.17.0": {
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
"dependencies": [
"@colors/colors",
"@dabh/diagnostics",
"async",
"is-stream",
"logform",
"one-time",
"readable-stream",
"safe-stable-stringify",
"stack-trace",
"triple-beam",
"winston-transport"
]
},
"ws@8.18.0": {
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
},
"yaeti@0.0.6": {
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug=="
}
},
"remote": {
"https://deno.land/x/hono@v4.3.11/adapter/deno/serve-static.ts": "db226d30f08f1a8bb77653ead42a911357b2f8710d653e43c01eccebb424b295",
"https://deno.land/x/hono@v4.3.11/client/client.ts": "dcda3887257fa3164db7b32c56665c6e757f0ef047a14f3f9599ef41725c1525",
"https://deno.land/x/hono@v4.3.11/client/index.ts": "30def535310a37bede261f1b23d11a9758983b8e9d60a6c56309cee5f6746ab2",
"https://deno.land/x/hono@v4.3.11/client/utils.ts": "8be84b49c5c7952666875a8e901fde3044c85c853ea6ba3a7e2c0468478459c0",
"https://deno.land/x/hono@v4.3.11/compose.ts": "37d6e33b7db80e4c43a0629b12fd3a1e3406e7d9e62a4bfad4b30426ea7ae4f1",
"https://deno.land/x/hono@v4.3.11/context.ts": "facfd749d823a645039571d66d9d228f5ae6836818b65d3b6c4c6891adfe071e",
"https://deno.land/x/hono@v4.3.11/helper/adapter/index.ts": "ff7e11eb1ca1fbd74ca3c46cd1d24014582f91491ef6d3846d66ed1cede18ec4",
"https://deno.land/x/hono@v4.3.11/helper/cookie/index.ts": "689c84eae410f0444a4598f136a4f859b9122ec6f790dff74412d34405883db8",
"https://deno.land/x/hono@v4.3.11/helper/html/index.ts": "48a0ddc576c10452db6c3cab03dd4ee6986ab61ebdc667335b40a81fa0487f69",
"https://deno.land/x/hono@v4.3.11/hono-base.ts": "fd7e9c1bba1e13119e95158270011784da3a7c3014c149ba0700e700f840ae0d",
"https://deno.land/x/hono@v4.3.11/hono.ts": "23edd0140bf0bd5a68c14ae96e5856a5cec6b844277e853b91025e91ea74f416",
"https://deno.land/x/hono@v4.3.11/http-exception.ts": "f5dd375e61aa4b764eb9b99dd45a7160f8317fd36d3f79ae22585b9a5e8ad7c5",
"https://deno.land/x/hono@v4.3.11/jsx/base.ts": "33f1c302c8f72ae948abd9c3ef85f4b3be6525251a13b95fd18fe2910b7d4a0d",
"https://deno.land/x/hono@v4.3.11/jsx/children.ts": "26ead0f151faba5307883614b5b064299558f06798c695c432f32acbb1127d56",
"https://deno.land/x/hono@v4.3.11/jsx/components.ts": "f79ab215f59388f01a69e2d6ec0b841fd3b42ba38e0ee7c93a525cdf06e159f9",
"https://deno.land/x/hono@v4.3.11/jsx/constants.ts": "984e0797194be1fbc935cb688c8d0a60c112b21bc59301be5354c02232f18820",
"https://deno.land/x/hono@v4.3.11/jsx/context.ts": "2b7a86e6b35da171fab27aa05f09748bb3eba64b26c037ea1da655c07e8f6bc1",
"https://deno.land/x/hono@v4.3.11/jsx/dom/components.ts": "733da654edb3d4c178a4479649fac2c64e79069e37e848add0c3a49f90e7f2d7",
"https://deno.land/x/hono@v4.3.11/jsx/dom/context.ts": "06209d14553398750c69252cc826082018cefa277f5c82cbe58d7261c8a2d81e",
"https://deno.land/x/hono@v4.3.11/jsx/dom/jsx-dev-runtime.ts": "ba87562d14b77dd5f2a3cc30d41b1eb5edb0800e5f4a7337b5b87b2e66f8a099",
"https://deno.land/x/hono@v4.3.11/jsx/dom/jsx-runtime.ts": "6a50a65306771a9000030f494d92a5fdeeb055112e0126234b2fd9179de1d4f5",
"https://deno.land/x/hono@v4.3.11/jsx/dom/render.ts": "7db816d40de58c60e1cbdab64ac3f170b1e30696ed61ad449bbb823f60b46146",
"https://deno.land/x/hono@v4.3.11/jsx/dom/utils.ts": "5d3e8c14996902db9c1223041fb21480fa0e921a4ccdc59f8c7571c08b7810f2",
"https://deno.land/x/hono@v4.3.11/jsx/hooks/index.ts": "b7e0f0a754f31a1e1fbe0ac636b38b031603eb0ae195c32a30769a11d79fb871",
"https://deno.land/x/hono@v4.3.11/jsx/index.ts": "fe3e582c2a4e24e5f8b6027925bddccaae0283747d8f0161eb6f5a34616edd11",
"https://deno.land/x/hono@v4.3.11/jsx/streaming.ts": "5e5dde9a546041353b9a3860fc9020471f762813f10e1290009ab6bd40e7bdcf",
"https://deno.land/x/hono@v4.3.11/jsx/types.ts": "51c2bdbb373860e2570ad403546a7fdbbb1cf00a47ce7ed10b2aece922031ac4",
"https://deno.land/x/hono@v4.3.11/jsx/utils.ts": "4b8299d402ba5395472c552d1fe3297ee60112bfc32e0ef86cfe8e40086f7d54",
"https://deno.land/x/hono@v4.3.11/middleware.ts": "2e7c6062e36b0e5f84b44a62e7b0e1cef33a9827c19937c648be4b63e1b7d7c6",
"https://deno.land/x/hono@v4.3.11/middleware/basic-auth/index.ts": "2c8cb563f3b89df1a7a2232be37377c3df6194af38613dc0a823c6595816fc66",
"https://deno.land/x/hono@v4.3.11/middleware/bearer-auth/index.ts": "b3b7469bc0eb9543c6c47f3ff67de879210dd73063307a61536042ff30e8720e",
"https://deno.land/x/hono@v4.3.11/middleware/body-limit/index.ts": "3fefeaf7e6e576aa1b33f2694072d2eaab692842acd29cb360d98e20eebfe5aa",
"https://deno.land/x/hono@v4.3.11/middleware/cache/index.ts": "5e6273e5c9ea73ef387b25923ab23274c220b29d7c981b62ac0be26d6a1aa3d8",
"https://deno.land/x/hono@v4.3.11/middleware/compress/index.ts": "98c403a5fe7e9c5f5d776350b422b0a125fb34696851b8b14f825b9b7b06f2ac",
"https://deno.land/x/hono@v4.3.11/middleware/cors/index.ts": "976eb9ce8cefc214b403a2939503a13177cec76223274609a07ca554e0dc623b",
"https://deno.land/x/hono@v4.3.11/middleware/csrf/index.ts": "077bb0ce299d79d0d232cb9e462aaa4eaa901164f1310f74a7630f7e6cfe74e8",
"https://deno.land/x/hono@v4.3.11/middleware/etag/index.ts": "95e0270ea349cf00537ee6e58985a4cc7dba44091ca8e2dc42b6d8b2f01bcfe7",
"https://deno.land/x/hono@v4.3.11/middleware/jsx-renderer/index.ts": "229322c66ebc7f426cd2d71f282438025b4ee7ce8cb8e97e87c7efbc94530c19",
"https://deno.land/x/hono@v4.3.11/middleware/jwt/index.ts": "fce4e2db52b4816bfe6bb3a468bd596ab4705527bee1edf679bc28ca53b28ba3",
"https://deno.land/x/hono@v4.3.11/middleware/logger/index.ts": "52a2e968890ada2c11ce89a7a783692c5767b8ed7fb23ccf6b559d255d13ccbc",
"https://deno.land/x/hono@v4.3.11/middleware/method-override/index.ts": "bc13bdcf70c777b72b1300a5cca1b51a8bd126e0d922b991d89e96fe7c694b5b",
"https://deno.land/x/hono@v4.3.11/middleware/powered-by/index.ts": "6faba0cf042278d60b317b690640bb0b58747690cf280fa09024424c5174e66d",
"https://deno.land/x/hono@v4.3.11/middleware/pretty-json/index.ts": "2216ce4c9910be009fecac63367c3626b46137d4cf7cb9a82913e501104b4a88",
"https://deno.land/x/hono@v4.3.11/middleware/secure-headers/index.ts": "f2e4c3858d26ff47bc6909513607e6a3c31184aabe78fb272ed08e1d62a750f0",
"https://deno.land/x/hono@v4.3.11/middleware/serve-static/index.ts": "14b760bbbc4478cc3a7fb9728730bc6300581c890365b7101b80c16e70e4b21e",
"https://deno.land/x/hono@v4.3.11/middleware/timing/index.ts": "6fddbb3e47ae875c16907cf23b9bb503ec2ad858406418b5f38f1e7fbca8c6f6",
"https://deno.land/x/hono@v4.3.11/middleware/trailing-slash/index.ts": "419cf0af99a137f591b72cc71c053e524fe3574393ce81e0e9dbce84a4046e24",
"https://deno.land/x/hono@v4.3.11/mod.ts": "35fd2a2e14b52365e0ad66f168b067363fd0a60d75cbcb1b01685b04de97d60e",
"https://deno.land/x/hono@v4.3.11/request.ts": "7b08602858e642d1626c3106c0bedc2aa8d97e30691a079351d9acef7c5955e6",
"https://deno.land/x/hono@v4.3.11/router.ts": "880316f561918fc197481755aac2165fdbe2f530b925c5357a9f98d6e2cc85c7",
"https://deno.land/x/hono@v4.3.11/router/linear-router/index.ts": "8a2a7144c50b1f4a92d9ee99c2c396716af144c868e10608255f969695efccd0",
"https://deno.land/x/hono@v4.3.11/router/linear-router/router.ts": "928d29894e4b45b047a4f453c7f1745c8b1869cd68447e1cb710c7bbf99a4e29",
"https://deno.land/x/hono@v4.3.11/router/pattern-router/index.ts": "304a66c50e243872037ed41c7dd79ed0c89d815e17e172e7ad7cdc4bc07d3383",
"https://deno.land/x/hono@v4.3.11/router/pattern-router/router.ts": "1b5f68e6af942579d3a40ee834294fea3d1f05fd5f70514e46ae301dd0107e46",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/node.ts": "7efaa6f4301efc2aad0519c84973061be8555da02e5868409293a1fd98536aaf",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/router.ts": "632f2fa426b3e45a66aeed03f7205dad6d13e8081bed6f8d1d987b6cad8fb455",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/trie.ts": "852ce7207e6701e47fa30889a0d2b8bfcd56d8862c97e7bc9831e0a64bd8835f",
"https://deno.land/x/hono@v4.3.11/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef",
"https://deno.land/x/hono@v4.3.11/router/smart-router/router.ts": "dc22a8505a0f345476f07dca3054c0c50a64d7b81c9af5a904476490dfd5cbb4",
"https://deno.land/x/hono@v4.3.11/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41",
"https://deno.land/x/hono@v4.3.11/router/trie-router/node.ts": "d3e00e8f1ba7fb26896459d5bba882356891a07793387c4655d1864c519a91de",
"https://deno.land/x/hono@v4.3.11/router/trie-router/router.ts": "54ced78d35676302c8fcdda4204f7bdf5a7cc907fbf9967c75674b1e394f830d",
"https://deno.land/x/hono@v4.3.11/utils/body.ts": "774cb319dfbe886a9d39f12c43dea15a39f9d01e45de0323167cdd5d0aad14d4",
"https://deno.land/x/hono@v4.3.11/utils/buffer.ts": "2fae689954b427b51fb84ad02bed11a72eae96692c2973802b3b4c1e39cd5b9c",
"https://deno.land/x/hono@v4.3.11/utils/color.ts": "10575c221f48bc806887710da8285f859f51daf9e6878bbdf99cb406b8494457",
"https://deno.land/x/hono@v4.3.11/utils/cookie.ts": "662529d55703d2c0bad8736cb1274eb97524c0ef7882d99254fc7c8fa925b46c",
"https://deno.land/x/hono@v4.3.11/utils/crypto.ts": "bda0e141bbe46d3a4a20f8fbcb6380d473b617123d9fdfa93e4499410b537acc",
"https://deno.land/x/hono@v4.3.11/utils/encode.ts": "311dfdfae7eb0b6345e9680f7ebbb3a692e872ed964e2029aca38567af8d1f33",
"https://deno.land/x/hono@v4.3.11/utils/filepath.ts": "a83e5fe87396bb291a6c5c28e13356fcbea0b5547bad2c3ba9660100ff964000",
"https://deno.land/x/hono@v4.3.11/utils/html.ts": "6ea4f6bf41587a51607dff7a6d2865ef4d5001e4203b07e5c8a45b63a098e871",
"https://deno.land/x/hono@v4.3.11/utils/jwt/index.ts": "3b66f48cdd3fcc2caed5e908ca31776e11b1c30391008931276da3035e6ba1e9",
"https://deno.land/x/hono@v4.3.11/utils/jwt/jwa.ts": "6874cacd8b6dde386636b81b5ea2754f8e4c61757802fa908dd1ce54b91a52fa",
"https://deno.land/x/hono@v4.3.11/utils/jwt/jws.ts": "878fa7d1966b0db20ae231cfee279ba2bb198943e949049cab3f5845cd5ee2d1",
"https://deno.land/x/hono@v4.3.11/utils/jwt/jwt.ts": "80452edc3498c6670a211fdcd33cfc4d5c00dfac79aa9f403b0623dedc039554",
"https://deno.land/x/hono@v4.3.11/utils/jwt/types.ts": "b6659ac85e7f8fcdd8cdfc7d51f5d1a91107ad8dfb647a8e4ea9c80f0f02afee",
"https://deno.land/x/hono@v4.3.11/utils/jwt/utf8.ts": "17c507f68f23ccb82503ea6183e54b5f748a6fe621eb60994adfb4a8c2a3f561",
"https://deno.land/x/hono@v4.3.11/utils/mime.ts": "d1fc2c047191ccb01d736c6acf90df731324536298181dba0ecc2259e5f7d661",
"https://deno.land/x/hono@v4.3.11/utils/url.ts": "855169632c61d03703bd08cafb27664ba3fdb352892f01687d5cce8fd49e3cb1",
"https://deno.land/x/hono@v4.3.11/validator/index.ts": "6c986e8b91dcf857ecc8164a506ae8eea8665792a4ff7215471df669c632ae7c",
"https://deno.land/x/hono@v4.3.11/validator/validator.ts": "53f3d2ad442e22f0bc2d85b7d8d90320d4e5ecf5fdd58882f906055d33a18e13",
"https://git.arx-ccn.com/Arx/ts-utils/raw/commit/c1d309ba097ada64cd072ec1e3e97edaaf8773b6/src/cashu.ts": "9fe2676838da581a051ebc4bcfca78b4b5eb04e05b5fbdd7a487767c16fec6e7",
"https://git.arx-ccn.com/Arx/ts-utils/raw/commit/c1d309ba097ada64cd072ec1e3e97edaaf8773b6/src/email.ts": "ee77141a139894b10bbd0bc68338ebf1a6261a241b97732de547c791e6b0462c",
"https://git.arx-ccn.com/Arx/ts-utils/raw/commit/c1d309ba097ada64cd072ec1e3e97edaaf8773b6/src/general.ts": "692b4c44ec137cf7ef7128337f63a4a96ef8b09057beb7ec9940a6939a615bb4",
"https://git.arx-ccn.com/Arx/ts-utils/raw/commit/c1d309ba097ada64cd072ec1e3e97edaaf8773b6/src/index.ts": "fae9d057707d0632a2d82611e773a9627f199fc038cf823bfc4ecca0cfa0f064",
"https://git.arx-ccn.com/Arx/ts-utils/raw/commit/c1d309ba097ada64cd072ec1e3e97edaaf8773b6/src/nostr.ts": "c72faf0cb4a76a746f141965d366926868b8cbc36bec1b559f921481402521c4"
},
"workspace": {
"dependencies": [
"jsr:@nostr/tools@^2.10.4",
"jsr:@std/assert@1",
"jsr:@std/bytes@^1.0.4",
"jsr:@std/dotenv@~0.225.2",
"jsr:@std/encoding@^1.0.5",
"npm:@cashu/cashu-ts@2",
"npm:@libsql/client@*",
"npm:@nostr-dev-kit/ndk@^2.10.7",
"npm:smtp-server@^3.13.6",
"npm:winston@^3.17.0"
]
}
}

View file

@ -1,34 +0,0 @@
{
"name": "mail-server",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"start": "DEBUG='ndk:*' bun --watch src/index.ts",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev"
},
"dependencies": {
"@arx/utils": "git+ssh://git@git.arx-ccn.com:222/Arx/ts-utils#v0.0.4",
"@elysiajs/cors": "^1.1.1",
"@elysiajs/server-timing": "^1.1.0",
"@elysiajs/swagger": "^1.1.6",
"@libsql/client": "^0.14.0",
"@nostr-dev-kit/ndk": "^2.10.7",
"@prisma/adapter-libsql": "^5.22.0",
"@prisma/client": "5.22.0",
"elysia": "^1.1.25",
"node-forge": "^1.3.1",
"smtp-server": "^3.13.6",
"websocket-polyfill": "^1.0.0",
"winston": "^3.17.0"
},
"devDependencies": {
"@types/node-forge": "^1.3.11",
"@types/smtp-server": "^3.5.10",
"bun-types": "latest",
"prisma": "5.22.0",
"typescript": "^5.7.2"
},
"private": true
}

View file

@ -1,29 +0,0 @@
-- CreateTable
CREATE TABLE "users" (
"npub" TEXT NOT NULL PRIMARY KEY,
"registeredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastPayment" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"subscriptionDuration" INTEGER
);
-- CreateTable
CREATE TABLE "aliases" (
"npub" TEXT NOT NULL,
"alias" TEXT NOT NULL,
PRIMARY KEY ("npub", "alias"),
CONSTRAINT "aliases_npub_fkey" FOREIGN KEY ("npub") REFERENCES "users" ("npub") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "mail_queue" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"alias" TEXT NOT NULL,
"sender" TEXT NOT NULL,
"data" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "mail_queue_alias_fkey" FOREIGN KEY ("alias") REFERENCES "aliases" ("alias") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "aliases_alias_key" ON "aliases"("alias");

View file

@ -1,10 +0,0 @@
/*
Warnings:
- You are about to drop the `mail_queue` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "mail_queue";
PRAGMA foreign_keys=on;

View file

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

View file

@ -1,28 +0,0 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = env("DB_URL")
}
model User {
npub String @id
registeredAt DateTime @default(now())
lastPayment DateTime @default(now())
subscriptionDuration Int?
aliases Alias[]
@@map("users")
}
model Alias {
npub String
alias String @unique
user User @relation(fields: [npub], references: [npub])
@@id([npub, alias])
@@map("aliases")
}

View file

@ -1,247 +1,241 @@
import {Context as HonoContext, Hono,} from "https://deno.land/x/hono@v4.3.11/mod.ts";
import {cors} from "https://deno.land/x/hono@v4.3.11/middleware.ts";
import {CashuMint, CashuWallet, getEncodedToken} from "@cashu/cashu-ts"; import {CashuMint, CashuWallet, getEncodedToken} from "@cashu/cashu-ts";
import {logger} from "./utils"; import {getAlias, getUserByNpub} from "./models.ts";
import {logger} from "./utils/index.ts";
import * as nip98 from "nostr-tools/nip98"; import * as nip98 from "nostr-tools/nip98";
import {Elysia, t} from "elysia"; import {Client as LibSQL} from "@libsql/client";
import {swagger} from "@elysiajs/swagger"; import {npubToPubKeyString, pubKeyStringToNpub, TokenInfoWithMailSubscriptionDuration,} from "@arx/utils";
import {serverTiming} from "@elysiajs/server-timing";
import {PrismaClient} from "@prisma/client";
import {TokenInfoWithMailSubscriptionDuration} from "@arx/utils/cashu.ts";
import {npubToPubKeyString, pubKeyStringToNpub} from "@arx/utils/nostr.ts";
import cors from "@elysiajs/cors";
const npubType = t.String({ const NPUB_REGEX = /^npub1[023456789acdefghjklmnpqrstuvwxyz]{58}$/;
pattern: `^npub1[023456789acdefghjklmnpqrstuvwxyz]{58}$`, const CASHU_REGEX = /^cashu[A-Za-z0-9+-_]*={0,3}$/;
error: 'Invalid npub format'
});
const cashuTokenType = t.String({
pattern: '^cashu[A-Za-z0-9+-_]*={0,3}$',
error: 'Invalid Cashu token format'
})
export class HttpServer { export class HttpServer {
constructor(private db: PrismaClient, port: number) { private app: Hono;
new Elysia()
.use(swagger({ constructor(private db: LibSQL, port: number) {
documentation: { this.app = new Hono();
info: {
title: 'npub.email Documentation', this.app
version: '0.0.1' .use("*", cors())
} .get("/", (c: HonoContext) => c.text("nostr.email server"))
} .get("/subscription/:npub", this.getSubscriptionForNpub)
})) .get("/aliases/:npub", this.getAliasesForNpub)
.use(serverTiming()) .get("/alias/:alias", this.getNpubForAlias)
.use(cors()) .post("/addAlias", this.addAlias)
.get('/', 'nostr.email server') .post("/addTime/:npub", this.addTimeToNpub);
.get('/subscription/:npub', this.getSubscriptionForNpub, {
params: t.Object({ Deno.serve({ port }, this.app.fetch);
npub: npubType
})
})
.get('/aliases/:npub', this.getAliasesForNpub, {
params: t.Object({
npub: npubType,
}),
})
.get('/alias/:alias', this.getNpubForAlias, {
params: t.Object({
alias: t.String(),
}),
})
.post('/addAlias', this.addAlias, {
body: t.Object({
alias: t.String()
})
})
.post('/addTime/:npub', this.addTimeToNpub, {
params: t.Object({
npub: npubType,
}),
body: t.Object({
tokenString: cashuTokenType
})
})
.listen(port)
logger.info(`HTTP Server running on port ${port}`); logger.info(`HTTP Server running on port ${port}`);
} }
getSubscriptionForNpub = async ({params: {npub}}: { getSubscriptionForNpub = async (c: HonoContext) => {
params: { const npub = c.req.param("npub");
npub: string if (!NPUB_REGEX.test(npub)) {
} return c.json({ error: "Invalid npub format" }, 400);
}) => {
const user = await this.db.user.findFirst({
where: {
npub
},
include: {
aliases: true
} }
const user = await getUserByNpub(this.db, npub);
if (!user) {
return c.json({
subscribed: false,
}); });
if (!user) return { }
subscribed: false return c.json({
};
return {
subscribed: true, subscribed: true,
subscribedUntil: user.subscriptionDuration == null ? Infinity : Math.floor(user.lastPayment.getTime() / 1000) + user.subscriptionDuration subscribedUntil: user.subscriptionDuration == null
? Infinity
: Math.floor(user.lastPayment.getTime() / 1000) +
user.subscriptionDuration,
});
}; };
getNpubForAlias = async (c: HonoContext) => {
const aliasParam = c.req.param("alias");
const alias = await getAlias(this.db, aliasParam);
if (!alias) {
return c.json({ error: "Not found" }, 404);
}
return c.json({ npub: alias.npub });
};
getAliasesForNpub = async (c: HonoContext) => {
const npub = c.req.param("npub");
if (!NPUB_REGEX.test(npub)) {
return c.json({ error: "Invalid npub format" }, 400);
} }
getNpubForAlias = async ({params: {alias}}: { try {
params: { const unpacked = await this.getUnpackedAuthHeader(
alias: string c.req.header("Authorization"),
} `/aliases/${npub}`,
}) => { );
const user = await this.db.user.findFirst({
where: {
aliases: {
some: {
alias
}
}
}
});
if (!user) return new Response('Not found', {
status: 404
});
return user.npub;
}
getAliasesForNpub = async ({params: {npub}, headers}: {
params: {
npub: string
},
headers: Record<string, string | undefined>
}) => {
const unpacked = await this.getUnpackedAuthHeader(headers, `/aliases/${npub}`);
const npubAsPubkey = npubToPubKeyString(npub); const npubAsPubkey = npubToPubKeyString(npub);
if (unpacked.pubkey !== npubAsPubkey)
return new Response('Unauthorized', { if (unpacked.pubkey !== npubAsPubkey) {
status: 401 return c.json({ error: "Unauthorized" }, 401);
})
const user = await this.db.user.findFirst({
where: {
npub
},
include: {
aliases: true
}
});
if (!user) return new Response('Not found', {
status: 404
});
return user.aliases.map(alias => alias.alias);
} }
addAlias = async ({body: {alias}, headers}: { const user = await getUserByNpub(this.db, npub);
body: {
alias: string if (!user) {
}, return c.json({ error: "Not found" }, 404);
headers: Record<string, string | undefined> }
}) => {
const unpacked = await this.getUnpackedAuthHeader(headers, '/addAlias'); return c.json(user.aliases);
} catch (error) {
if (error instanceof Error) {
return c.json({ error: error.message }, 401);
} else {
return c.json({ error: `${error}` }, 401);
}
}
};
addAlias = async (c: HonoContext) => {
const { alias } = await c.req.json<{
alias: string;
}>();
try {
const unpacked = await this.getUnpackedAuthHeader(
c.req.header("Authorization"),
"/addAlias",
);
const unpackedKeyToNpub = pubKeyStringToNpub(unpacked.pubkey); const unpackedKeyToNpub = pubKeyStringToNpub(unpacked.pubkey);
const userInDb = await this.db.user.findFirst({
where: {
npub: unpackedKeyToNpub
}
});
if (!userInDb) return new Response('Unauthorized', {
status: 401
});
const stillHasSubscription = userInDb.subscriptionDuration === null || Math.floor(userInDb.lastPayment.getTime() / 1000) + userInDb.subscriptionDuration > Date.now() / 1000; const user = await getUserByNpub(this.db, unpackedKeyToNpub);
if (!stillHasSubscription) return new Response('User has no subscription', {
status: 400 if (!user) {
}); return c.json({ error: "Unauthorized" }, 401);
const aliasInDb = await this.db.alias.findFirst({
where: {
alias
}
});
if (aliasInDb) return new Response('Alias already exists', {
status: 400
});
return this.db.user.update({
where: {
npub: unpackedKeyToNpub
},
data: {
aliases: {
create: {
alias
}
}
}
});
} }
addTimeToNpub = async ({params: {npub}, body: {tokenString}}: { const stillHasSubscription = user.subscriptionDuration === null ||
params: { Math.floor(user.lastPayment.getTime() / 1000) +
npub: string user.subscriptionDuration > Date.now() / 1000;
},
body: { if (!stillHasSubscription) {
tokenString: string return c.json({ error: "User has no subscription" }, 400);
} }
}) => {
const userInDb = await this.db.user.findFirst({ const aliasInDb = await getAlias(this.db, alias);
where: {
npub if (aliasInDb) {
return c.json({ error: "Alias already exists" }, 400);
} }
await this.db.execute({
sql: `
INSERT INTO aliases (alias, npub)
VALUES ($alias, $npub)
ON CONFLICT (alias) DO NOTHING
`,
args: { alias, npub: unpackedKeyToNpub },
}); });
if (userInDb && (userInDb.subscriptionDuration === null || userInDb.subscriptionDuration === -1)) return c.json({ alias, npub: unpackedKeyToNpub });
return new Response('User has unlimited subscription', { } catch (error) {
status: 400 if (error instanceof Error) {
}) return c.json({ error: error.message }, 401);
} else {
return c.json({ error: `${error}` }, 401);
}
}
};
addTimeToNpub = async (c: HonoContext) => {
const npub = c.req.param("npub");
const { tokenString } = await c.req.json();
if (!NPUB_REGEX.test(npub)) {
return c.json({ error: "Invalid npub format" }, 400);
}
if (!CASHU_REGEX.test(tokenString)) {
return c.json({ error: "Invalid Cashu token format" }, 400);
}
const user = await getUserByNpub(this.db, npub);
if (
user &&
(user.subscriptionDuration === null ||
user.subscriptionDuration === -1)
) {
return c.json({ error: "User has unlimited subscription" }, 400);
}
const tokenInfo = new TokenInfoWithMailSubscriptionDuration(tokenString); const tokenInfo = new TokenInfoWithMailSubscriptionDuration(tokenString);
const mint = new CashuMint(tokenInfo.mint); const mint = new CashuMint(tokenInfo.mint);
const wallet = new CashuWallet(mint); const wallet = new CashuWallet(mint);
const newToken = await wallet.receive(tokenString); const newToken = await wallet.receive(tokenString);
const encodedToken = getEncodedToken({ const encodedToken = getEncodedToken({
token: [{
mint: tokenInfo.mint, mint: tokenInfo.mint,
proofs: newToken proofs: newToken,
}]
}); });
logger.info(`New cashu token: ${encodedToken}`); logger.info(`New cashu token: ${encodedToken}`);
if (userInDb) { if (user) {
let timeRemaining = Math.max(0, Math.floor((+new Date(userInDb.lastPayment.getTime() + userInDb.subscriptionDuration! * 1000) - +new Date()) / 1000)); let timeRemaining = Math.max(
0,
Math.floor(
(+new Date(
user.lastPayment.getTime() +
user.subscriptionDuration! * 1000,
) - +new Date()) / 1000,
),
);
timeRemaining += tokenInfo.duration; timeRemaining += tokenInfo.duration;
await this.db.user.update({ await this.db.execute({
where: { sql: `
npub UPDATE users
}, SET lastPayment = $lastPayment, subscriptionDuration = $subscriptionDuration
data: { WHERE npub = $npub
`,
args: {
lastPayment: new Date(), lastPayment: new Date(),
subscriptionDuration: timeRemaining subscriptionDuration: timeRemaining,
} npub,
},
});
return c.json({
newTimeRemaining: timeRemaining,
}); });
return {
newTimeRemaining: timeRemaining
} }
} await this.db.execute({
await this.db.user.create({ sql: `
data: { INSERT INTO users (npub, registeredAt, lastPayment, subscriptionDuration)
VALUES ($npub, $registeredAt, $lastPayment, $subscriptionDuration)
`,
args: {
npub, npub,
registeredAt: new Date(), registeredAt: new Date(),
lastPayment: new Date(), lastPayment: new Date(),
subscriptionDuration: tokenInfo.duration subscriptionDuration: tokenInfo.duration,
} },
}); });
return { return c.json({
newTimeRemaining: tokenInfo.duration newTimeRemaining: tokenInfo.duration,
} });
} };
private getUnpackedAuthHeader = async (headers: Record<string, string | undefined>, url: string) => { private getUnpackedAuthHeader = async (
if (!headers.authorization) auth: string | undefined,
throw new Error('Unauthorized'); url: string,
const authHeader = headers.authorization.split(' ')[1]; ) => {
const validate = await nip98.validateToken(authHeader, `${process.env.PUBLIC_API_BASE_URL!}${url}`, "POST"); if (!auth) {
if (!validate) throw new Error("Unauthorized");
throw new Error('Unauthorized');
return await nip98.unpackEventFromToken(authHeader);
} }
const authHeader = auth.split(" ")[1];
const validate = await nip98.validateToken(
authHeader,
`${Deno.env.get("PUBLIC_API_BASE_URL")!}${url}`,
"POST",
);
if (!validate) {
throw new Error("Unauthorized");
}
return await nip98.unpackEventFromToken(authHeader);
};
} }

View file

@ -1,26 +1,34 @@
import {createClient as createLibSQLClient} from "@libsql/client"; import {NostrSmtpServer} from "./smtpServer.ts";
import "websocket-polyfill"; import {HttpServer} from "./httpServer.ts";
import {PrismaClient} from "@prisma/client"; import {createClient as createDB} from "@libsql/client/sqlite3";
import {PrismaLibSQL} from "@prisma/adapter-libsql"; import "@std/dotenv/load";
import {NostrSmtpServer} from "./smtpServer";
import {HttpServer} from "./httpServer";
if (!process.env.BASE_DOMAIN) const requiredEnvVars = [
throw new Error("BASE_DOMAIN is not set"); "BASE_DOMAIN",
if (!process.env.DB_URL) "DB_URL",
throw new Error("DB_URL is not set"); "PUBLIC_API_BASE_URL",
if (!process.env.PUBLIC_API_BASE_URL) "MASTER_NSEC",
throw new Error("PUBLIC_API_BASE_URL is not set"); ];
if (!process.env.MASTER_NSEC)
throw new Error("MASTER_NSEC is not set");
const dbClient = createLibSQLClient({ for (const envVar of requiredEnvVars) {
url: process.env.DB_URL, if (!Deno.env.has(envVar)) {
throw new Error(`${envVar} is not set`);
}
}
export const db = createDB({
url: Deno.env.get("DB_URL")!,
}); });
const db = new PrismaClient({ new NostrSmtpServer(
adapter: new PrismaLibSQL(dbClient) db,
}); parseInt(
Deno.env.get("SMTP_PORT") || "6587",
new NostrSmtpServer(db, parseInt(process.env.SMTP_PORT || '6587')); ),
new HttpServer(db, parseInt(process.env.HTTP_PORT || '3000')); );
new HttpServer(
db,
parseInt(
Deno.env.get("HTTP_PORT") || "3000",
),
);

113
src/models.ts Normal file
View file

@ -0,0 +1,113 @@
import {Client as LibSQL} from "@libsql/client";
type User = {
npub: string;
registeredAt: Date;
lastPayment: Date;
subscriptionDuration: number | null;
};
type Alias = {
alias: string;
npub: string;
};
type UserWithAliases = User & {
aliases: string[];
};
export type AliasRowResult = {
rows: Array<{
alias: string;
npub: string;
}>;
columns: string[];
rowsAffected: number;
};
export type UserRowResult = {
rows: Array<{
npub: string;
registeredAt: Date;
lastPayment: Date;
subscriptionDuration: number | null;
alias: string | null;
}>;
columns: string[];
rowsAffected: number;
};
export const mapSingleAlias = (rows: AliasRowResult["rows"]): Alias | null => {
if (rows.length === 0) {
return null;
}
const firstRow = rows[0];
return {
alias: firstRow.alias,
npub: firstRow.npub,
};
};
export const mapSingleUserWithAliases = (
rows: UserRowResult["rows"],
): UserWithAliases | null => {
if (rows.length === 0) {
return null;
}
const firstRow = rows[0];
return {
npub: firstRow.npub,
registeredAt: firstRow.registeredAt,
lastPayment: new Date(firstRow.lastPayment),
subscriptionDuration: firstRow.subscriptionDuration,
aliases: rows
.filter((row) => row.alias !== null)
.map((row) => row.alias!),
};
};
export const getUserByNpub = async (
db: LibSQL,
npub: string,
): Promise<UserWithAliases | null> => {
const result: UserRowResult = await db.execute({
sql: `
SELECT u.*, a.*
FROM users u LEFT JOIN aliases a ON u.npub = a.npub
WHERE u.npub = $npub;
`,
args: { npub },
}) as any as UserRowResult;
return mapSingleUserWithAliases(result.rows);
};
export const getUserByAlias = async (
db: LibSQL,
alias: string,
): Promise<UserWithAliases | null> => {
const result: UserRowResult = await db.execute({
sql: `
SELECT u.*, a.*
FROM users u LEFT JOIN aliases a ON u.npub = a.npub
WHERE a.alias = $alias;
`,
args: { alias },
}) as any as UserRowResult;
return mapSingleUserWithAliases(result.rows);
};
export const getAlias = async (db: LibSQL, aliasParam: string) => {
const result: AliasRowResult = await db.execute({
sql: `
SELECT *
FROM aliases
WHERE alias = $alias
LIMIT 1;
`,
args: { alias: aliasParam },
}) as any as AliasRowResult;
return mapSingleAlias(result.rows);
};

View file

@ -1,10 +1,12 @@
import {SMTPServer, SMTPServerAddress, SMTPServerDataStream, SMTPServerSession} from "smtp-server"; import {SMTPServer, SMTPServerAddress, SMTPServerDataStream, SMTPServerSession,} from "smtp-server";
import {deriveNsecForEmail, getNDK, logger} from "./utils"; import {deriveNsecForEmail, getNDK, getUserByAlias, logger,} from "./utils/index.ts";
import {NDKEvent, NDKKind, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk"; import {NDKEvent, NDKKind, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk";
import {PrismaClient} from "@prisma/client"; import {Client as LibSQL} from "@libsql/client";
import {encryptEventForRecipient, parseEmail} from "@arx/utils"; import {encryptEventForRecipient, parseEmail} from "@arx/utils";
import * as path from "node:path"; import {concat} from "@std/bytes";
import fs from 'node:fs/promises'; import path from "node:path";
import fs from "node:fs/promises";
import process from "node:process";
interface QueuedEmail { interface QueuedEmail {
id: string; id: string;
@ -19,20 +21,24 @@ export class NostrSmtpServer {
private emailQueue: QueuedEmail[] = []; private emailQueue: QueuedEmail[] = [];
private isProcessing: boolean = false; private isProcessing: boolean = false;
private readonly MAX_RETRIES = 3; private readonly MAX_RETRIES = 3;
private readonly BACKUP_DIR = path.join(process.cwd(), 'email-backups'); private readonly BACKUP_DIR = path.join(Deno.cwd(), "email-backups");
constructor(private db: PrismaClient, port: number) { constructor(private db: LibSQL, port: number) {
this.server = new SMTPServer({ this.server = new SMTPServer({
authOptional: true, authOptional: true,
logger: false, logger: false,
onData: (stream, session, callback) => this.handleEmailData(stream, session, callback, db) onData: (
stream: SMTPServerDataStream,
session: SMTPServerSession,
callback: () => void,
) => this.handleEmailData(stream, session, callback),
}); });
this.server.listen(port, '0.0.0.0'); this.server.listen(port, "0.0.0.0");
logger.info(`SMTP Server running on port ${port}`); logger.info(`SMTP Server running on port ${port}`);
fs.mkdir(this.BACKUP_DIR, {recursive: true}).catch(err => { fs.mkdir(this.BACKUP_DIR, { recursive: true }).catch((err) => {
logger.error('Failed to create backup directory:', err); logger.error("Failed to create backup directory:", err);
}); });
this.setupGracefulShutdown(); this.setupGracefulShutdown();
@ -43,7 +49,9 @@ export class NostrSmtpServer {
const files = await fs.readdir(this.BACKUP_DIR); const files = await fs.readdir(this.BACKUP_DIR);
for (const file of files) { for (const file of files) {
try { try {
const data = JSON.parse(await fs.readFile(path.join(this.BACKUP_DIR, file), 'utf-8')); const data = JSON.parse(
await fs.readFile(path.join(this.BACKUP_DIR, file), "utf-8"),
);
this.emailQueue.push(data); this.emailQueue.push(data);
} catch (error) { } catch (error) {
logger.error(`Failed to recover backup ${file}:`, error); logger.error(`Failed to recover backup ${file}:`, error);
@ -54,71 +62,78 @@ export class NostrSmtpServer {
private setupGracefulShutdown(): void { private setupGracefulShutdown(): void {
const shutdown = async () => { const shutdown = async () => {
logger.info('Graceful shutdown initiated'); logger.info("Graceful shutdown initiated");
this.server.close(); this.server.close();
while (this.isProcessing) while (this.isProcessing) {
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
}
for (const email of this.emailQueue) for (const email of this.emailQueue) {
await this.backupEmail(email); await this.backupEmail(email);
}
logger.info('Graceful shutdown completed'); logger.info("Graceful shutdown completed");
process.exit(0); process.exit(0);
}; };
process.on('SIGTERM', shutdown); process.on("SIGTERM", shutdown);
process.on('SIGINT', shutdown); process.on("SIGINT", shutdown);
} }
private async handleEmailData(stream: SMTPServerDataStream, session: SMTPServerSession, callback: () => void, db: PrismaClient) { private async handleEmailData(
const chunks: Buffer[] = []; stream: SMTPServerDataStream,
session: SMTPServerSession,
callback: () => void,
) {
const chunks: Uint8Array[] = [];
stream.on('data', (chunk: Buffer) => { try {
for await (const chunk of stream) {
chunks.push(chunk); chunks.push(chunk);
}); }
stream.on('end', async () => {
if (!this.validateSender(session)) { if (!this.validateSender(session)) {
callback(); callback();
return; return;
} }
const mailData = Buffer.concat(chunks).toString(); const mailData = new TextDecoder().decode(
concat(chunks),
);
try {
const queuedEmail: QueuedEmail = { const queuedEmail: QueuedEmail = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, id: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
mailData, mailData,
session, session,
attempts: 0, attempts: 0,
createdAt: Date.now() createdAt: Date.now(),
}; };
this.emailQueue.push(queuedEmail); this.emailQueue.push(await this.backupEmail(queuedEmail));
await this.backupEmail(queuedEmail);
this.processQueue(); this.processQueue();
} catch (e) { } catch (e) {
logger.error(`Error processing recipients: ${e}`, e); logger.error(`Error processing recipients: ${e}`, e);
} }
callback(); callback();
});
} }
private async backupEmail(email: QueuedEmail): Promise<void> { private async backupEmail(email: QueuedEmail): Promise<QueuedEmail> {
try { try {
const backupPath = path.join(this.BACKUP_DIR, `${email.id}.json`); const backupPath = path.join(this.BACKUP_DIR, `${email.id}.json`);
await fs.writeFile(backupPath, JSON.stringify(email)); await fs.writeFile(backupPath, JSON.stringify(email));
} catch (error) { } catch (error) {
logger.error('Failed to backup email:', error); logger.error("Failed to backup email:", error);
} }
return JSON.parse(JSON.stringify(email)) as QueuedEmail; // returning the email because of a weird bug with smtp-server
} }
private async processQueue(): Promise<void> { private async processQueue(): Promise<void> {
if (this.isProcessing) if (this.isProcessing) {
return; return;
}
this.isProcessing = true; this.isProcessing = true;
while (this.emailQueue.length > 0) { while (this.emailQueue.length > 0) {
const email = this.emailQueue[0]; const email = this.emailQueue[0];
@ -128,7 +143,9 @@ export class NostrSmtpServer {
} }
try { try {
const parsedEmail: ReturnType<typeof parseEmail> = parseEmail(email.mailData); const parsedEmail: ReturnType<typeof parseEmail> = parseEmail(
email.mailData,
);
await this.processRecipients(email.session, parsedEmail, this.db); await this.processRecipients(email.session, parsedEmail, this.db);
// Remove from queue and delete backup if successful // Remove from queue and delete backup if successful
@ -149,77 +166,93 @@ export class NostrSmtpServer {
const backupPath = path.join(this.BACKUP_DIR, `${id}.json`); const backupPath = path.join(this.BACKUP_DIR, `${id}.json`);
await fs.unlink(backupPath); await fs.unlink(backupPath);
} catch (error) { } catch (error) {
logger.error(`[TOXIC DATA!!!] Failed to delete email backup ${id}.json:`, error); logger.error(
`[TOXIC DATA!!!] Failed to delete email backup ${id}.json:`,
error,
);
} }
} }
private validateSender(session: SMTPServerSession): boolean { private validateSender(session: SMTPServerSession): boolean {
if (!session.envelope.mailFrom) { if (!session.envelope.mailFrom) {
logger.warn('Ignoring email without sender'); logger.warn("Ignoring email without sender");
return false; return false;
} }
return true; return true;
} }
private async processRecipients(session: SMTPServerSession, parsedEmail: ReturnType<typeof parseEmail>, db: PrismaClient) { private async processRecipients(
session: SMTPServerSession,
parsedEmail: ReturnType<typeof parseEmail>,
db: LibSQL,
) {
for (const recipientEmail of session.envelope.rcptTo) { for (const recipientEmail of session.envelope.rcptTo) {
const address = recipientEmail.address; const address = recipientEmail.address;
const [alias, domain] = address.split('@'); const [alias, domain] = address.split("@");
if (domain !== process.env.BASE_DOMAIN) { if (domain !== Deno.env.get("BASE_DOMAIN")) {
logger.warn(`Not sending email to ${address} because it is not in the allowed domain`); logger.warn(
`Not sending email to ${address} because it is not in the allowed domain`,
);
continue; continue;
} }
const user = await this.getUser(alias, db); const user = await this.getUser(alias, db);
if (!user || !this.isSubscriptionValid(user)) continue; if (!user || !this.isSubscriptionValid(user)) return;
await this.sendNostrLetter(session, parsedEmail, user.npub); await this.sendNostrLetter(session, parsedEmail, user.npub);
} }
} }
private async getUser(alias: string, db: PrismaClient) { private async getUser(alias: string, db: LibSQL) {
const user = await db.alias.findUnique({ const user = await getUserByAlias(db, alias);
where: {alias},
include: {user: true}
});
if (!user) { if (!user) {
logger.warn('No user found for', alias, 'skipping'); logger.warn("No user found for", alias, "skipping");
return null; return null;
} }
return user; return user;
} }
private isSubscriptionValid(user: NonNullable<Awaited<ReturnType<NostrSmtpServer['getUser']>>>): boolean { private isSubscriptionValid(
user: NonNullable<Awaited<ReturnType<NostrSmtpServer["getUser"]>>>,
): boolean {
// If there's no duration set, it's an unlimited subscription // If there's no duration set, it's an unlimited subscription
if (user.user.subscriptionDuration === null) if (user.subscriptionDuration === null) {
return true; return true;
}
const subscriptionDurationMs = user.user.subscriptionDuration * 1000; const subscriptionDurationMs = user.subscriptionDuration * 1000;
const lastPaymentTimestamp = user.user.lastPayment.getTime(); const lastPaymentTimestamp = user.lastPayment.getTime();
const currentTimestamp = Date.now(); const currentTimestamp = Date.now();
const subscriptionEndTime = lastPaymentTimestamp + subscriptionDurationMs; const subscriptionEndTime = lastPaymentTimestamp + subscriptionDurationMs;
const timeRemaining = subscriptionEndTime - currentTimestamp; const timeRemaining = subscriptionEndTime - currentTimestamp;
if (timeRemaining <= 0) { if (timeRemaining <= 0) {
logger.warn(`Subscription has expired for ${user.alias}`); logger.warn(`Subscription has expired for ${user.npub}`);
return false; return false;
} }
return true; return true;
} }
private async sendNostrLetter(session: SMTPServerSession, parsedEmail: ReturnType<typeof parseEmail>, recipient: string) { private async sendNostrLetter(
session: SMTPServerSession,
parsedEmail: ReturnType<typeof parseEmail>,
recipient: string,
) {
const randomKeySinger = new NDKPrivateKeySigner( const randomKeySinger = new NDKPrivateKeySigner(
deriveNsecForEmail(process.env.MASTER_NSEC!, (session.envelope.mailFrom as SMTPServerAddress).address) deriveNsecForEmail(
Deno.env.get("MASTER_NSEC")!,
(session.envelope.mailFrom as SMTPServerAddress).address,
),
); );
const ndk = getNDK(); const ndk = getNDK();
ndk.signer = randomKeySinger; ndk.signer = randomKeySinger;
await ndk.connect(); await ndk.connect();
const ndkUser = ndk.getUser({npub: recipient}); const ndkUser = ndk.getUser({ npub: recipient });
const randomKeyUser = await randomKeySinger.user(); const randomKeyUser = await randomKeySinger.user();
const event = new NDKEvent(); const event = new NDKEvent();
event.kind = NDKKind.Article; event.kind = NDKKind.Article;
@ -227,17 +260,17 @@ export class NostrSmtpServer {
event.created_at = Math.floor(Date.now() / 1000); event.created_at = Math.floor(Date.now() / 1000);
event.pubkey = randomKeyUser.pubkey; event.pubkey = randomKeyUser.pubkey;
event.tags.push( event.tags.push(
['p', ndkUser.pubkey], ["p", ndkUser.pubkey],
['subject', parsedEmail.subject], ["subject", parsedEmail.subject],
['email:localIP', session.localAddress], ["email:localIP", session.localAddress],
['email:remoteIP', session.remoteAddress], ["email:remoteIP", session.remoteAddress],
['email:isEmail', 'true'], ["email:isEmail", "true"],
['email:session', session.id], ["email:session", session.id],
['email:from', (session.envelope.mailFrom as SMTPServerAddress).address] ["email:from", (session.envelope.mailFrom as SMTPServerAddress).address],
); );
for (const to of session.envelope.rcptTo) { for (const to of session.envelope.rcptTo) {
event.tags.push(['email:to', to.address]); event.tags.push(["email:to", to.address]);
} }
for (const header of Object.keys(parsedEmail.headers)) { for (const header of Object.keys(parsedEmail.headers)) {

View file

@ -1,15 +1,16 @@
import NDK from "@nostr-dev-kit/ndk"; import NDK from "@nostr-dev-kit/ndk";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import {decodeHex} from "@std/encoding/hex";
export * from "./logs"; export * from "./logs.ts";
export * from "../models.ts";
export function getNDK() { export function getNDK() {
return new NDK({ return new NDK({
explicitRelayUrls: [ explicitRelayUrls: [
'wss://relay.primal.net', "wss://relay.primal.net",
'wss://relay.damus.io', "wss://relay.nostr.band",
'wss://relay.nostr.band', "wss://offchain.pub",
'wss://offchain.pub'
], ],
autoConnectUserRelays: false, autoConnectUserRelays: false,
enableOutboxModel: true, enableOutboxModel: true,
@ -27,9 +28,16 @@ export function getNDK() {
* @param email - The email address. * @param email - The email address.
* @returns The nostr private key derived from the master key and email address as a uint8array. * @returns The nostr private key derived from the master key and email address as a uint8array.
*/ */
export function deriveNsecForEmail(masterNsec: string, email: string): Uint8Array { export function deriveNsecForEmail(
const masterNsecHash = crypto.createHash('sha256').update(masterNsec).digest('hex'); masterNsec: string,
const emailHash = crypto.createHash('sha256').update(email).digest('hex'); email: string,
const sharedSecret = crypto.createHash('sha256').update(masterNsecHash + emailHash).digest('hex'); ): Uint8Array {
return Uint8Array.from(Buffer.from(sharedSecret, 'hex')); const masterNsecHash = crypto.createHash("sha256").update(masterNsec).digest(
"hex",
);
const emailHash = crypto.createHash("sha256").update(email).digest("hex");
const sharedSecret = crypto.createHash("sha256").update(
masterNsecHash + emailHash,
).digest("hex");
return decodeHex(sharedSecret);
} }

View file

@ -1,22 +1,22 @@
import winston from "winston"; import winston from "winston";
const {combine, timestamp, printf, align, colorize, json} = winston.format; const { combine, timestamp, printf, align, colorize, json } = winston.format;
export const logger = winston.createLogger({ export const logger = winston.createLogger({
level: 'info', level: "info",
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
format: combine( format: combine(
colorize({all: true}), colorize({ all: true }),
timestamp({ timestamp({
format: 'YYYY-MM-DD hh:mm:ss.SSS A', format: "YYYY-MM-DD hh:mm:ss.SSS A",
}), }),
align(), align(),
printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`) printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`),
), ),
}), }),
new winston.transports.File({ new winston.transports.File({
filename: process.env.LOG_FILE || '/tmp/nostr-email.log', filename: Deno.env.get("LOG_FILE") || "/tmp/nostr-email.log",
format: combine(timestamp(), json()), format: combine(timestamp(), json()),
}), }),
], ],