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:
parent
095791f44f
commit
bec36602b5
14 changed files with 1235 additions and 431 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
19
deno.json
Normal file
19
deno.json
Normal 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
733
deno.lock
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
34
package.json
34
package.json
|
@ -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
|
||||
}
|
|
@ -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");
|
|
@ -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;
|
|
@ -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"
|
|
@ -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")
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
52
src/index.ts
52
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'));
|
||||
new NostrSmtpServer(
|
||||
db,
|
||||
parseInt(
|
||||
Deno.env.get("SMTP_PORT") || "6587",
|
||||
),
|
||||
);
|
||||
new HttpServer(
|
||||
db,
|
||||
parseInt(
|
||||
Deno.env.get("HTTP_PORT") || "3000",
|
||||
),
|
||||
);
|
||||
|
|
113
src/models.ts
Normal file
113
src/models.ts
Normal 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);
|
||||
};
|
|
@ -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)) {
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
}),
|
||||
],
|
||||
|
|
Loading…
Reference in a new issue