diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index af68f95..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..a70a678 --- /dev/null +++ b/deno.json @@ -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" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..4154e3b --- /dev/null +++ b/deno.lock @@ -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" + ] + } +} diff --git a/package.json b/package.json deleted file mode 100644 index 582753b..0000000 --- a/package.json +++ /dev/null @@ -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 -} diff --git a/prisma/migrations/20241125122247_init/migration.sql b/prisma/migrations/20241125122247_init/migration.sql deleted file mode 100644 index aa6b44d..0000000 --- a/prisma/migrations/20241125122247_init/migration.sql +++ /dev/null @@ -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"); diff --git a/prisma/migrations/20241126193747_remove_mail_queue/migration.sql b/prisma/migrations/20241126193747_remove_mail_queue/migration.sql deleted file mode 100644 index a0e1043..0000000 --- a/prisma/migrations/20241126193747_remove_mail_queue/migration.sql +++ /dev/null @@ -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; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index e5e5c47..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 42ccefa..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -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") -} diff --git a/src/httpServer.ts b/src/httpServer.ts index e56b85e..997b897 100644 --- a/src/httpServer.ts +++ b/src/httpServer.ts @@ -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 {logger} from "./utils"; +import {getAlias, getUserByNpub} from "./models.ts"; +import {logger} from "./utils/index.ts"; import * as nip98 from "nostr-tools/nip98"; -import {Elysia, t} from "elysia"; -import {swagger} from "@elysiajs/swagger"; -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"; +import {Client as LibSQL} from "@libsql/client"; +import {npubToPubKeyString, pubKeyStringToNpub, TokenInfoWithMailSubscriptionDuration,} from "@arx/utils"; -const npubType = t.String({ - pattern: `^npub1[023456789acdefghjklmnpqrstuvwxyz]{58}$`, - error: 'Invalid npub format' -}); - -const cashuTokenType = t.String({ - pattern: '^cashu[A-Za-z0-9+-_]*={0,3}$', - error: 'Invalid Cashu token format' -}) +const NPUB_REGEX = /^npub1[023456789acdefghjklmnpqrstuvwxyz]{58}$/; +const CASHU_REGEX = /^cashu[A-Za-z0-9+-_]*={0,3}$/; export class HttpServer { - constructor(private db: PrismaClient, port: number) { - new Elysia() - .use(swagger({ - documentation: { - info: { - title: 'npub.email Documentation', - version: '0.0.1' - } - } - })) - .use(serverTiming()) - .use(cors()) - .get('/', 'nostr.email server') - .get('/subscription/:npub', this.getSubscriptionForNpub, { - params: t.Object({ - 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) + private app: Hono; + + constructor(private db: LibSQL, port: number) { + this.app = new Hono(); + + this.app + .use("*", cors()) + .get("/", (c: HonoContext) => c.text("nostr.email server")) + .get("/subscription/:npub", this.getSubscriptionForNpub) + .get("/aliases/:npub", this.getAliasesForNpub) + .get("/alias/:alias", this.getNpubForAlias) + .post("/addAlias", this.addAlias) + .post("/addTime/:npub", this.addTimeToNpub); + + Deno.serve({ port }, this.app.fetch); logger.info(`HTTP Server running on port ${port}`); } - getSubscriptionForNpub = async ({params: {npub}}: { - params: { - npub: string + getSubscriptionForNpub = async (c: HonoContext) => { + const npub = c.req.param("npub"); + 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 - } - }); - if (!user) return { - subscribed: false - }; - return { + + const user = await getUserByNpub(this.db, npub); + + if (!user) { + return c.json({ + subscribed: false, + }); + } + return c.json({ 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 ({params: {alias}}: { - params: { - alias: string + 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); } - }) => { - const user = await this.db.user.findFirst({ - where: { - aliases: { - some: { - alias - } - } - } - }); - if (!user) return new Response('Not found', { - status: 404 - }); - return user.npub; - } + return c.json({ npub: alias.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); - if (unpacked.pubkey !== npubAsPubkey) - return new Response('Unauthorized', { - status: 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}: { - body: { - alias: string - }, - headers: Record<string, string | undefined> - }) => { - const unpacked = await this.getUnpackedAuthHeader(headers, '/addAlias'); - 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; - if (!stillHasSubscription) return new Response('User has no subscription', { - status: 400 - }); - 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}}: { - params: { - npub: string - }, - body: { - tokenString: string + getAliasesForNpub = async (c: HonoContext) => { + const npub = c.req.param("npub"); + if (!NPUB_REGEX.test(npub)) { + return c.json({ error: "Invalid npub format" }, 400); } - }) => { - const userInDb = await this.db.user.findFirst({ - where: { - npub - } - }); - if (userInDb && (userInDb.subscriptionDuration === null || userInDb.subscriptionDuration === -1)) - return new Response('User has unlimited subscription', { - status: 400 - }) + try { + const unpacked = await this.getUnpackedAuthHeader( + c.req.header("Authorization"), + `/aliases/${npub}`, + ); + const npubAsPubkey = npubToPubKeyString(npub); + + if (unpacked.pubkey !== npubAsPubkey) { + return c.json({ error: "Unauthorized" }, 401); + } + + const user = await getUserByNpub(this.db, npub); + + if (!user) { + return c.json({ error: "Not found" }, 404); + } + + 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 user = await getUserByNpub(this.db, unpackedKeyToNpub); + + if (!user) { + return c.json({ error: "Unauthorized" }, 401); + } + + const stillHasSubscription = user.subscriptionDuration === null || + Math.floor(user.lastPayment.getTime() / 1000) + + user.subscriptionDuration > Date.now() / 1000; + + if (!stillHasSubscription) { + return c.json({ error: "User has no subscription" }, 400); + } + + const aliasInDb = await getAlias(this.db, alias); + + 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 }, + }); + + return c.json({ alias, npub: unpackedKeyToNpub }); + } catch (error) { + 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 mint = new CashuMint(tokenInfo.mint); const wallet = new CashuWallet(mint); const newToken = await wallet.receive(tokenString); const encodedToken = getEncodedToken({ - token: [{ - mint: tokenInfo.mint, - proofs: newToken - }] + mint: tokenInfo.mint, + proofs: newToken, }); logger.info(`New cashu token: ${encodedToken}`); - if (userInDb) { - let timeRemaining = Math.max(0, Math.floor((+new Date(userInDb.lastPayment.getTime() + userInDb.subscriptionDuration! * 1000) - +new Date()) / 1000)); + if (user) { + let timeRemaining = Math.max( + 0, + Math.floor( + (+new Date( + user.lastPayment.getTime() + + user.subscriptionDuration! * 1000, + ) - +new Date()) / 1000, + ), + ); timeRemaining += tokenInfo.duration; - await this.db.user.update({ - where: { - npub - }, - data: { + await this.db.execute({ + sql: ` + UPDATE users + SET lastPayment = $lastPayment, subscriptionDuration = $subscriptionDuration + WHERE npub = $npub + `, + args: { lastPayment: new Date(), - subscriptionDuration: timeRemaining - } + subscriptionDuration: timeRemaining, + npub, + }, + }); + return c.json({ + newTimeRemaining: timeRemaining, }); - return { - newTimeRemaining: timeRemaining - } } - await this.db.user.create({ - data: { + await this.db.execute({ + sql: ` + INSERT INTO users (npub, registeredAt, lastPayment, subscriptionDuration) + VALUES ($npub, $registeredAt, $lastPayment, $subscriptionDuration) + `, + args: { npub, registeredAt: new Date(), lastPayment: new Date(), - subscriptionDuration: tokenInfo.duration - } + subscriptionDuration: tokenInfo.duration, + }, }); - return { - newTimeRemaining: tokenInfo.duration - } - } + return c.json({ + newTimeRemaining: tokenInfo.duration, + }); + }; - private getUnpackedAuthHeader = async (headers: Record<string, string | undefined>, url: string) => { - if (!headers.authorization) - throw new Error('Unauthorized'); - const authHeader = headers.authorization.split(' ')[1]; - const validate = await nip98.validateToken(authHeader, `${process.env.PUBLIC_API_BASE_URL!}${url}`, "POST"); - if (!validate) - throw new Error('Unauthorized'); + private getUnpackedAuthHeader = async ( + auth: string | undefined, + url: string, + ) => { + if (!auth) { + throw new Error("Unauthorized"); + } + 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); - } + }; } diff --git a/src/index.ts b/src/index.ts index 2d87a08..b0323a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,34 @@ -import {createClient as createLibSQLClient} from "@libsql/client"; -import "websocket-polyfill"; -import {PrismaClient} from "@prisma/client"; -import {PrismaLibSQL} from "@prisma/adapter-libsql"; -import {NostrSmtpServer} from "./smtpServer"; -import {HttpServer} from "./httpServer"; +import {NostrSmtpServer} from "./smtpServer.ts"; +import {HttpServer} from "./httpServer.ts"; +import {createClient as createDB} from "@libsql/client/sqlite3"; +import "@std/dotenv/load"; -if (!process.env.BASE_DOMAIN) - throw new Error("BASE_DOMAIN is not set"); -if (!process.env.DB_URL) - throw new Error("DB_URL is not set"); -if (!process.env.PUBLIC_API_BASE_URL) - throw new Error("PUBLIC_API_BASE_URL is not set"); -if (!process.env.MASTER_NSEC) - throw new Error("MASTER_NSEC is not set"); +const requiredEnvVars = [ + "BASE_DOMAIN", + "DB_URL", + "PUBLIC_API_BASE_URL", + "MASTER_NSEC", +]; -const dbClient = createLibSQLClient({ - url: process.env.DB_URL, +for (const envVar of requiredEnvVars) { + 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({ - adapter: new PrismaLibSQL(dbClient) -}); - -new NostrSmtpServer(db, parseInt(process.env.SMTP_PORT || '6587')); -new HttpServer(db, parseInt(process.env.HTTP_PORT || '3000')); \ No newline at end of file +new NostrSmtpServer( + db, + parseInt( + Deno.env.get("SMTP_PORT") || "6587", + ), +); +new HttpServer( + db, + parseInt( + Deno.env.get("HTTP_PORT") || "3000", + ), +); diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..f18303b --- /dev/null +++ b/src/models.ts @@ -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); +}; diff --git a/src/smtpServer.ts b/src/smtpServer.ts index 933d387..1d83597 100644 --- a/src/smtpServer.ts +++ b/src/smtpServer.ts @@ -1,10 +1,12 @@ -import {SMTPServer, SMTPServerAddress, SMTPServerDataStream, SMTPServerSession} from "smtp-server"; -import {deriveNsecForEmail, getNDK, logger} from "./utils"; +import {SMTPServer, SMTPServerAddress, SMTPServerDataStream, SMTPServerSession,} from "smtp-server"; +import {deriveNsecForEmail, getNDK, getUserByAlias, logger,} from "./utils/index.ts"; 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 * as path from "node:path"; -import fs from 'node:fs/promises'; +import {concat} from "@std/bytes"; +import path from "node:path"; +import fs from "node:fs/promises"; +import process from "node:process"; interface QueuedEmail { id: string; @@ -19,20 +21,24 @@ export class NostrSmtpServer { private emailQueue: QueuedEmail[] = []; private isProcessing: boolean = false; 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({ authOptional: true, 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}`); - fs.mkdir(this.BACKUP_DIR, {recursive: true}).catch(err => { - logger.error('Failed to create backup directory:', err); + fs.mkdir(this.BACKUP_DIR, { recursive: true }).catch((err) => { + logger.error("Failed to create backup directory:", err); }); this.setupGracefulShutdown(); @@ -43,7 +49,9 @@ export class NostrSmtpServer { const files = await fs.readdir(this.BACKUP_DIR); for (const file of files) { 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); } catch (error) { logger.error(`Failed to recover backup ${file}:`, error); @@ -54,71 +62,78 @@ export class NostrSmtpServer { private setupGracefulShutdown(): void { const shutdown = async () => { - logger.info('Graceful shutdown initiated'); + logger.info("Graceful shutdown initiated"); this.server.close(); - while (this.isProcessing) - await new Promise(resolve => setTimeout(resolve, 100)); + while (this.isProcessing) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } - for (const email of this.emailQueue) + for (const email of this.emailQueue) { await this.backupEmail(email); + } - logger.info('Graceful shutdown completed'); + logger.info("Graceful shutdown completed"); process.exit(0); }; - process.on('SIGTERM', shutdown); - process.on('SIGINT', shutdown); + process.on("SIGTERM", shutdown); + process.on("SIGINT", shutdown); } - private async handleEmailData(stream: SMTPServerDataStream, session: SMTPServerSession, callback: () => void, db: PrismaClient) { - const chunks: Buffer[] = []; + private async handleEmailData( + stream: SMTPServerDataStream, + session: SMTPServerSession, + callback: () => void, + ) { + const chunks: Uint8Array[] = []; - stream.on('data', (chunk: Buffer) => { - chunks.push(chunk); - }); + try { + for await (const chunk of stream) { + chunks.push(chunk); + } - stream.on('end', async () => { if (!this.validateSender(session)) { callback(); return; } - const mailData = Buffer.concat(chunks).toString(); + const mailData = new TextDecoder().decode( + concat(chunks), + ); - try { - const queuedEmail: QueuedEmail = { - id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - mailData, - session, - attempts: 0, - createdAt: Date.now() - }; + const queuedEmail: QueuedEmail = { + id: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, + mailData, + session, + attempts: 0, + createdAt: Date.now(), + }; - this.emailQueue.push(queuedEmail); - await this.backupEmail(queuedEmail); - this.processQueue(); - } catch (e) { - logger.error(`Error processing recipients: ${e}`, e); - } + this.emailQueue.push(await this.backupEmail(queuedEmail)); + this.processQueue(); + } catch (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 { const backupPath = path.join(this.BACKUP_DIR, `${email.id}.json`); await fs.writeFile(backupPath, JSON.stringify(email)); } 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> { - if (this.isProcessing) + if (this.isProcessing) { return; + } this.isProcessing = true; while (this.emailQueue.length > 0) { const email = this.emailQueue[0]; @@ -128,7 +143,9 @@ export class NostrSmtpServer { } 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); // Remove from queue and delete backup if successful @@ -149,77 +166,93 @@ export class NostrSmtpServer { const backupPath = path.join(this.BACKUP_DIR, `${id}.json`); await fs.unlink(backupPath); } 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 { if (!session.envelope.mailFrom) { - logger.warn('Ignoring email without sender'); + logger.warn("Ignoring email without sender"); return false; } 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) { const address = recipientEmail.address; - const [alias, domain] = address.split('@'); + const [alias, domain] = address.split("@"); - if (domain !== process.env.BASE_DOMAIN) { - logger.warn(`Not sending email to ${address} because it is not in the allowed domain`); + if (domain !== Deno.env.get("BASE_DOMAIN")) { + logger.warn( + `Not sending email to ${address} because it is not in the allowed domain`, + ); continue; } 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); } } - private async getUser(alias: string, db: PrismaClient) { - const user = await db.alias.findUnique({ - where: {alias}, - include: {user: true} - }); + private async getUser(alias: string, db: LibSQL) { + const user = await getUserByAlias(db, alias); if (!user) { - logger.warn('No user found for', alias, 'skipping'); + logger.warn("No user found for", alias, "skipping"); return null; } 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 (user.user.subscriptionDuration === null) + if (user.subscriptionDuration === null) { return true; + } - const subscriptionDurationMs = user.user.subscriptionDuration * 1000; - const lastPaymentTimestamp = user.user.lastPayment.getTime(); + const subscriptionDurationMs = user.subscriptionDuration * 1000; + const lastPaymentTimestamp = user.lastPayment.getTime(); const currentTimestamp = Date.now(); const subscriptionEndTime = lastPaymentTimestamp + subscriptionDurationMs; const timeRemaining = subscriptionEndTime - currentTimestamp; if (timeRemaining <= 0) { - logger.warn(`Subscription has expired for ${user.alias}`); + logger.warn(`Subscription has expired for ${user.npub}`); return false; } 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( - 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(); ndk.signer = randomKeySinger; await ndk.connect(); - const ndkUser = ndk.getUser({npub: recipient}); + const ndkUser = ndk.getUser({ npub: recipient }); const randomKeyUser = await randomKeySinger.user(); const event = new NDKEvent(); event.kind = NDKKind.Article; @@ -227,17 +260,17 @@ export class NostrSmtpServer { event.created_at = Math.floor(Date.now() / 1000); event.pubkey = randomKeyUser.pubkey; event.tags.push( - ['p', ndkUser.pubkey], - ['subject', parsedEmail.subject], - ['email:localIP', session.localAddress], - ['email:remoteIP', session.remoteAddress], - ['email:isEmail', 'true'], - ['email:session', session.id], - ['email:from', (session.envelope.mailFrom as SMTPServerAddress).address] + ["p", ndkUser.pubkey], + ["subject", parsedEmail.subject], + ["email:localIP", session.localAddress], + ["email:remoteIP", session.remoteAddress], + ["email:isEmail", "true"], + ["email:session", session.id], + ["email:from", (session.envelope.mailFrom as SMTPServerAddress).address], ); 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)) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 43362bc..257f4ca 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,15 +1,16 @@ import NDK from "@nostr-dev-kit/ndk"; 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() { return new NDK({ explicitRelayUrls: [ - 'wss://relay.primal.net', - 'wss://relay.damus.io', - 'wss://relay.nostr.band', - 'wss://offchain.pub' + "wss://relay.primal.net", + "wss://relay.nostr.band", + "wss://offchain.pub", ], autoConnectUserRelays: false, enableOutboxModel: true, @@ -27,9 +28,16 @@ export function getNDK() { * @param email - The email address. * @returns The nostr private key derived from the master key and email address as a uint8array. */ -export function deriveNsecForEmail(masterNsec: string, email: string): Uint8Array { - 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 Uint8Array.from(Buffer.from(sharedSecret, 'hex')); -} \ No newline at end of file +export function deriveNsecForEmail( + masterNsec: string, + email: string, +): Uint8Array { + 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); +} diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 80c0465..e95a6c0 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -1,22 +1,22 @@ 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({ - level: 'info', + level: "info", transports: [ new winston.transports.Console({ format: combine( - colorize({all: true}), + colorize({ all: true }), timestamp({ - format: 'YYYY-MM-DD hh:mm:ss.SSS A', + format: "YYYY-MM-DD hh:mm:ss.SSS A", }), align(), - printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`) + printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`), ), }), 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()), }), ],