diff options
| author | Adam Soltys <[email protected]> | 2026-02-28 14:14:30 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-28 14:14:30 -0800 |
| commit | a1632ae402d11fdd7ffc360de97c55e0b4fad677 (patch) | |
| tree | aad5a1ea73f95dcd055c1d0b4ebf2708c36b4e01 | |
| parent | 76c7e52172f3dc25ee109434157d4d982ad9d2bd (diff) | |
| download | coinos-server-a1632ae402d11fdd7ffc360de97c55e0b4fad677.tar.gz coinos-server-a1632ae402d11fdd7ffc360de97c55e0b4fad677.zip | |
migrate to hono
| -rw-r--r-- | .github/workflows/lint.yml | 8 | ||||
| -rw-r--r-- | .husky/pre-commit | 1 | ||||
| -rw-r--r-- | bun.lock | 162 | ||||
| -rw-r--r-- | index.ts | 62 | ||||
| -rw-r--r-- | lib/app.ts | 225 | ||||
| -rw-r--r-- | lib/auth.ts | 87 | ||||
| -rw-r--r-- | lib/lightning.ts | 4 | ||||
| -rw-r--r-- | lib/sockets.ts | 137 | ||||
| -rw-r--r-- | lib/upload.ts | 17 | ||||
| -rw-r--r-- | lib/utils.ts | 2 | ||||
| -rw-r--r-- | package.json | 17 | ||||
| -rw-r--r-- | routes/ecash.ts | 63 | ||||
| -rw-r--r-- | routes/email.ts | 7 | ||||
| -rw-r--r-- | routes/info.ts | 16 | ||||
| -rw-r--r-- | routes/invoices.ts | 51 | ||||
| -rw-r--r-- | routes/items.ts | 59 | ||||
| -rw-r--r-- | routes/lnurl.ts | 67 | ||||
| -rw-r--r-- | routes/locations.ts | 20 | ||||
| -rw-r--r-- | routes/nostr.ts | 139 | ||||
| -rw-r--r-- | routes/payments.ts | 358 | ||||
| -rw-r--r-- | routes/rates.ts | 12 | ||||
| -rw-r--r-- | routes/shopify.ts | 19 | ||||
| -rw-r--r-- | routes/square.ts | 24 | ||||
| -rw-r--r-- | routes/users.ts | 579 |
24 files changed, 954 insertions, 1182 deletions
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c7c9dd41..23049352 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,6 +10,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref_name }} + token: ${{ secrets.GITHUB_TOKEN }} - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile - - run: bun run lint + - run: bun run lint:fix + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "style: auto-fix lint & formatting" diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index ea5a55b6..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -bunx lint-staged @@ -10,14 +10,6 @@ "@aws-sdk/client-ses": "^3.997.0", "@cashu/cashu-ts": "^2.9.0", "@coinos/rpc": "^0.0.6", - "@fastify/cors": "^11.2.0", - "@fastify/http-proxy": "^11.4.1", - "@fastify/multipart": "^9.4.0", - "@fastify/passport": "^3.0.2", - "@fastify/rate-limit": "^10.3.0", - "@fastify/secure-session": "^8.3.0", - "@fastify/static": "^9.0.0", - "@fastify/websocket": "^11.2.0", "@noble/hashes": "^2.0.1", "@scure/base": "^2.0.0", "@scure/bip32": "^2.0.1", @@ -26,19 +18,17 @@ "bech32": "^2.0.0", "clightning-client": "github:asoltys/lightning-client-js#master", "eventsource": "^4.1.0", - "fastify": "^5.7.4", "file-type": "^21.3.0", "fluent-ffmpeg": "^2.1.3", "got": "^14.6.6", "handlebars": "^4.7.8", + "hono": "^4.7.0", "jsonwebtoken": "^9.0.3", "lnurl": "^0.27.0", "mqtt": "^5.15.0", "nostr": "^0.2.8", "nostr-tools": "^2.23.1", "otplib": "^12.0.1", - "passport-jwt": "^4.0.1", - "passport-local": "1.0.0", "pino": "^10.3.1", "pino-pretty": "^13.1.3", "pump": "^3.0.3", @@ -166,48 +156,6 @@ "@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@fastify/accept-negotiator": ["@fastify/[email protected]", "", {}, "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ=="], - - "@fastify/ajv-compiler": ["@fastify/[email protected]", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="], - - "@fastify/busboy": ["@fastify/[email protected]", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="], - - "@fastify/cookie": ["@fastify/[email protected]", "", { "dependencies": { "cookie": "^1.0.0", "fastify-plugin": "^5.0.0" } }, "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA=="], - - "@fastify/cors": ["@fastify/[email protected]", "", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw=="], - - "@fastify/deepmerge": ["@fastify/[email protected]", "", {}, "sha512-aO5giNgFN+rD4fMUAkro9nEL7c9gh5Q3lh0ZGKMDAhQAytf22HLicF/qZ2EYTDmH+XL2WvdazwBfOdmp6NiwBg=="], - - "@fastify/error": ["@fastify/[email protected]", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], - - "@fastify/fast-json-stringify-compiler": ["@fastify/[email protected]", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="], - - "@fastify/flash": ["@fastify/[email protected]", "", { "dependencies": { "fastify-plugin": "^5.0.0" } }, "sha512-MyUvkiIR9zRqfMPCuWeTf8uyYpPhL+eZH4oSTxvqtbW+ifbo1Bk1SJtlrxmFgu52ZAAKUnYtI5OuSY9/GR22SA=="], - - "@fastify/forwarded": ["@fastify/[email protected]", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="], - - "@fastify/http-proxy": ["@fastify/[email protected]", "", { "dependencies": { "@fastify/reply-from": "^12.5.0", "fast-querystring": "^1.1.2", "fastify-plugin": "^5.1.0", "ws": "^8.18.3" } }, "sha512-sMYEIB0c7qCYutpZyS12c8xnVgmEMSUUVU2XjcNq2JzHf6Hta1BWcpnG5FXxR3WEm48uZNCi0MxnIYtwjwd21Q=="], - - "@fastify/merge-json-schemas": ["@fastify/[email protected]", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], - - "@fastify/multipart": ["@fastify/[email protected]", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@fastify/deepmerge": "^3.0.0", "@fastify/error": "^4.0.0", "fastify-plugin": "^5.0.0", "secure-json-parse": "^4.0.0" } }, "sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ=="], - - "@fastify/passport": ["@fastify/[email protected]", "", { "dependencies": { "@fastify/flash": "^6.0.0", "fastify-plugin": "^5.0.0" } }, "sha512-pUzOxHTaTta6RvtbD4Ct4uTH7wWfgFtyj8apTu+IMe0w67qm5Q5fZsESOVdcC5IothVEySf54fyM9qQ5VNHP3A=="], - - "@fastify/proxy-addr": ["@fastify/[email protected]", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="], - - "@fastify/rate-limit": ["@fastify/[email protected]", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q=="], - - "@fastify/reply-from": ["@fastify/[email protected]", "", { "dependencies": { "@fastify/error": "^4.0.0", "end-of-stream": "^1.4.4", "fast-content-type-parse": "^3.0.0", "fast-querystring": "^1.1.2", "fastify-plugin": "^5.0.1", "toad-cache": "^3.7.0", "undici": "^7.0.0" } }, "sha512-NIV5YiQR5eNU0dg0G050yHpxb1D1HZ1XD8gIcpyT74qDHYLXDD913VjnfzwI7bbiZ/F+cxHkbduC22K6rfXywA=="], - - "@fastify/secure-session": ["@fastify/[email protected]", "", { "dependencies": { "@fastify/cookie": "^11.0.1", "fastify-plugin": "^5.0.0", "sodium-native": "^5.0.1" }, "bin": { "secure-session": "genkey.js" } }, "sha512-jyXbYWAp5MCaCA+7idsV+Op3kMN/oDtskZ0SE3QPl53Lo0GFF7p/lk1UEliGaT0OSX+RGSrd2gQlUu1ta1t7yQ=="], - - "@fastify/send": ["@fastify/[email protected]", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "escape-html": "~1.0.3", "fast-decode-uri-component": "^1.0.1", "http-errors": "^2.0.0", "mime": "^3" } }, "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw=="], - - "@fastify/static": ["@fastify/[email protected]", "", { "dependencies": { "@fastify/accept-negotiator": "^2.0.0", "@fastify/send": "^4.0.0", "content-disposition": "^1.0.1", "fastify-plugin": "^5.0.0", "fastq": "^1.17.1", "glob": "^13.0.0" } }, "sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA=="], - - "@fastify/websocket": ["@fastify/[email protected]", "", { "dependencies": { "duplexify": "^4.1.3", "fastify-plugin": "^5.0.0", "ws": "^8.16.0" } }, "sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w=="], - "@hexagon/base64": ["@hexagon/[email protected]", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], "@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], @@ -256,8 +204,6 @@ "@levischuck/tiny-cbor": ["@levischuck/[email protected]", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], - "@lukeed/ms": ["@lukeed/[email protected]", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], - "@marcbachmann/cel-js": ["@marcbachmann/[email protected]", "", { "bin": { "cel-evaluate": "bin/cel-evaluate.js" } }, "sha512-P6o26TvjStT8V8+8EF+yq9Pp7ZFV00bpiUMbssr76XbIZGxaB+NNWeBp6WNxOrR9gp0JPzvJueCKHpOs5LE9PQ=="], "@noble/ciphers": ["@noble/[email protected]", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], @@ -504,16 +450,10 @@ "abort-controller": ["[email protected]", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "abstract-logging": ["[email protected]", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], - "accepts": ["[email protected]", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ajv-formats": ["[email protected]", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ansi-escapes": ["[email protected]", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["[email protected]", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -536,16 +476,8 @@ "available-typed-arrays": ["[email protected]", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "avvio": ["[email protected]", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="], - "axios": ["[email protected]", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], - "bare-addon-resolve": ["[email protected]", "", { "dependencies": { "bare-module-resolve": "^1.10.0", "bare-semver": "^1.0.0" }, "peerDependencies": { "bare-url": "*" }, "optionalPeers": ["bare-url"] }, "sha512-cIo93Y4Maw8ZNrfq5qlvRBqCHUyeSzMyFBqJRNi91SOspeWiFq3ixiq0ExYWmtOBauIkCLod2Mng73xr7fmkXA=="], - - "bare-module-resolve": ["[email protected]", "", { "dependencies": { "bare-semver": "^1.0.0" }, "peerDependencies": { "bare-url": "*" }, "optionalPeers": ["bare-url"] }, "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg=="], - - "bare-semver": ["[email protected]", "", {}, "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA=="], - "base-x": ["[email protected]", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="], "base64-js": ["[email protected]", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -634,11 +566,11 @@ "concat-stream": ["[email protected]", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="], - "content-disposition": ["[email protected]", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + "content-disposition": ["[email protected]", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "content-type": ["[email protected]", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "cookie": ["[email protected]", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie": ["[email protected]", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "cookie-signature": ["[email protected]", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], @@ -658,8 +590,6 @@ "depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "dequal": ["[email protected]", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "destroy": ["[email protected]", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], "detect-browser": ["[email protected]", "", {}, "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w=="], @@ -670,8 +600,6 @@ "dunder-proto": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "duplexify": ["[email protected]", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], - "ecdsa-sig-formatter": ["[email protected]", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], "ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -712,40 +640,20 @@ "express-handlebars": ["[email protected]", "", { "dependencies": { "glob": "^8.1.0", "graceful-fs": "^4.2.10", "handlebars": "^4.7.7" } }, "sha512-iYeMFpc/hMD+E6FNAZA5fgWeXnXr4rslOSPkeEV6TwdmpJ5lEXuWX0u9vFYs31P2MURctQq2batR09oeNj0LIg=="], - "fast-content-type-parse": ["[email protected]", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], - "fast-copy": ["[email protected]", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], - "fast-decode-uri-component": ["[email protected]", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - - "fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stringify": ["[email protected]", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="], - - "fast-querystring": ["[email protected]", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], - "fast-safe-stringify": ["[email protected]", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], "fast-unique-numbers": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.28.6", "tslib": "^2.8.1" } }, "sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig=="], - "fast-uri": ["[email protected]", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fast-xml-parser": ["[email protected]", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], - "fastify": ["[email protected]", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="], - - "fastify-plugin": ["[email protected]", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], - - "fastq": ["[email protected]", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "file-type": ["[email protected]", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], "fill-range": ["[email protected]", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["[email protected]", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg=="], - "find-my-way": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w=="], - "fluent-ffmpeg": ["[email protected]", "", { "dependencies": { "async": "^0.2.9", "which": "^1.1.1" } }, "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q=="], "follow-redirects": ["[email protected]", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], @@ -776,7 +684,7 @@ "get-stream": ["[email protected]", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - "glob": ["[email protected]", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w=="], + "glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], "glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -806,6 +714,8 @@ "hmac-drbg": ["[email protected]", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], + "hono": ["[email protected]", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="], + "http-cache-semantics": ["[email protected]", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], "http-errors": ["[email protected]", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], @@ -832,7 +742,7 @@ "ip-address": ["[email protected]", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], - "ipaddr.js": ["[email protected]", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], + "ipaddr.js": ["[email protected]", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-arrayish": ["[email protected]", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], @@ -860,10 +770,6 @@ "js-sdsl": ["[email protected]", "", {}, "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ=="], - "json-schema-ref-resolver": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="], - - "json-schema-traverse": ["[email protected]", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "jsonwebtoken": ["[email protected]", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jwa": ["[email protected]", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], @@ -872,8 +778,6 @@ "keyv": ["[email protected]", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], - "light-my-request": ["[email protected]", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], - "lightning-backends": ["[email protected]", "", { "dependencies": { "@bleskomat/form": "1.3.2", "async": "3.2.6", "bolt11": "1.4.1", "secp256k1": "5.0.0", "socks-proxy-agent": "8.0.4" } }, "sha512-jVYaWLhbgGRugndh/W30nd0AjP9D1CcZnbf3tZwGFpG700cuEx2rrufdov8Q28gC3//mfheOQjnRNgf0xPU6Ww=="], "lint-staged": ["[email protected]", "", { "dependencies": { "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow=="], @@ -924,7 +828,7 @@ "micromatch": ["[email protected]", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["[email protected]", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime": ["[email protected]", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], "mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -942,8 +846,6 @@ "minimist": ["[email protected]", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["[email protected]", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "mqtt": ["[email protected]", "", { "dependencies": { "@types/readable-stream": "^4.0.21", "@types/ws": "^8.18.1", "commist": "^3.2.0", "concat-stream": "^2.0.0", "debug": "^4.4.1", "help-me": "^5.0.0", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "mqtt-packet": "^9.0.2", "number-allocator": "^1.0.14", "readable-stream": "^4.7.0", "rfdc": "^1.4.1", "socks": "^2.8.6", "split2": "^4.2.0", "worker-timers": "^8.0.23", "ws": "^8.18.3" }, "bin": { "mqtt": "build/bin/mqtt.js", "mqtt_pub": "build/bin/pub.js", "mqtt_sub": "build/bin/sub.js" } }, "sha512-KC+wAssYk83Qu5bT8YDzDYgUJxPhbLeVsDvpY2QvL28PnXYJzC2WkKruyMUgBAZaQ7h9lo9k2g4neRNUUxzgMw=="], "mqtt-packet": ["[email protected]", "", { "dependencies": { "bl": "^6.0.8", "debug": "^4.3.4", "process-nextick-args": "^2.0.1" } }, "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA=="], @@ -998,14 +900,6 @@ "parseurl": ["[email protected]", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "passport-jwt": ["[email protected]", "", { "dependencies": { "jsonwebtoken": "^9.0.0", "passport-strategy": "^1.0.0" } }, "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ=="], - - "passport-local": ["[email protected]", "", { "dependencies": { "passport-strategy": "1.x.x" } }, "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow=="], - - "passport-strategy": ["[email protected]", "", {}, "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA=="], - - "path-scurry": ["[email protected]", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], - "path-to-regexp": ["[email protected]", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1062,28 +956,18 @@ "reflect-metadata": ["[email protected]", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], - "require-addon": ["[email protected]", "", { "dependencies": { "bare-addon-resolve": "^1.3.0" } }, "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA=="], - - "require-from-string": ["[email protected]", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-alpn": ["[email protected]", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], "responselike": ["[email protected]", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA=="], "restore-cursor": ["[email protected]", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "ret": ["[email protected]", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], - - "reusify": ["[email protected]", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rfdc": ["[email protected]", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "ripemd160": ["[email protected]", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="], "safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safe-regex2": ["[email protected]", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="], - "safe-stable-stringify": ["[email protected]", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -1098,8 +982,6 @@ "serve-static": ["[email protected]", "", { "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.18.0" } }, "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g=="], - "set-cookie-parser": ["[email protected]", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "set-function-length": ["[email protected]", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "setprototypeof": ["[email protected]", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -1130,8 +1012,6 @@ "socks-proxy-agent": ["[email protected]", "", { "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw=="], - "sodium-native": ["[email protected]", "", { "dependencies": { "require-addon": "^1.1.0", "which-runtime": "^1.2.1" } }, "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA=="], - "sonic-boom": ["[email protected]", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], "source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -1144,8 +1024,6 @@ "statuses": ["[email protected]", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - "stream-shift": ["[email protected]", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], - "strfry-rpc": ["[email protected]", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-jbfycgsU41pgIMHprZeMnmmgO57o7G1IRgmMgkY08BAEvzf0YFzTfSG8TWAXudAnMLHSqYLPfHwOueBn1g6roA=="], "string-argv": ["[email protected]", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], @@ -1178,8 +1056,6 @@ "to-regex-range": ["[email protected]", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toad-cache": ["[email protected]", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], - "toidentifier": ["[email protected]", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "token-types": ["[email protected]", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], @@ -1210,8 +1086,6 @@ "undefsafe": ["[email protected]", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], - "undici": ["[email protected]", "", {}, "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ=="], - "unpipe": ["[email protected]", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "util-deprecate": ["[email protected]", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -1232,8 +1106,6 @@ "which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - "which-runtime": ["[email protected]", "", {}, "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw=="], - "which-typed-array": ["[email protected]", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], "wordwrap": ["[email protected]", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], @@ -1294,18 +1166,8 @@ "concat-stream/readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "duplexify/readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "express/content-disposition": ["[email protected]", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "express/cookie": ["[email protected]", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - "express/debug": ["[email protected]", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "express-handlebars/glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - - "fastify/pino": ["[email protected]", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="], - "finalhandler/debug": ["[email protected]", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "hash-base/readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -1314,8 +1176,6 @@ "https-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "light-my-request/process-warning": ["[email protected]", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], - "lightning-backends/async": ["[email protected]", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "lnurl/async": ["[email protected]", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], @@ -1332,16 +1192,10 @@ "number-allocator/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], - - "proxy-addr/ipaddr.js": ["[email protected]", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "raw-body/http-errors": ["[email protected]", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "send/debug": ["[email protected]", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "send/mime": ["[email protected]", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "socks-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "tsyringe/tslib": ["[email protected]", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -17,7 +17,7 @@ import { import nwc from "$lib/nwc"; import { catchUp, check } from "$lib/payments"; import { getFx } from "$lib/rates"; -import { sendHeartbeat } from "$lib/sockets"; +import { sendHeartbeat, websocket } from "$lib/sockets"; import { initTigerBeetle } from "$lib/tb"; import { startZmq } from "$lib/zmq"; @@ -35,7 +35,6 @@ import shopify from "$routes/shopify"; import square from "$routes/square"; import users from "$routes/users"; -console.log("test"); try { await initTigerBeetle(); getLocations(); @@ -74,7 +73,9 @@ app.post("/invoice", optional, invoices.create); app.post("/invoice/:id", optional, invoices.update); app.post("/sign", auth, invoices.sign); -app.get("/assetlinks.json", (_, res) => res.sendFile("assetlinks.json")); +app.get("/assetlinks.json", (c) => { + return new Response(Bun.file("assetlinks.json")); +}); app.get("/nostr.json", nostr.identities); app.get("/profile/:profile", nostr.profile); app.get("/:pubkey/count", nostr.count); @@ -97,7 +98,6 @@ app.get("/payments", auth, payments.list); app.get("/payments/:hash", payments.get); app.post("/parse", auth, payments.parse); app.get("/fund/:id", payments.fund); -// app.get("/fund/:name/withdraw", auth, payments.withdraw); app.get("/fund/:name/managers", payments.managers); app.post("/fund/managers", auth, payments.addManager); app.post("/fund/:name/managers/delete", auth, payments.deleteManager); @@ -125,7 +125,6 @@ app.get("/lnurlp/:username", lnurl.lnurlp); app.get("/lnurl/:id", lnurl.lnurl); app.get("/pay/:username", lnurl.pay); app.get("/pay/:username/:amount", lnurl.pay); -// app.get("/lnurlw", lnurl.withdraw); app.post("/freeze", payments.freeze); @@ -160,19 +159,7 @@ app.post("/superuser", users.superuser); app.get("/verify/:code", users.verify); app.post("/request", auth, users.request); app.post("/forgot", users.forgot); -app.post( - "/login", - { - config: { - rateLimit: { - max: 5, - timeWindow: "5 seconds", - keyGenerator: (req) => req.headers["cf-connecting-ip"] as string, // IP-based rate limiting - }, - }, - }, - users.login, -); +app.post("/login", users.login); app.post("/flash", users.flash); app.get("/challenge", users.challenge); app.post("/authKeyLogin", users.authKeyLogin); @@ -220,26 +207,27 @@ app.post("/mint", auth, ecash.mint); app.post("/melt", auth, ecash.melt); app.post("/ecash/:id", ecash.receive); -app.get("/replay/:index", (req, res) => { - replay(req.params.index); - res.send({}); +app.get("/replay/:index", (c) => { + replay(c.req.param("index")); + return c.json({}); }); -app.post("/echo", (req, res) => { - console.log("echo", req.body); - res.send(req.body); +app.post("/echo", async (c) => { + const body = await c.req.json(); + console.log("echo", body); + return c.json(body); }); -app.post("/test/ark/send", async (req, res) => { +app.post("/test/ark/send", async (c) => { try { - const secret = req.headers["x-test-secret"] as string; + const secret = c.req.header("x-test-secret"); if (!config.testSecret || secret !== config.testSecret) { - return res.code(403).send("Forbidden"); + return c.json("Forbidden", 403); } - const { address, amount, iid } = req.body; + const body = await c.req.json(); + const { address, amount, iid } = body; const txid = await sendArk(address, amount); - // Mark invoice as paid so tests don't need client-side SDK sync if (iid) { const invoice = await g(`invoice:${iid}`); if (invoice) { @@ -248,16 +236,24 @@ app.post("/test/ark/send", async (req, res) => { } } - res.send({ txid, iid }); + return c.json({ txid, iid }); } catch (e: any) { - res.code(500).send(e.message); + return c.json(e.message, 500); } }); -const host: string = process.env["HOST"] || "0.0.0.0"; +const host_: string = process.env["HOST"] || "0.0.0.0"; const port: number = Number.parseInt(process.env["PORT"]) || 3119; -app.listen({ host, port }); +Bun.serve({ + fetch(req, server) { + if (new URL(req.url).pathname === "/ws" && server.upgrade(req)) return; + return app.fetch(req, server); + }, + websocket, + port, + hostname: host_, +}); const logerr = (e: Error) => console.log(e); process.on("unhandledRejection", logerr); @@ -1,34 +1,88 @@ -import config from "$config"; -import cors from "@fastify/cors"; -import fastifyProxy from "@fastify/http-proxy"; -import fastifyMultipart from "@fastify/multipart"; -import fastifyPassport from "@fastify/passport"; -import fastifyRateLimit from "@fastify/rate-limit"; -import fastifySecureSession from "@fastify/secure-session"; -import fastifyStatic from "@fastify/static"; -import fastify from "fastify"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { serveStatic } from "hono/bun"; import pino from "pino"; -import * as path from "path"; +const app = new Hono(); -import { jwtStrategy } from "$lib/auth"; +const reqLogger = pino(pino.destination("req")); +const resLogger = pino(pino.destination("res")); + +// CORS +app.use( + "*", + cors({ + origin: (origin) => origin || "*", + credentials: true, + }), +); + +// Static files +app.use("/public/*", serveStatic({ root: "/home/bun/app/data/uploads", rewriteRequestPath: (p) => p.replace("/public", "") })); + +// Rate limiting +const rateLimits = new Map<string, { count: number; reset: number }>(); +const strictLimits = new Map<string, { count: number; reset: number }>(); -const app = fastify({ - logger: true, - disableRequestLogging: true, - routerOptions: { maxParamLength: 500 }, +app.use("*", async (c, next) => { + const url = c.req.path; + + // Skip rate limiting for public assets + if (url.includes("public")) return next(); + + const ip = (c.req.header("cf-connecting-ip") as string) || c.env?.ip || "unknown"; + const ua = c.req.header("user-agent") || "unknown-ua"; + const rateLimitBy = c.req.header("rate-limit-by"); + const key = rateLimitBy === "ua" ? ua : ip; + const now = Date.now(); + + // General rate limit: 2000 req / 2s + const gen = rateLimits.get(key); + if (gen && now < gen.reset) { + gen.count++; + if (gen.count > 2000) { + return c.json( + { statusCode: 429, error: "Too Many Requests", message: "Rate limit exceeded, retry in 2 seconds" }, + 429, + ); + } + } else { + rateLimits.set(key, { count: 1, reset: now + 2000 }); + } + + // Strict rate limit for /login and /send: 10 req / 10s + const isStrict = url.includes("/login") || url.includes("/send"); + if (isStrict) { + const strictKey = `strict:${ua}`; + const s = strictLimits.get(strictKey); + if (s && now < s.reset) { + s.count++; + if (s.count > 10) { + return c.json( + { statusCode: 429, error: "Too Many Requests", message: "Rate limit exceeded" }, + 429, + ); + } + } else { + strictLimits.set(strictKey, { count: 1, reset: now + 10000 }); + } + } + + return next(); }); -const reqLogger = pino(pino.destination("req")); -const resLogger = pino(pino.destination("res")); +// Clean up rate limit maps periodically +setInterval(() => { + const now = Date.now(); + for (const [k, v] of rateLimits) if (now >= v.reset) rateLimits.delete(k); + for (const [k, v] of strictLimits) if (now >= v.reset) strictLimits.delete(k); +}, 5000); -// app.addHook("onRequest", async (req) => { -// reqLogger.info({ url: req.raw.url, id: req.id }); -// }); -// +// Request logging +app.use("*", async (c, next) => { + const start = Date.now(); + const url = c.req.path; -app.addHook("preHandler", async (req) => { - const url = req.raw.url; const ignore = [ "/ws", "/me", @@ -42,28 +96,36 @@ app.addHook("preHandler", async (req) => { "/accounts", "/contacts", ]; - if (ignore.some((path) => url.startsWith(path))) return; - if (req.method === "GET" && url.startsWith("/users")) return; - const xff = req.headers["x-forwarded-for"]; - const forwardedIp = Array.isArray(xff) ? xff[0] : xff?.split(",")[0]?.trim(); - const ip = req.headers["cf-connecting-ip"] || forwardedIp || req.ip; + const shouldLog = !ignore.some((path) => url.startsWith(path)) && + !(c.req.method === "GET" && url.startsWith("/users")); - reqLogger.info({ - method: req.method, - url, - ip, - headers: req.headers, - query: req.query, - body: req.body, - user: (req.user as any)?.username, - id: req.id, - }); -}); + if (shouldLog) { + const xff = c.req.header("x-forwarded-for"); + const forwardedIp = xff?.split(",")[0]?.trim(); + const ip = c.req.header("cf-connecting-ip") || forwardedIp || c.env?.ip || "unknown"; -app.addHook("onResponse", async (req, reply) => { - const rawCookies = req.raw.headers.cookie || ""; + let body; + // Only parse body for non-GET requests + if (c.req.method !== "GET" && c.req.method !== "HEAD") { + try { + body = await c.req.raw.clone().json(); + } catch {} + } + + reqLogger.info({ + method: c.req.method, + url, + ip, + query: c.req.query(), + body, + user: c.get("user")?.username, + }); + } + await next(); + + const rawCookies = c.req.header("cookie") || ""; const cookies: any = rawCookies.split(";").reduce((acc, cookie) => { const [key, value] = cookie.split("=").map((s) => s.trim()); if (key && value) acc[key] = value; @@ -71,84 +133,19 @@ app.addHook("onResponse", async (req, reply) => { }, {}); resLogger.info({ - id: req.id, - url: req.raw.url, - statusCode: reply.raw.statusCode, - durationMs: reply.elapsedTime, + url, + statusCode: c.res.status, + durationMs: Date.now() - start, username: cookies.username, }); }); -app.register(fastifyRateLimit, { - allowList: (req) => req.raw.url?.includes("public"), - max: 2000, - timeWindow: 2000, - keyGenerator: (req) => { - const ip = (req.headers["cf-connecting-ip"] as string) || req.ip; - const ua = req.headers["user-agent"] || "unknown-ua"; - return req.headers["rate-limit-by"] === "ua" ? ua : ip; - }, - errorResponseBuilder: () => { - return { - statusCode: 429, - error: "Too Many Requests", - message: "Rate limit exceeded, retry in 2 seconds", - }; - }, -}); - -app.register(fastifyRateLimit, { - allowList: (req) => { - const url = req.raw.url || ""; - const matches = url.includes("/login") || url.includes("/send"); - return !matches; - }, - max: 10, - timeWindow: 10000, - keyGenerator: (req) => (req.headers["user-agent"] as string) || "unknown-ua", - errorResponseBuilder: () => ({ - statusCode: 429, - error: "Too Many Requests", - message: "Rate limit exceeded", - }), -}); - -app.register(fastifyProxy, { - upstream: "http://localhost:3120", - prefix: "/ws", - rewritePrefix: "/ws", - websocket: true, -}); - -app.register(fastifySecureSession, { - key: Buffer.from(config.jwt, "hex"), -}); - -app.register(fastifyPassport.initialize()); -app.register(fastifyPassport.secureSession()); - -fastifyPassport.use("jwt", jwtStrategy); - -app.register(fastifyMultipart, { limits: { fileSize: 10 ** 7 } }); - -app.register(fastifyStatic, { - root: path.join("/home/bun/app/data/uploads"), - prefix: "/public/", -}); - -await app.register(cors, { - origin: true, - credentials: true, -}); - -app.setErrorHandler((error, _, reply) => { - if (error instanceof fastify.errorCodes.FST_ERR_BAD_STATUS_CODE) { - reply.status(500).send({ ok: false }); - } else { - reply.send(error); - } +// Error handler +app.onError((err, c) => { + return c.json({ ok: false }, 500); }); -app.setNotFoundHandler((_, reply) => reply.code(404).type("text/html").send("Not Found")); +// Not found handler +app.notFound((c) => c.text("Not Found", 404)); export default app; diff --git a/lib/auth.ts b/lib/auth.ts index c2eca6d4..30d5438b 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,58 +1,53 @@ import config from "$config"; import { fail, getUser } from "$lib/utils"; -import fastifyPassport from "@fastify/passport"; -import jwt from "passport-jwt"; +import jwt from "jsonwebtoken"; +import { getCookie } from "hono/cookie"; -export const admin = { - preValidation: fastifyPassport.authenticate( - "jwt", - { session: false }, - (req, res, err, user, info) => { - if (!user.admin) return res.code(401).send("unauthorized"); - req.user = user; - }, - ), +const extractToken = (c) => { + const authHeader = c.req.header("authorization"); + if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7); + return getCookie(c, "token") || null; }; -export const optional = { - preValidation: fastifyPassport.authenticate( - "jwt", - { session: false }, - (req, res, err, user, info) => { - req.user = user; - }, - ), -}; +const authenticate = async (c) => { + const token = extractToken(c); + if (!token) return null; -export const auth = { - preValidation: fastifyPassport.authenticate("jwt", { - authInfo: false, - session: false, - }), -}; + try { + const payload = jwt.verify(token, config.jwt); + let { id } = payload as any; + const url = c.req.path; + const method = c.req.method; -export const requirePin = async ({ body, user }) => { - if (!user || (user.pin && user.pin !== body.pin)) fail("Invalid pin"); + const wl = { GET: ["/invoice", "/payments"], POST: ["/invoice"] }; + if (id.endsWith("-ro") && wl[method]?.some((p) => url.startsWith(p))) id = id.slice(0, -3); + + return await getUser(id); + } catch { + return null; + } }; -export const jwtStrategy = new jwt.Strategy( - { - jwtFromRequest: jwt.ExtractJwt.fromExtractors([ - jwt.ExtractJwt.fromAuthHeaderAsBearerToken(), - (req) => (req.cookies ? req.cookies.token : null), - ]), - secretOrKey: config.jwt, - passReqToCallback: true, - }, - async (req, payload, next) => { - const { originalUrl: u, method: m } = req; +export const auth = async (c, next) => { + const user = await authenticate(c); + if (!user) return c.json("unauthorized", 401); + c.set("user", user); + await next(); +}; - let { id } = payload; - const wl = { GET: ["/invoice", "/payments"], POST: ["/invoice"] }; +export const optional = async (c, next) => { + const user = await authenticate(c); + if (user) c.set("user", user); + await next(); +}; - if (id.endsWith("-ro") && wl[m].some((p) => u.startsWith(p))) id = id.slice(0, -3); +export const admin = async (c, next) => { + const user = await authenticate(c); + if (!user?.admin) return c.json("unauthorized", 401); + c.set("user", user); + await next(); +}; - const user = await getUser(id); - next(null, user); - }, -); +export const requirePin = async ({ body, user }) => { + if (!user || (user.pin && user.pin !== body.pin)) fail("Invalid pin"); +}; diff --git a/lib/lightning.ts b/lib/lightning.ts index a259b601..3b1882d4 100644 --- a/lib/lightning.ts +++ b/lib/lightning.ts @@ -164,7 +164,7 @@ export async function replay(index) { } } -export const fixBolt12 = async (_, res) => { +export const fixBolt12 = async (c) => { for await (const k of scan("payment:*")) { const p = await g(k); if (p.type === "bolt12") { @@ -182,5 +182,5 @@ export const fixBolt12 = async (_, res) => { } } - res.send({}); + return c.json({}); }; diff --git a/lib/sockets.ts b/lib/sockets.ts index 88817728..51ba15fa 100644 --- a/lib/sockets.ts +++ b/lib/sockets.ts @@ -81,88 +81,77 @@ const track = async (ws, token) => { ws.user = user; }; -Bun.serve({ - hostname: "0.0.0.0", - port: 3120, - fetch(req, server) { - // upgrade the request to a WebSocket - if (server.upgrade(req)) { - return; // do not return a Response - } - return new Response("Upgrade failed", { status: 500 }); - }, - websocket: { - async message(ws: any, message: string) { - let type; - let data; +export const websocket = { + async message(ws: any, message: string) { + let type; + let data; - try { - ({ type, data } = JSON.parse(message)); - } catch (e) { - err("coudn't parse socket message"); - } + try { + ({ type, data } = JSON.parse(message)); + } catch (e) { + err("coudn't parse socket message"); + } - switch (type) { - case "heartbeat": - ws.beats = 0; - try { - if (data) await track(ws, data); - ws.send(JSON.stringify({ type: "id", data: ws.id })); - } catch (e) { - // err("Failed to send heartbeat", e.message); - } - break; + switch (type) { + case "heartbeat": + ws.beats = 0; + try { + if (data) await track(ws, data); + ws.send(JSON.stringify({ type: "id", data: ws.id })); + } catch (e) { + // err("Failed to send heartbeat", e.message); + } + break; - case "login": - try { - if (!data || data === "null" || !jwt.decode(data)) - return ws.close(code, `bad token ${data}`); + case "login": + try { + if (!data || data === "null" || !jwt.decode(data)) + return ws.close(code, `bad token ${data}`); - await track(ws, data); - } catch (e) { - if (e.message.includes("not found")) ws.send(JSON.stringify({ type: "logout" })); - setTimeout(() => ws?.close(code, `closing due to error ${e.message}`), 1000); - } - break; - case "token": - if (!ws.user) return; - ws.token = data; - break; - case "lnurl": - store.sessions[data] = ws; - break; - case "subscribe": - subscriptions.push({ invoice: data, ws }); - break; - default: - // warn("received socket message of unknown type", type, data); - } - }, - open(ws: any) { - const id = v4(); - ws.id = id; - ws.beats = 0; - ws.send(JSON.stringify({ type: "connected", data: id })); - all[id] = ws; - }, - close(ws: any) { - const { id } = ws; - try { - const uid = users[id]; - if (store.sockets[uid]?.[id]) { - delete store.sockets[uid][id]; + await track(ws, data); + } catch (e) { + if (e.message.includes("not found")) ws.send(JSON.stringify({ type: "logout" })); + setTimeout(() => ws?.close(code, `closing due to error ${e.message}`), 1000); } + break; + case "token": + if (!ws.user) return; + ws.token = data; + break; + case "lnurl": + store.sessions[data] = ws; + break; + case "subscribe": + subscriptions.push({ invoice: data, ws }); + break; + default: + // warn("received socket message of unknown type", type, data); + } + }, + open(ws: any) { + const id = v4(); + ws.id = id; + ws.beats = 0; + ws.send(JSON.stringify({ type: "connected", data: id })); + all[id] = ws; + }, + close(ws: any) { + const { id } = ws; + try { + const uid = users[id]; + if (store.sockets[uid]?.[id]) { + delete store.sockets[uid][id]; + } - let i; - ~(i = subscriptions.findIndex((s) => s.ws.id === id)) && subscriptions.splice(i, 1); + let i; + ~(i = subscriptions.findIndex((s) => s.ws.id === id)) && subscriptions.splice(i, 1); - if (all[id]) delete all[id]; - } catch (e) { - err("problem closing socket", e.message); - } - }, + if (all[id]) delete all[id]; + } catch (e) { + err("problem closing socket", e.message); + } }, -}); +}; export const sendHeartbeat = () => { for (const uid in store.sockets) { diff --git a/lib/upload.ts b/lib/upload.ts index 2ed5d905..900d49dd 100644 --- a/lib/upload.ts +++ b/lib/upload.ts @@ -5,14 +5,15 @@ import { bail, fail } from "$lib/utils"; import { fileTypeFromBuffer } from "file-type"; import sharp from "sharp"; -export default async (req, res) => { +export default async (c) => { try { - const { - params: { type }, - } = req; + const type = c.req.param("type"); - const data = await req.file(); - let buf = await data.toBuffer(); + const body = await c.req.parseBody(); + const file = body.file || Object.values(body).find((v) => v instanceof File); + if (!file) fail("no file uploaded"); + + let buf = Buffer.from(await (file as File).arrayBuffer()); const [format, ext] = (await fileTypeFromBuffer(buf)).mime.split("/"); @@ -26,9 +27,9 @@ export default async (req, res) => { const filePath = `/home/bun/app/data/uploads/${hash}.webp`; writeFileSync(filePath, buf); - res.send({ hash }); + return c.json({ hash }); } catch (e) { err("problem uploading", e.message); - bail(res, e.message); + return bail(c, e.message); } }; diff --git a/lib/utils.ts b/lib/utils.ts index 7a3d4c13..6f032cc7 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -20,7 +20,7 @@ export const wait = async (f, s = 300, n = 50) => { export const prod = process.env.NODE_ENV === "production"; -export const bail = (res, msg) => res.code(500).send(msg); +export const bail = (c, msg) => c.json(msg, 500); export const SATS = 100000000; export const sats = (n) => Math.round(n * SATS); diff --git a/package.json b/package.json index b8d15c1d..864a3347 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "dev": "nodemon --exec bun index.ts", "lint": "oxlint . && oxfmt --check .", "lint:fix": "oxlint --fix . && oxfmt .", - "format": "oxfmt .", - "prepare": "husky" + "format": "oxfmt ." }, "dependencies": { "@arkade-os/sdk": "0.3.13", @@ -17,14 +16,6 @@ "@aws-sdk/client-ses": "^3.997.0", "@cashu/cashu-ts": "^2.9.0", "@coinos/rpc": "^0.0.6", - "@fastify/cors": "^11.2.0", - "@fastify/http-proxy": "^11.4.1", - "@fastify/multipart": "^9.4.0", - "@fastify/passport": "^3.0.2", - "@fastify/rate-limit": "^10.3.0", - "@fastify/secure-session": "^8.3.0", - "@fastify/static": "^9.0.0", - "@fastify/websocket": "^11.2.0", "@noble/hashes": "^2.0.1", "@scure/base": "^2.0.0", "@scure/bip32": "^2.0.1", @@ -33,7 +24,7 @@ "bech32": "^2.0.0", "clightning-client": "github:asoltys/lightning-client-js#master", "eventsource": "^4.1.0", - "fastify": "^5.7.4", + "hono": "^4.7.0", "file-type": "^21.3.0", "fluent-ffmpeg": "^2.1.3", "got": "^14.6.6", @@ -44,8 +35,6 @@ "nostr": "^0.2.8", "nostr-tools": "^2.23.1", "otplib": "^12.0.1", - "passport-jwt": "^4.0.1", - "passport-local": "1.0.0", "pino": "^10.3.1", "pino-pretty": "^13.1.3", "pump": "^3.0.3", @@ -61,8 +50,6 @@ }, "devDependencies": { "@types/bun": "^1.3.9", - "husky": "^9.1.7", - "lint-staged": "^16.2.7", "nodemon": "^2.0.22", "oxfmt": "^0.35.0", "oxlint": "^1.50.0", diff --git a/routes/ecash.ts b/routes/ecash.ts index b4c16404..fc6a8d3a 100644 --- a/routes/ecash.ts +++ b/routes/ecash.ts @@ -11,7 +11,7 @@ import { v4 } from "uuid"; import { PaymentType } from "$lib/types"; const { ecash: type } = PaymentType; -Error.stackTraceLimit = 100; // Set this to the desired limit +Error.stackTraceLimit = 100; const sendCash = async ({ amount, user }) => { const id = v4(); @@ -28,39 +28,33 @@ const sendCash = async ({ amount, user }) => { }; export default { - async save(req, res) { - const { - body: { token }, - } = req; + async save(c) { + const { token } = await c.req.json(); try { const id = v4(); await s(`cash:${id}`, token); - res.send({ id }); + return c.json({ id }); } catch (e) { err(e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async get(req, res) { - const { - params: { id }, - } = req; + async get(c) { + const id = c.req.param("id"); try { const token = await get(id); const status = await check(token); - res.send({ token, status }); + return c.json({ token, status }); } catch (e) { err(e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async claim(req, res) { - const { - body: { token }, - user, - } = req; + async claim(c) { + const { token } = await c.req.json(); + const user = c.get("user"); try { const amount = await claim(token); @@ -79,25 +73,25 @@ export default { await credit({ hash, amount, memo, ref: user.id, type }); - res.send({ ok: true }); + return c.json({ ok: true }); } catch (e) { err(e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async mint(req, res) { - const { body, user } = req; + async mint(c) { + const body = await c.req.json(); + const user = c.get("user"); const amount = parseInt(body.amount); fail("disabled"); - res.send(await sendCash({ amount, user })); + return c.json(await sendCash({ amount, user })); }, - async melt(req, res) { - let { - body: { amount, bolt11: hash, preimage }, - user, - } = req; + async melt(c) { + const body = await c.req.json(); + const user = c.get("user"); + let { amount, bolt11: hash, preimage } = body; try { amount = Math.round(amount / 1000); const ref = preimage; @@ -133,15 +127,16 @@ export default { l(user.username, "sent", type, amount); emit(user.id, "payment", p); - res.send(p); + return c.json(p); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async receive(req, res) { + async receive(c) { try { - const { id, proofs, mint, memo } = req.body; + const body = await c.req.json(); + const { id, proofs, mint, memo } = body; const { uid: ref } = await getInvoice(id); const amount = await claim( @@ -153,11 +148,11 @@ export default { await credit({ hash: id, amount, memo, ref, type }); - res.send({ id }); + return c.json({ id }); } catch (e) { console.log(e); err(e.message); - bail(res, e.message); + return bail(c, e.message); } }, }; diff --git a/routes/email.ts b/routes/email.ts index 25c90ee7..ff6ffaeb 100644 --- a/routes/email.ts +++ b/routes/email.ts @@ -6,8 +6,9 @@ import { SESClient } from "@aws-sdk/client-ses"; import { SendEmailCommand } from "@aws-sdk/client-ses"; export default { - async send({ body }, res) { + async send(c) { try { + const body = await c.req.json(); const { email, message, username, token: response } = body; const Charset = "UTF-8"; @@ -48,9 +49,9 @@ export default { }), ); - res.send({ ok: true }); + return c.json({ ok: true }); } else { - bail(res, "failed captcha"); + return bail(c, "failed captcha"); } } catch (e) { console.log(e); diff --git a/routes/info.ts b/routes/info.ts index 35d1c315..adad02c6 100644 --- a/routes/info.ts +++ b/routes/info.ts @@ -5,23 +5,15 @@ import ln from "$lib/ln"; import { getDecodedToken } from "@cashu/cashu-ts"; export default { - async health(_, res) { + async health(c) { const status = getHealthStatus(); const httpStatus = status.healthy ? 200 : 503; - res.status(httpStatus).send(status); + return c.json(status, httpStatus); }, - async balances(_, res) { + async balances(c) { let total = 0; - // for await (const k of db.scanIterator({ MATCH: "balance:*" })) { - // total += parseInt(await db.get(k)); - // } - // - // for await (const k of archive.scanIterator({ MATCH: "balance:*" })) { - // total += parseInt(await archive.get(k)); - // } - const funds = await ln.listfunds(); const lnchannel = parseInt(funds.channels.reduce((a, b) => a + b.channel_sat, 0)); const lnwallet = parseInt(funds.outputs.reduce((a, b) => a + b.value, 0)); @@ -34,6 +26,6 @@ export default { lnwallet, }; - res.send(info); + return c.json(info); }, }; diff --git a/routes/invoices.ts b/routes/invoices.ts index 7f44a28d..6fc0b67a 100644 --- a/routes/invoices.ts +++ b/routes/invoices.ts @@ -6,11 +6,9 @@ import { bail, fail, fields, getAccount, getInvoice, getUser } from "$lib/utils" import rpc from "@coinos/rpc"; export default { - async get(req, res) { + async get(c) { try { - const { - params: { id }, - } = req; + const id = c.req.param("id"); if (id === "undefined") fail("invalid id"); const invoice = await getInvoice(id); @@ -21,36 +19,37 @@ export default { invoice.items ||= []; } - if (invoice) res.send(invoice); + if (invoice) return c.json(invoice); else fail("invoice not found"); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async create(req, res) { - let { body, user } = req; + async create(c) { + const body = await c.req.json(); + let user = c.get("user"); const { invoice } = body; if (body.user) user = body.user; - if (req.user.username === user.username) invoice.own = true; + if (c.get("user")?.username === user.username) invoice.own = true; else invoice.own = false; try { const result = await generate({ invoice, user }); - res.send(result); + return c.json(result); } catch (e) { console.trace(); console.log(e); - err("problem generating invoice", req.user?.username, body.user?.username, e.message); - bail(res, e.message); + err("problem generating invoice", c.get("user")?.username, body.user?.username, e.message); + return bail(c, e.message); } }, - async update(req, res) { + async update(c) { try { - const { id } = req.params; - const { body } = req; + const id = c.req.param("id"); + const body = await c.req.json(); const { tip, webhook, secret, received } = body.invoice; if (tip < 0) fail("Invalid tip"); @@ -61,7 +60,7 @@ export default { if (typeof tip !== "undefined") invoice.tip = tip; if (webhook && secret) { - if (invoice.uid !== req.user?.id) fail("Unauthorized"); + if (invoice.uid !== c.get("user")?.id) fail("Unauthorized"); invoice.webhook = webhook; invoice.secret = secret; } @@ -70,30 +69,32 @@ export default { await s(`invoice:${id}`, invoice); - res.send(invoice); + return c.json(invoice); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async list(req, res) { - const { id } = req.user; + async list(c) { + const user = c.get("user"); + const { id } = user; let invoices = await db.lRange(`${id}:invoices`, 0, -1); invoices = (await Promise.all(invoices.map((i) => getInvoice(i)))).filter(Boolean); - res.send(invoices); + return c.json(invoices); }, - async sign(req, res) { + async sign(c) { try { - const { address, message, type = "bitcoin" } = req.body; + const body = await c.req.json(); + const { address, message, type = "bitcoin" } = body; const node = rpc(config[type]); if (config[type].walletpass) await node.walletPassphrase(config[type].walletpass, 300); const signature = await node.signMessage({ address, message }); - res.send({ signature }); + return c.json({ signature }); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, }; diff --git a/routes/items.ts b/routes/items.ts index df1d86ea..b9706b4c 100644 --- a/routes/items.ts +++ b/routes/items.ts @@ -3,10 +3,8 @@ import { v4 } from "uuid"; import { bail, fail } from "$lib/utils"; export default { - async list(req, res) { - const { - params: { id: uid }, - } = req; + async list(c) { + const uid = c.req.param("id"); const items = []; for (const id of await db.lRange(`${uid}:items`, 0, -1)) { const item = await g(`item:${id}`); @@ -14,27 +12,24 @@ export default { else await db.lRem(`${uid}:items`, 0, id); } - res.send(items); + return c.json(items); }, - async get(req, res) { - const { - params: { id }, - } = req; + async get(c) { + const id = c.req.param("id"); try { const item = await g(`item:${id}`); if (!item) fail("Item not found"); - res.send(item); + return c.json(item); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async create(req, res) { - const { - body: item, - user: { id }, - } = req; + async create(c) { + const item = await c.req.json(); + const user = c.get("user"); + const { id } = user; try { if (!item.id) { item.id = v4(); @@ -46,33 +41,33 @@ export default { await s(`item:${item.id}`, item); - res.send(item); + return c.json(item); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async del(req, res) { - const { - body: { item }, - user: { id }, - } = req; + async del(c) { + const body = await c.req.json(); + const { item } = body; + const user = c.get("user"); + const { id } = user; try { const n = await db.lRem(`${id}:items`, 0, item.id); if (n) db.del(`item:${item.id}`); else fail("item not found"); - res.send({}); + return c.json({}); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async sort(req, res) { - const { - body: { items }, - user: { id }, - } = req; + async sort(c) { + const body = await c.req.json(); + const { items } = body; + const user = c.get("user"); + const { id } = user; try { await db.del(`${id}:items`); @@ -80,9 +75,9 @@ export default { await db.rPush(`${id}:items`, item.id); } - res.send({}); + return c.json({}); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, }; diff --git a/routes/lnurl.ts b/routes/lnurl.ts index cb74e2b5..9cd6e8d5 100644 --- a/routes/lnurl.ts +++ b/routes/lnurl.ts @@ -14,10 +14,8 @@ const host = URL.split("/").at(-1); const fiveMinutes = 1000 * 60 * 5; export default { - async encode(req, res) { - const { - query: { address }, - } = req; + async encode(c) { + const address = c.req.query("address"); const [name, domain] = address.split("@"); const url = `https://${domain}/.well-known/lnurlp/${name.toLowerCase().replace(/\s/g, "")}`; @@ -27,32 +25,29 @@ export default { } catch (e) { const m = `failed to lookup lightning address ${address}`; warn(m); - return bail(res, m); + return bail(c, m); } const enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000); - res.send(enc); + return c.json(enc); }, - async decode(req, res) { - const { - query: { text }, - } = req; + async decode(c) { + const text = c.req.query("text"); try { const url = Buffer.from(bech32.fromWords(bech32.decode(text, 20000).words)).toString(); const r = await got(url).json(); - res.send(r); + return c.json(r); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async lnurlp(req, res) { - const { - params: { username }, - query: { minSendable = 1000, maxSendable = 100000000000 }, - } = req; + async lnurlp(c) { + const username = c.req.param("username"); + const minSendable = c.req.query("minSendable") || 1000; + const maxSendable = c.req.query("maxSendable") || 100000000000; try { const user = await getUser( username.replace("lightning:", "").replace(/\s/g, "").replace("=", "").toLowerCase(), @@ -69,7 +64,7 @@ export default { const id = v4(); await s(`lnurl:${id}`, uid); - res.send({ + return c.json({ allowsNostr: true, minSendable, maxSendable, @@ -82,15 +77,15 @@ export default { } catch (e) { if (!e.message.includes("found")) warn("problem generating lnurlp request", username, e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async lnurl(req, res) { - const { - params: { id }, - query: { amount, comment, nostr }, - } = req; + async lnurl(c) { + const id = c.req.param("id"); + const amount = c.req.query("amount"); + const comment = c.req.query("comment"); + const nostr = c.req.query("nostr"); try { const iid = await g(`lnurl:${id}:invoice`); const uid = await g(`lnurl:${id}`); @@ -109,7 +104,6 @@ export default { if (nostr) { try { const event = JSON.parse(decodeURIComponent(nostr)); - // TODO: validate the event await s(`zap:${id}`, event); metadata = nostr; } catch (e) { @@ -128,31 +122,30 @@ export default { user, }); - res.send({ + return c.json({ pr: invoice.text, routes: [], verify: `${URL}/api/lnurl/verify/${invoice.id}`, }); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async verify(req, res) { - const { - params: { id }, - } = req; + async verify(c) { + const id = c.req.param("id"); const inv = await getInvoice(id); - if (!inv) return res.send({ status: "ERROR", reason: "Not found" }); + if (!inv) return c.json({ status: "ERROR", reason: "Not found" }); const { hash, received, amount, preimage } = inv; const settled = received >= amount; - res.send({ pr: hash, status: "OK", settled, preimage: preimage || null }); + return c.json({ pr: hash, status: "OK", settled, preimage: preimage || null }); }, - async pay(req, res) { - const { amount, username } = req.params; + async pay(c) { + const amount = c.req.param("amount"); + const username = c.req.param("username"); try { const user = await getUser(username); @@ -206,7 +199,7 @@ export default { await s(`lnurl:${id}`, uid); if (total > 0) await s(`lnurl:${id}:invoice`, invoice.id); - res.send({ + return c.json({ allowsNostr: true, minSendable: invoice.amount ? total : 1000, maxSendable: invoice.amount ? total : 10 * 1000 * SATS, @@ -219,7 +212,7 @@ export default { } catch (e) { if (!e.message.includes("found")) warn("problem generating lnurlp request", username, e.message); - bail(res, e.message); + return bail(c, e.message); } }, }; diff --git a/routes/locations.ts b/routes/locations.ts index 23b0f3b4..6dd364ec 100644 --- a/routes/locations.ts +++ b/routes/locations.ts @@ -1,19 +1,19 @@ import { db, g } from "$lib/db"; export default { - async list(_, res) { + async list(c) { const locations = await g("locations"); - res.send({ locations }); + return c.json({ locations }); }, - async nearby(req, res) { - const lat = parseFloat(req.query.lat); - const lon = parseFloat(req.query.lon); - const radius = parseFloat(req.query.radius) || 50; - const count = parseInt(req.query.count) || 50; + async nearby(c) { + const lat = parseFloat(c.req.query("lat")); + const lon = parseFloat(c.req.query("lon")); + const radius = parseFloat(c.req.query("radius")) || 50; + const count = parseInt(c.req.query("count")) || 50; if (isNaN(lat) || isNaN(lon)) { - return res.status(400).send({ error: "lat and lon required" }); + return c.json({ error: "lat and lon required" }, 400); } try { @@ -34,11 +34,11 @@ export default { if (loc) locations.push(loc); } - res.send({ locations }); + return c.json({ locations }); } catch (e) { console.log("nearby search failed", e); const locations = await g("locations"); - res.send({ locations }); + return c.json({ locations }); } }, }; diff --git a/routes/nostr.ts b/routes/nostr.ts index 4436a1c7..554a230c 100644 --- a/routes/nostr.ts +++ b/routes/nostr.ts @@ -22,20 +22,20 @@ import type { ProfilePointer } from "nostr-tools/nip19"; import { getZapEndpoint, makeZapRequest } from "nostr-tools/nip57"; export default { - async event(req, res) { + async event(c) { try { - let { id } = req.params; - const full = req.url.endsWith("full"); + let id = c.req.param("id"); + const full = c.req.url.endsWith("full"); if (id.startsWith("nevent")) id = decode(id).data.id; if (id.startsWith("note")) id = decode(id).data; const k = `event:${id}${full ? ":full" : ""}`; let event = await g(k); - if (event) return res.send(event); + if (event) return c.json(event); event = await get({ ids: [id] }); - if (!full) return res.send(event); + if (!full) return c.json(event); const parts = parseContent(event); @@ -98,15 +98,16 @@ export default { await db.set(k, JSON.stringify(event), { EX }); - res.send(event); + return c.json(event); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async parse(req, res) { - const { event } = req.body; + async parse(c) { + const body = await c.req.json(); + const { event } = body; const parts = parseContent(event); const names = {}; @@ -117,12 +118,12 @@ export default { } } - res.send({ parts, names }); + return c.json({ parts, names }); }, - async thread(req, res) { + async thread(c) { try { - const { id } = req.params; + const id = c.req.param("id"); const event = await get({ ids: [id] }); @@ -147,16 +148,16 @@ export default { } } - res.send(thread); + return c.json(thread); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async zaps(req, res) { + async zaps(c) { try { - let { id } = req.params; + let id = c.req.param("id"); if (id.startsWith("nevent")) id = decode(id).data.id; if (id.startsWith("note")) id = decode(id).data; const filter = { kinds: [9735], "#e": [id] }; @@ -165,7 +166,7 @@ export default { await sync("wss://relay.primal.net", filter); events = await scan(filter); } - if (!events.length) return res.send([]); + if (!events.length) return c.json([]); const zaps = []; for (const { tags } of events) { @@ -187,17 +188,19 @@ export default { } } - res.send(zaps.filter((z) => z.amount > 0)); + return c.json(zaps.filter((z) => z.amount > 0)); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async publish(req, res) { + async publish(c) { try { - const { event } = req.body; - const { pubkey } = req.user; + const body = await c.req.json(); + const { event } = body; + const user = c.get("user"); + const { pubkey } = user; await publish(event); @@ -206,17 +209,15 @@ export default { db.del(`${pubkey}:follows:n`); } - res.send({}); + return c.json({}); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async events(req, res) { - const { - params: { pubkey }, - } = req; + async events(c) { + const pubkey = c.req.param("pubkey"); try { const events = await q({ kinds: [1], authors: [pubkey], limit: 20 }); @@ -233,25 +234,25 @@ export default { } } - res.send(events); + return c.json(events); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async follows(req, res) { - const { - params: { pubkey }, - query: { limit = 20, offset = 0, pubkeysOnly }, - } = req; + async follows(c) { + const pubkey = c.req.param("pubkey"); + const limit = parseInt(c.req.query("limit") || "20"); + const offset = parseInt(c.req.query("offset") || "0"); + const pubkeysOnly = c.req.query("pubkeysOnly"); try { const k = `${pubkey}:follows${pubkeysOnly ? ":pubkeys" : ""}`; let follows = await g(k); - if (follows?.length) return res.send(follows); + if (follows?.length) return c.json(follows); const event = await get({ authors: [pubkey], kinds: [3] }); - if (!event) return res.send([]); + if (!event) return c.json([]); let pubkeys = event.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]); @@ -273,24 +274,23 @@ export default { await db.set(k, JSON.stringify(follows), { EX }); - res.send(follows); + return c.json(follows); } catch (e) { console.log("follows fail", e); - bail(res, e.message); + return bail(c, e.message); } }, - async followers(req, res) { - const { - params: { pubkey }, - query: { limit = 20, offset = 0 }, - } = req; + async followers(c) { + const pubkey = c.req.param("pubkey"); + const limit = parseInt(c.req.query("limit") || "20"); + const offset = parseInt(c.req.query("offset") || "0"); try { let followers = await g(`${pubkey}:followers`); - if (followers?.length) return res.send(followers); + if (followers?.length) return c.json(followers); const events = await q({ kinds: [3], "#p": [pubkey], limit }); - if (!events.length) return res.send([]); + if (!events.length) return c.json([]); const pubkeys = events.map((e) => e.pubkey).slice(offset, offset + limit); followers = ( @@ -306,28 +306,25 @@ export default { await db.set(`${pubkey}:followers`, JSON.stringify(followers), { EX }); - res.send(followers); + return c.json(followers); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async count(req, res) { + async count(c) { try { - const { pubkey } = req.params; - // res.send({ followers: 0, follows: 0 }); - res.send(await getCount(pubkey)); + const pubkey = c.req.param("pubkey"); + return c.json(await getCount(pubkey)); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async identities(req, res) { - const { - query: { name }, - } = req; + async identities(c) { + const name = c.req.query("name"); let names = {}; if (name) { names = { [name]: (await getUser(name, fields)).pubkey }; @@ -339,25 +336,26 @@ export default { } } - res.send({ names }); + return c.json({ names }); }, - async info(_, res) { - res.send({ pubkey: serverPubkey }); + async info(c) { + return c.json({ pubkey: serverPubkey }); }, - async profile(req, res) { - const { profile } = req.params; + async profile(c) { + const profile = c.req.param("profile"); const { data } = decode(profile); const { pubkey, relays } = data as ProfilePointer; const recipient = await getProfile(pubkey, relays); recipient.relays = relays; - res.send(recipient); + return c.json(recipient); }, - async zapRequest(req, res) { + async zapRequest(c) { try { - const { amount, id } = req.body; + const body = await c.req.json(); + const { amount, id } = body; const { pubkey } = await get({ ids: [id] }); const event = await makeZapRequest({ profile: pubkey, @@ -367,15 +365,16 @@ export default { comment: "", }); - res.send(event); + return c.json(event); } catch (e) { console.log(e); } }, - async zap(req, res) { + async zap(c) { try { - const { event } = req.body; + const body = await c.req.json(); + const { event } = body; const amount = event.tags.find((t) => t[0] === "amount")[1]; const pubkey = event.tags.find((t) => t[0] === "p")[1]; const content = JSON.stringify(await getProfile(pubkey)); @@ -386,10 +385,10 @@ export default { const url = `${callback}?amount=${amount}&nostr=${encodedEvent}`; const json = await got(url).json(); - res.send(json); + return c.json(json); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, }; diff --git a/routes/payments.ts b/routes/payments.ts index 33bf3381..ab7a6711 100644 --- a/routes/payments.ts +++ b/routes/payments.ts @@ -41,12 +41,13 @@ import got from "got"; import { v4 } from "uuid"; export default { - async info(_, res) { - res.send(await ln.getinfo()); + async info(c) { + return c.json(await ln.getinfo()); }, - async create(req, res) { - const { body, user } = req; + async create(c) { + const body = await c.req.json(); + const user = c.get("user"); let { amount, hash, fee, fund, memo, payreq, aid } = body; const balance = await getBalance(user.id); @@ -93,7 +94,6 @@ export default { await s(`payment:${p.id}`, p); await s(`payment:${txid}`, p.id); - // Instant vault notification try { const vaultInfo = await g(`arkaddr:${invoice.text}`); if (vaultInfo) { @@ -125,7 +125,6 @@ export default { throw e; } } else { - // Check if recipient account is non-custodial (vault) const recipientAccount = invoice?.aid ? await g(`account:${invoice.aid}`) : null; if (recipientAccount?.pubkey || recipientAccount?.seed) { p = await sendOnchain({ @@ -157,19 +156,24 @@ export default { } } - res.send(p); + return c.json(p); } catch (e) { warn(user.username, "payment failed", amount, balance, hash, payreq); err(e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async list(req, res) { - let { - user: { id }, - query: { aid, start, end, limit, offset, received }, - } = req; + async list(c) { + const user = c.get("user"); + let { id } = user; + let aid = c.req.query("aid"); + const start = c.req.query("start"); + const end = c.req.query("end"); + let limit = c.req.query("limit"); + let offset = c.req.query("offset"); + const received = c.req.query("received"); + if (!aid || aid === "undefined") aid = id; const index = await db.lPos(`${id}:accounts`, aid); @@ -244,30 +248,27 @@ export default { const { length: count } = payments; if (limit) payments = payments.slice(offset, offset + limit); - res.send({ payments, count, incoming, outgoing }); + return c.json({ payments, count, incoming, outgoing }); }, - async get(req, res) { + async get(c) { try { - const { - params: { hash }, - } = req; + const hash = c.req.param("hash"); const p = await getPayment(hash); if (p?.type === PaymentType.internal) p.with = await getUser(p.ref, fields); if (p?.type === PaymentType.fund) p.with = await getUser(p.uid, fields); - res.send(p); + return c.json(p); } catch (e) { console.log(e); err("failed to get payment", e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async parse(req, res) { - const { - body: { payreq }, - user, - } = req; + async parse(c) { + const body = await c.req.json(); + const { payreq } = body; + const user = c.get("user"); try { const hour = 1000 * 60 * 60; let nodes = await g("nodes"); @@ -297,20 +298,18 @@ export default { const covered = Math.min(creditBal, ourfee) || 0; ourfee -= covered; - res.send({ alias, amount, ourfee }); + return c.json({ alias, amount, ourfee }); } catch (e) { console.log(e); err("problem parsing", e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async fund(req, res) { - const { - params: { id }, - } = req; + async fund(c) { + const id = c.req.param("id"); const amount = await getFundBalance(id); - if (amount === null) return bail(res, "fund not found"); + if (amount === null) return bail(c, "fund not found"); let payments = (await db.lRange(`fund:${id}:payments`, 0, -1)) || []; payments = await Promise.all(payments.map((hash) => gf(`payment:${hash}`))); @@ -319,30 +318,14 @@ export default { payments = payments.filter((p) => p); const authorization = await g(`authorization:${id}`); - res.send({ amount, authorization: authorization?.amount, payments }); + return c.json({ amount, authorization: authorization?.amount, payments }); }, - // async withdraw(req, res) { - // const { - // params: { name }, - // } = req; - // const { user } = req; - // const balance = await g(`fund:${name}`); - // const managers = await db.sMembers(`fund:${name}:managers`); - // if (managers.length && !managers.includes(user.id)) fail("Unauthorized"); - // res.send({ - // tag: "withdrawRequest", - // callback: `${URL}/api/lnurlw`, - // k1: name, - // defaultDescription: `Withdraw from coinos fund ${name}`, - // minWithdrawable: balance > 0 ? 1000 : 0, - // maxWithdrawable: balance * 1000, - // }); - // }, - - async authorize(req, res) { - const { id: uid } = req.user; - const { id, fiat, currency, amount } = req.body; + async authorize(c) { + const user = c.get("user"); + const { id: uid } = user; + const body = await c.req.json(); + const { id, fiat, currency, amount } = body; const managers = await db.sMembers(`fund:${id}:managers`); if (managers.length && !managers.includes(uid)) fail("Unauthorized"); @@ -355,14 +338,13 @@ export default { }; await s(`authorization:${id}`, authorization); - res.send({}); + return c.json({}); }, - async take(req, res) { - let { - body: { id, amount, invoice: iid }, - user, - } = req; + async take(c) { + const body = await c.req.json(); + const user = c.get("user"); + let { id, amount, invoice: iid } = body; try { amount = Number.parseInt(amount); if (amount <= 0) fail("Invalid amount"); @@ -421,15 +403,15 @@ export default { await db.lPush(`fund:${id}:payments`, payment.id); - res.send(payment); + return c.json(payment); } catch (e) { warn("problem withdrawing from fund", user.username, e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async managers(req, res) { - const { name } = req.params; + async managers(c) { + const name = c.req.param("name"); const ids = await db.sMembers(`fund:${name}:managers`); @@ -437,12 +419,13 @@ export default { Boolean, ); - res.send(managers); + return c.json(managers); }, - async addManager(req, res) { - const { id, username } = req.body; - const { user } = req; + async addManager(c) { + const body = await c.req.json(); + const { id, username } = body; + const user = c.get("user"); const k = `fund:${id}:managers`; @@ -463,14 +446,15 @@ export default { if (!managers.length) managers = await Promise.all(ids.map(async (id) => await getUser(id, fields))); - res.send(managers); + return c.json(managers); }, - async deleteManager(req, res) { + async deleteManager(c) { try { - const { name } = req.params; - const { id: uid } = req.body; - const { user } = req; + const name = c.req.param("name"); + const body = await c.req.json(); + const { id: uid } = body; + const user = c.get("user"); const k = `fund:${name}:managers`; let managers = await db.sMembers(k); @@ -484,16 +468,15 @@ export default { const ids = await db.sMembers(k); managers = await Promise.all(ids.map(async (id) => await getUser(id, fields))); - res.send(managers); + return c.json(managers); } catch (e) {} }, - async confirm(req, res) { - const { - body: { txid, wallet, type }, - } = req; + async confirm(c) { + const body = await c.req.json(); + const { txid, wallet, type } = body; - if (type !== PaymentType.liquid) return res.send({}); + if (type !== PaymentType.liquid) return c.json({}); try { const node = rpc({ ...config[type], wallet }); @@ -546,17 +529,18 @@ export default { await completePayment(invoice, p, user); } } - res.send({}); + return c.json({}); } catch (e) { console.log(e); warn(`problem processing ${txid}`); - bail(res, e.message); + return bail(c, e.message); } }, - async txWebhook(req, res) { - const { txid, secret } = req.body || {}; - const headerSecret = req.headers["x-hook-secret"]; + async txWebhook(c) { + const body = await c.req.json(); + const { txid, secret } = body || {}; + const headerSecret = c.req.header("x-hook-secret"); const hookSecret = secret || headerSecret; try { @@ -566,59 +550,59 @@ export default { const tx = await getTx(txid); await processWatchedTx(tx); - res.send({}); + return c.json({}); } catch (e) { warn("problem processing tx webhook", e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async fee(req, res) { - const { body, user } = req; + async fee(c) { + const body = await c.req.json(); + const user = c.get("user"); try { - res.send(await build({ ...body, user })); + return c.json(await build({ ...body, user })); } catch (e) { warn("problem estimating fee", e.message, user.username, body.amount, body.address); let msg = e.message; if (msg.includes("500")) msg = ""; - bail(res, `Failed to prepare transaction ${msg}`); + return bail(c, `Failed to prepare transaction ${msg}`); } }, - async send(req, res) { - const { body, user } = req; + async send(c) { + const body = await c.req.json(); + const user = c.get("user"); try { await requirePin({ body, user }); const { hash: txid } = await sendOnchain({ ...body, user }); const pid = await g(`payment:${txid}`); const p = await g(`payment:${pid}`); - res.send(p); + return c.json(p); } catch (e) { warn(user.username, "payment failed", e.message); - res.code(500).send(e.message); + return c.json(e.message, 500); } }, - async freeze(req, res) { - const { - body: { secret }, - } = req; + async freeze(c) { + const body = await c.req.json(); + const { secret } = body; try { if (secret !== config.adminpass) fail("unauthorized"); await s("freeze", true); - res.send("ok"); + return c.json("ok"); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async print(req, res) { - const { - body: { id }, - user, - } = req; + async print(c) { + const body = await c.req.json(); + const { id } = body; + const user = c.get("user"); try { const p = await gf(`payment:${id}`); if (!p) fail("Payment not found"); @@ -629,18 +613,17 @@ export default { mqtt.publish(username, `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}`); - res.send({ ok: true }); + return c.json({ ok: true }); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async lnaddress(req, res) { - let { - params: { lnaddress, amount }, - body, - user, - } = req; + async lnaddress(c) { + const body = await c.req.json(); + const user = c.get("user"); + let lnaddress = c.req.param("lnaddress"); + let amount = c.req.param("amount"); const { fee } = body; try { lnaddress = decodeURIComponent(lnaddress); @@ -667,27 +650,25 @@ export default { await credit({ hash: pr, amount, memo, ref: user.id }); } else p = await sendLightning({ user, pr, amount, fee, memo }); - res.send(p); + return c.json(p); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async gateway(req, res) { - const { - body: { short_channel_id, webhook }, - } = req; + async gateway(c) { + const body = await c.req.json(); + const { short_channel_id, webhook } = body; await s(short_channel_id, webhook); - res.send({ ok: true }); + return c.json({ ok: true }); }, - async replace(req, res) { - const { - body: { id }, - user, - } = req; + async replace(c) { + const body = await c.req.json(); + const { id } = body; + const user = c.get("user"); try { const p = await gf(`payment:${id}`); if (!p) fail("Payment not found"); @@ -724,47 +705,50 @@ export default { if (!r[0].allowed) fail(`transaction rejected ${p.hex}`); warn("bump", user.username, p.hex); - res.send({ ok: true }); + return c.json({ ok: true }); } catch (e) { err("failed to bump payment", id, e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async internal(req, res) { - const { - body: { username, amount }, - user: sender, - } = req; + async internal(c) { + const body = await c.req.json(); + const { username, amount } = body; + const sender = c.get("user"); const recipient = await getUser(username); - res.send(await sendInternal({ amount, sender, recipient })); + return c.json(await sendInternal({ amount, sender, recipient })); }, - async decode(req, res) { - const { bolt11 } = req.params; - res.send(await ln.decode(bolt11)); + async decode(c) { + const bolt11 = c.req.param("bolt11"); + return c.json(await ln.decode(bolt11)); }, - async fetchinvoice(req, res) { - const { amount, offer } = req.body; - res.send(await ln.fetchinvoice(offer, amount ? amount * 1000 : null)); + async fetchinvoice(c) { + const body = await c.req.json(); + const { amount, offer } = body; + return c.json(await ln.fetchinvoice(offer, amount ? amount * 1000 : null)); }, - async auth(req, res) { - console.log(req.query); - res.send(req.query); + async auth(c) { + const query = c.req.query(); + console.log(query); + return c.json(query); }, - async order(req, res) { - console.log(req.body); - res.send(req.body); + async order(c) { + const body = await c.req.json(); + console.log(body); + return c.json(body); }, - async sendinvoice(req, res) { + async sendinvoice(c) { try { - const { user } = req; - const { invreq } = req.body; + const user = c.get("user"); + const body = await c.req.json(); + const { invreq } = body; const { amount_msat, bolt12, pay_index } = await ln.sendinvoice({ invreq, @@ -782,14 +766,15 @@ export default { const p = await replay(pay_index); - res.send(p); + return c.json(p); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async ark(req, res) { - const { body, user } = req; + async ark(c) { + const body = await c.req.json(); + const user = c.get("user"); const { address, amount, aid } = body; try { @@ -812,16 +797,11 @@ export default { await s(`payment:${p.id}`, p); await s(`payment:${txid}`, p.id); - // Instant vault notification: if destination is a vault on our platform, - // create the receive payment immediately so the client doesn't have to - // wait for ASP indexer + arkSync backoff delays try { const vaultInfo = await g(`arkaddr:${address}`); if (vaultInfo) { const { aid: vaultAid, uid: vaultUid } = vaultInfo; - // Skip notification if this is a forward (e.g. vault → lightning); - // the funds are transient and will leave again immediately const isForward = await g(`custodial-ark-invoice:${address}`); const vaultOwner = await g(`user:${vaultUid}`); @@ -848,16 +828,17 @@ export default { throw e; } - res.send(p); + return c.json(p); } catch (e) { warn(user.username, "ark payment failed", e.message); - res.code(500).send(e.message); + return c.json(e.message, 500); } }, - async arkVaultSend(req, res) { + async arkVaultSend(c) { try { - const { body, user } = req; + const body = await c.req.json(); + const user = c.get("user"); const { hash, amount, aid } = body; const { rate, currency } = await getUserRate(user); @@ -871,15 +852,16 @@ export default { mapHashToId: true, }); - res.send(p); + return c.json(p); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async arkVaultReceive(req, res) { + async arkVaultReceive(c) { try { - const { body, user } = req; + const body = await c.req.json(); + const user = c.get("user"); const { amount, hash, aid } = body; if (amount <= 0) fail("Invalid amount"); @@ -895,19 +877,20 @@ export default { currency, }); - res.send(p); + return c.json(p); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async arkAddress(_, res) { - res.send(await getArkAddress()); + async arkAddress(c) { + return c.json(await getArkAddress()); }, - async arkReceive(req, res) { + async arkReceive(c) { try { - const { body, user } = req; + const body = await c.req.json(); + const user = c.get("user"); const { iid, amount, hash } = body; const { id: uid } = user; @@ -921,7 +904,6 @@ export default { if (hash) { await acquireArkLock(hash); - // Skip VTXO verification for vault→custodial (same user, debit handled by arkVaultSend) if (!(aid && aid !== uid)) { if (!(await verifyArkVtxo(hash))) fail("VTXO not found in server wallet"); } @@ -931,7 +913,6 @@ export default { const { rates } = await getUserRate(user); const rate = rates[currency]; - // Always credit custodial balance (Ark funds received by server) await tbCredit(uid, uid, type, amount, false); const p = await createArkPayment({ @@ -951,26 +932,25 @@ export default { await completePayment(invoice, p, user); - // Clean up custodial ark invoice index if (invoice.text) await db.del(`custodial-ark-invoice:${invoice.text}`); - res.send(p); + return c.json(p); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async arkSync(req, res) { + async arkSync(c) { try { - const { user } = req; - const { transactions = [], aid } = req.body; + const user = c.get("user"); + const body = await c.req.json(); + const { transactions = [], aid } = body; const { id: uid } = user; - l("arkSync", user.username, aid, "txs:", transactions.length, "bal:", req.body.balance); + l("arkSync", user.username, aid, "txs:", transactions.length, "bal:", body.balance); - // Per-account lock prevents concurrent syncs from racing const lockKey = `arksynclock:${aid}`; const gotLock = await db.set(lockKey, "1", { NX: true, EX: 30 }); - if (!gotLock) return res.send({ synced: 0, received: 0, payments: [] }); + if (!gotLock) return c.json({ synced: 0, received: 0, payments: [] }); try { const { rate, currency } = await getUserRate(user); @@ -983,7 +963,6 @@ export default { const hashes = [tx.arkTxid, tx.commitmentTxid, tx.hash].filter(Boolean); if (!hashes.length) continue; - // Check if any hash is already known let found = false; for (const h of hashes) { const existingId = await g(`payment:${aid}:${h}`); @@ -999,8 +978,6 @@ export default { } if (found) continue; - // Skip sent/zero transactions not recorded via vault-send — these are - // from internal wallet operations (settle, refresh) not real sends if (tx.amount <= 0) continue; const primaryHash = hashes[0]; @@ -1021,7 +998,6 @@ export default { if (tx.amount > 0) { received += tx.amount; - // Find an unpaid ark invoice for this account and mark it received const invoiceIds = await db.lRange(`${aid}:invoices`, 0, 20); for (const iid of invoiceIds) { const inv = await getInvoice(iid); @@ -1038,8 +1014,7 @@ export default { } } - // Reconcile expired VTXOs - const { balance } = req.body; + const { balance } = body; if (typeof balance === "number" && balance >= 0) { const paymentIds = await db.lRange(`${aid}:payments`, 0, -1); let expectedBalance = 0; @@ -1049,8 +1024,6 @@ export default { const pay = await g(`payment:${pid}`); if (pay) { expectedBalance += pay.amount; - // Skip reconciliation if there are recent payments the SDK - // may not have indexed yet (e.g. instant vault credits) if (pay.amount > 0 && now - pay.created < 120_000) { hasRecentPayments = true; } @@ -1075,7 +1048,6 @@ export default { } } - // Check for pending custodial ark invoice to forward let forward; if (received > 0) { const account = await g(`account:${aid}`); @@ -1098,12 +1070,12 @@ export default { } } - res.send({ synced, received, payments, forward }); + return c.json({ synced, received, payments, forward }); } finally { await db.del(lockKey); } } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, }; diff --git a/routes/rates.ts b/routes/rates.ts index e532ec78..d4426a6e 100644 --- a/routes/rates.ts +++ b/routes/rates.ts @@ -2,17 +2,17 @@ import { g } from "$lib/db"; import { rate } from "$lib/rates"; export default { - async fx(_, res) { + async fx(c) { const { fx } = await g("fx"); - res.send({ fx }); + return c.json({ fx }); }, - async last(_, res) { - res.send(rate || (await g("rate"))); + async last(c) { + return c.json(rate || (await g("rate"))); }, - async index(_, res) { + async index(c) { const { date, fx, ...rates } = await g("rates"); - res.send(rates); + return c.json(rates); }, }; diff --git a/routes/shopify.ts b/routes/shopify.ts index d955d73d..abd7c81b 100644 --- a/routes/shopify.ts +++ b/routes/shopify.ts @@ -3,21 +3,20 @@ import { g } from "$lib/db"; import { l, err } from "$lib/logging"; import { getPayment } from "$lib/utils"; -const query = `mutation orderMarkAsPaid($input: OrderMarkAsPaidInput!) { - orderMarkAsPaid(input: $input) { - order { id } - userErrors { +const query = `mutation orderMarkAsPaid($input: OrderMarkAsPaidInput!) { + orderMarkAsPaid(input: $input) { + order { id } + userErrors { field message } } }`; -export default async (req, res) => { - const { - body: { hash }, - params: { id }, - } = req; +export default async (c) => { + const body = await c.req.json(); + const { hash } = body; + const id = c.req.param("id"); const p = await getPayment(hash); const user = await g(`user:${p.uid}`); @@ -39,7 +38,7 @@ export default async (req, res) => { l("shopify success", r); - res.send(r); + return c.json(r); } catch (e) { err("problem marking shopify order as paid", e.message); } diff --git a/routes/square.ts b/routes/square.ts index f738e1ef..269cdd0d 100644 --- a/routes/square.ts +++ b/routes/square.ts @@ -12,11 +12,11 @@ import { v4 } from "uuid"; const { scopes, url, appId, clientSecret, environment } = config.square; export default { - async connect(req, res) { - const { user } = req; + async connect(c) { + const user = c.get("user"); if (await db.exists(`${user.id}:square`)) { - return res.send("connected"); + return c.json("connected"); } const codeVerifier = base64.encode(crypto.randomBytes(32)); @@ -27,13 +27,13 @@ export default { const state = base64.encode(crypto.randomBytes(12)); const scope = scopes.join("+"); - res.send( + return c.json( `${url}oauth2/authorize?client_id=${appId}&session=false&scope=${scope}&state=${state}&code_challenge=${challenge}`, ); }, - async auth(req, res) { - const { user } = req; + async auth(c) { + const user = c.get("user"); const codeVerifier = await g(`${user.id}:codeVerifier`); @@ -41,7 +41,7 @@ export default { environment, }); - const { code } = req.query; + const code = c.req.query("code"); try { const result = await client.oAuth.obtainToken({ @@ -53,15 +53,15 @@ export default { await s(`${user.id}:square`, result); await s(result.merchantId, user.id); - res.send({}); + return c.json({}); } catch (e) { console.log(e); } }, - async payment(req, res) { + async payment(c) { try { - const { body } = req; + const body = await c.req.json(); const { data, type, merchant_id } = body; const { payment } = data.object; @@ -86,9 +86,9 @@ export default { await generate({ invoice, user }); } - res.send({}); + return c.json({}); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, }; diff --git a/routes/users.ts b/routes/users.ts index 07e4fb85..f3b747af 100644 --- a/routes/users.ts +++ b/routes/users.ts @@ -27,6 +27,7 @@ import { getPublicKey, nip19, verifyEvent } from "nostr-tools"; import { hexToBytes } from "@noble/hashes/utils.js"; import { authenticator } from "otplib"; import { v4 } from "uuid"; +import { setCookie } from "hono/cookie"; import { PaymentType } from "$lib/types"; import { createBalanceAccount, getBalance, getCredit, getPending } from "$lib/tb"; @@ -36,16 +37,14 @@ import type { ProfilePointer } from "nostr-tools/nip19"; const { host } = new URL(process.env.URL); const relay = encodeURIComponent(config.publicRelay); -const verifyRecaptcha = async (response, req?) => { +const verifyRecaptcha = async (response, c?, body?) => { const { recaptcha: secret } = config; if (!secret) return true; - // Skip captcha for Tor users accessing via .onion - const host = req?.headers?.["host"] || ""; - if (host.endsWith(".onion")) return true; + const reqHost = c?.req?.header("host") || ""; + if (reqHost.endsWith(".onion")) return true; - // Skip captcha for allowlisted User-Agents (prefix match) - const uaRaw = req?.headers?.["user-agent"] || req?.headers?.["User-Agent"]; + const uaRaw = c?.req?.header("user-agent"); const ua = Array.isArray(uaRaw) ? uaRaw[0] : uaRaw; if (typeof ua === "string") { const prefixes = await db.sMembers("nocaptcha_ua"); @@ -55,20 +54,19 @@ const verifyRecaptcha = async (response, req?) => { } } - // Check for valid API key to bypass captcha - const apiKey = req?.headers?.["x-api-key"]; + const apiKey = c?.req?.header("x-api-key"); if (apiKey && (await db.sIsMember("apikeys", apiKey))) { return true; } - const { username } = req.body; + const { username } = body || {}; if (username && (await db.sIsMember("nocaptcha", username.toLowerCase().replace(/\s/g, "")))) return true; if (!response) return false; try { - const ip = req?.headers?.["cf-connecting-ip"] || req?.socket?.remoteAddress; + const ip = c?.req?.header("cf-connecting-ip") || c?.env?.ip; const { success } = await got .post("https://www.google.com/recaptcha/api/siteverify", { form: { @@ -87,8 +85,8 @@ const verifyRecaptcha = async (response, req?) => { export default { upload, - async me(req, res) { - const { user } = req; + async me(c) { + const user = c.get("user"); try { user.balance = await getBalance(user.id); user.locked = await ga(`balance:${user.id}`); @@ -104,15 +102,15 @@ export default { user.prompt = !!user.prompt; if (user.pubkey) user.npub = nip19.npubEncode(user.pubkey); - res.send(pick(user, whitelist)); + return c.json(pick(user, whitelist)); } catch (e) { console.log("problem fetching user", e); - res.code(500).send(e.message); + return c.json(e.message, 500); } }, - async list(req, res) { - const { user } = req; + async list(c) { + const user = c.get("user"); if (!user.admin) fail("unauthorized"); const users = []; @@ -140,13 +138,11 @@ export default { users.push(u); } - res.send(users); + return c.json(users); }, - async get(req, res) { - let { - params: { key }, - } = req; + async get(c) { + let key = c.req.param("key"); key = key.toLowerCase().replace(/\s/g, ""); try { if (key.startsWith("npub")) { @@ -162,43 +158,43 @@ export default { } const user = await getNostrUser(key); - res.send(pick(user, fields)); + return c.json(pick(user, fields)); } catch (e) { err("problem getting user", key, e.message); - res.code(500).send(e.message); + return c.json(e.message, 500); } }, - async create(req, res) { - const { body, headers } = req; + async create(c) { + const body = await c.req.json(); + const headers = c.req.header(); try { const ip = headers["cf-connecting-ip"]; if (!body.user) fail("no user object provided"); let { user } = body; - const fields = ["pubkey", "password", "username", "picture", "fresh", "authPubkey"]; - user = await register(pick(user, fields), ip); + const flds = ["pubkey", "password", "username", "picture", "fresh", "authPubkey"]; + user = await register(pick(user, flds), ip); const payload = { id: user.id }; const token = jwt.sign(payload, config.jwt); l("registered new user", user.username); - res.send({ ...pick(user, whitelist), sk: user.sk, token }); + return c.json({ ...pick(user, whitelist), sk: user.sk, token }); } catch (e) { err("problem registering", e.message); - res.code(500).send(e.message); + return c.json(e.message, 500); } }, - async disable2fa(req, res) { - const { - user, - body: { token }, - } = req; + async disable2fa(c) { + const user = c.get("user"); + const body = await c.req.json(); + const { token } = body; const { id, twofa, username, otpsecret } = user; if (twofa && !authenticator.check(token, otpsecret)) { - return res.code(401).send("2fa required"); + return c.json("2fa required", 401); } user.twofa = false; @@ -206,15 +202,14 @@ export default { emit(username, "user", user); emit(username, "otpsecret", user.otpsecret); l("disabled 2fa", username); - res.send({}); + return c.json({}); }, - async enable2fa(req, res) { + async enable2fa(c) { try { - const { - user, - body: { token }, - } = req; + const user = c.get("user"); + const body = await c.req.json(); + const { token } = body; const { id, otpsecret, username } = user; const isValid = authenticator.check(token, otpsecret); if (isValid) { @@ -222,21 +217,22 @@ export default { await s(`user:${id}`, user); emit(username, "user", user); } else { - return res.code(500).send("Invalid token"); + return c.json("Invalid token", 500); } l("enabled 2fa", username); - res.send({}); + return c.json({}); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async update(req, res) { - const { user, body } = req; + async update(c) { + const user = c.get("user"); + const body = await c.req.json(); try { - const { id: tokid } = jwt.verify(req.headers.authorization.split(" ")[1], config.jwt); + const { id: tokid } = jwt.verify(c.req.header("authorization").split(" ")[1], config.jwt); l("updating user", user.username, tokid); if (user.id !== tokid) fail("unauthorized"); @@ -265,8 +261,8 @@ export default { const event = JSON.parse(body.event); const challenge = event.tags.find((t) => t[0] === "challenge")[1]; - const c = await g(`challenge:${challenge}`); - if (!c) fail("Invalid or expired challenge"); + const ch = await g(`challenge:${challenge}`); + if (!ch) fail("Invalid or expired challenge"); if (!verifyEvent(event) || event.pubkey !== pubkey) fail("Invalid signature or challenge mismatch."); @@ -361,27 +357,28 @@ export default { if (user.nip5) await db.sAdd("nip5", `${user.username}:${user.pubkey}`); emit(user.id, "user", user); - res.send({ user }); + return c.json({ user }); } catch (e) { console.log(e); warn("failed to update", user.username, e.message); - bail(res, e.message); + return bail(c, e.message); } }, - async login(req, res) { + async login(c) { try { - let { username, password, token: twofa, recaptcha } = req.body; - const ip = req.headers["cf-connecting-ip"] || req.socket.remoteAddress; + const body = await c.req.json(); + let { username, password, token: twofa, recaptcha } = body; + const ip = c.req.header("cf-connecting-ip") || c.env?.ip || "unknown"; const ipKey = `ip:${ip}:login`; const ipCount = await db.incr(ipKey); if (ipCount === 1) await db.expire(ipKey, 10); - if (ipCount > 30) return res.code(429).send({}); + if (ipCount > 30) return c.json({}, 429); - const recaptchaOk = await verifyRecaptcha(recaptcha, req); + const recaptchaOk = await verifyRecaptcha(recaptcha, c, body); if (!recaptchaOk) { - return res.code(401).send("failed captcha"); + return c.json("failed captcha", 401); } username = username.toLowerCase().replace(/\s/g, ""); @@ -391,7 +388,7 @@ export default { const failures = await g(fk); const ipFailKey = `ip:${ip}:login:fail`; const ipFailures = await g(ipFailKey); - if (ipFailures > 20) return res.code(429).send({}); + if (ipFailures > 20) return c.json({}, 429); let user = await getUser(username); @@ -401,7 +398,7 @@ export default { if (!(await db.ttl(ipFailKey))) await db.expire(ipFailKey, 600); await db.incrBy(fk, 1); setTimeout(() => db.decrBy(fk, 1), 120000); - return res.code(401).send({}); + return c.json({}, 401); } if ( @@ -410,56 +407,57 @@ export default { ) { await db.incrBy(ipFailKey, 1); if (!(await db.ttl(ipFailKey))) await db.expire(ipFailKey, 600); - return res.code(401).send("2fa required"); + return c.json("2fa required", 401); } } - if (req.body.authPubkey && !user.authPubkey) { - user.authPubkey = req.body.authPubkey; + if (body.authPubkey && !user.authPubkey) { + user.authPubkey = body.authPubkey; await s(`user:${user.id}`, user); } - if (username !== "coinos") l("logged in", username, req.headers["cf-connecting-ip"]); + if (username !== "coinos") l("logged in", username, c.req.header("cf-connecting-ip")); const payload = { id: user.id }; const token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + setCookie(c, "token", token, { expires: new Date(Date.now() + 432000000), path: "/" }); user = pick(user, whitelist); - res.send({ user, token }); + return c.json({ user, token }); } catch (e) { console.log(e); - err("login error", e.message, req.socket.remoteAddress); - res.code(401).send({}); + err("login error", e.message, c.env?.ip); + return c.json({}, 401); } }, - async challenge(req, res) { + async challenge(c) { const id = v4(); await db.set(`challenge:${id}`, id, { EX: 300 }); - const username = req.query?.username; + const username = c.req.query("username"); let hasAuthKey = false; if (username) { const user = await getUser(username.toLowerCase().replace(/\s/g, "")); if (user?.authPubkey) hasAuthKey = true; } - res.send({ challenge: id, hasAuthKey }); + return c.json({ challenge: id, hasAuthKey }); }, - async authKeyLogin(req, res) { + async authKeyLogin(c) { try { - const { event, challenge, username: rawUsername, twofa, recaptcha } = req.body; - const ip = req.headers["cf-connecting-ip"] || req.socket.remoteAddress; + const body = await c.req.json(); + const { event, challenge, username: rawUsername, twofa, recaptcha } = body; + const ip = c.req.header("cf-connecting-ip") || c.env?.ip || "unknown"; const ipKey = `ip:${ip}:login`; const ipCount = await db.incr(ipKey); if (ipCount === 1) await db.expire(ipKey, 10); - if (ipCount > 30) return res.code(429).send({}); + if (ipCount > 30) return c.json({}, 429); - const recaptchaOk = await verifyRecaptcha(recaptcha, req); - if (!recaptchaOk) return res.code(401).send("failed captcha"); + const recaptchaOk = await verifyRecaptcha(recaptcha, c, body); + if (!recaptchaOk) return c.json("failed captcha", 401); - const c = await g(`challenge:${challenge}`); - if (!c) fail("Invalid or expired challenge"); + const ch = await g(`challenge:${challenge}`); + if (!ch) fail("Invalid or expired challenge"); const { kind } = event; if (kind !== 27235) fail("Invalid event"); @@ -477,35 +475,36 @@ export default { user.twofa && (typeof twofa === "undefined" || !authenticator.check(twofa, user.otpsecret)) ) { - return res.code(401).send("2fa required"); + return c.json("2fa required", 401); } l("authkey login", username, ip); const payload = { id: user.id }; const token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + setCookie(c, "token", token, { expires: new Date(Date.now() + 432000000), path: "/" }); user = pick(user, whitelist); - res.send({ user, token }); + return c.json({ user, token }); } catch (e) { console.log(e); - err("authkey login error", e.message, req.socket.remoteAddress); - res.code(401).send({}); + err("authkey login error", e.message, c.env?.ip); + return c.json({}, 401); } }, - async nostrAuth(req, res) { + async nostrAuth(c) { try { - const { event, challenge, twofa, recaptcha } = req.body; - const ip = req.headers["cf-connecting-ip"]; - const recaptchaOk = await verifyRecaptcha(recaptcha, req); + const body = await c.req.json(); + const { event, challenge, twofa, recaptcha } = body; + const ip = c.req.header("cf-connecting-ip"); + const recaptchaOk = await verifyRecaptcha(recaptcha, c, body); if (!recaptchaOk) { - return res.code(401).send("failed captcha"); + return c.json("failed captcha", 401); } - const c = await g(`challenge:${challenge}`); + const ch = await g(`challenge:${challenge}`); const { pubkey: key, kind } = event; if (kind !== 27235) fail("Invalid event"); - if (!c) fail("Invalid or expired login challenge"); + if (!ch) fail("Invalid or expired login challenge"); if (!verifyEvent(event) || event.tags.find((t) => t[0] === "challenge")?.[1] !== challenge) fail("Invalid signature or challenge mismatch."); @@ -536,86 +535,89 @@ export default { const payload = { id: user.id }; const token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + setCookie(c, "token", token, { expires: new Date(Date.now() + 432000000), path: "/" }); user = pick(user, whitelist); - res.send({ user, token }); + return c.json({ user, token }); } catch (e) { console.log(e); - err("nostr login error", e.message, req.socket.remoteAddress); - res.code(401).send({}); + err("nostr login error", e.message, c.env?.ip); + return c.json({}, 401); } }, - async subscriptions(req, res) { + async subscriptions(c) { try { - const { user } = req; + const user = c.get("user"); const subscriptions = await db.sMembers(`${user.id}:subscriptions`); - res.send(subscriptions); + return c.json(subscriptions); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async subscription(req, res) { + async subscription(c) { try { - const { subscription } = req.body; - const { id } = req.user; + const body = await c.req.json(); + const { subscription } = body; + const { id } = c.get("user"); await db.sAdd(`${id}:subscriptions`, JSON.stringify(subscription)); - res.send(subscription); + return c.json(subscription); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async deleteSubscription(req, res) { + async deleteSubscription(c) { try { - const { subscription } = req.body; - const { id } = req.user; + const body = await c.req.json(); + const { subscription } = body; + const { id } = c.get("user"); await db.sRem(`${id}:subscriptions`, JSON.stringify(subscription)); - res.send(subscription); + return c.json(subscription); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async password(req, res) { - const { - body: { password }, - user, - } = req; - if (!user.password) return res.send(true); + async password(c) { + const body = await c.req.json(); + const { password } = body; + const user = c.get("user"); + if (!user.password) return c.json(true); try { if (!password) fail("password not provided"); - res.send(await Bun.password.verify(password, user.password)); + return c.json(await Bun.password.verify(password, user.password)); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async pin(req, res) { - const { - body: { pin }, - user, - } = req; - res.send(!user.pin || user.pin === pin); + async pin(c) { + const body = await c.req.json(); + const { pin } = body; + const user = c.get("user"); + return c.json(!user.pin || user.pin === pin); }, - async otpsecret(req, res) { + async otpsecret(c) { try { - await requirePin(req); - const { otpsecret, username } = req.user; - res.send({ secret: otpsecret, username }); + const user = c.get("user"); + const body = await c.req.json(); + await requirePin({ body, user }); + const { otpsecret, username } = user; + return c.json({ secret: otpsecret, username }); } catch (e) { - res.code(500).send(e.message); + return c.json(e.message, 500); } }, - async contacts(req, res) { - const { params, user } = req; + async contacts(c) { + const user = c.get("user"); const { id } = user; + const limit = c.req.param("limit"); const lastlen = (await g(`${id}:lastlen`)) || 0; const len = await db.lLen(`${id}:payments`); const payments = (await db.lRange(`${id}:payments`, 0, len - lastlen)) || []; @@ -654,24 +656,21 @@ export default { c.trusted = true; }); - let { limit } = params; - limit ||= contacts.length; + let contactLimit = limit || contacts.length; contacts = contacts.filter((c) => !pins.includes(c.id)); - contacts = contacts.slice(0, limit); + contacts = contacts.slice(0, contactLimit); const combined = [...pinned, ...contacts]; - res.send(combined); + return c.json(combined); }, - async del(req, res) { - let { - params: { username }, - headers: { authorization }, - } = req; + async del(c) { + let username = c.req.param("username"); + const authorization = c.req.header("authorization"); fail("Unauthorized"); username = username.toLowerCase(); - if (!authorization?.includes(config.admin)) return res.code(401).send("unauthorized"); + if (!authorization?.includes(config.admin)) return c.json("unauthorized", 401); const { id, pubkey } = await g( `user:${await g(`user:${username.replace(/\s/g, "").toLowerCase()}`)}`, @@ -685,14 +684,13 @@ export default { db.del(`user:${id}`); db.del(`user:${pubkey}`); - res.send({}); + return c.json({}); }, - async reset(req, res) { - const { - body: { code, username, password }, - user: u, - } = req; + async reset(c) { + const body = await c.req.json(); + const { code, username, password } = body; + const u = c.get("user"); try { const admin = u?.admin; let id; @@ -704,7 +702,7 @@ export default { if (!user) fail("user not found"); - warn("password reset", user.username, code, req.headers["cf-connecting-ip"]); + warn("password reset", user.username, code, c.req.header("cf-connecting-ip")); user.pin = null; user.nsec = null; @@ -718,40 +716,38 @@ export default { await s(`user:${id}`, user); await db.del(`reset:${code}`); - res.send(pick(user, whitelist)); + return c.json(pick(user, whitelist)); } catch (e) { - err("password reset failed", e.message, req.headers["cf-connecting-ip"]); - bail(res, e.message); + err("password reset failed", e.message, c.req.header("cf-connecting-ip")); + return bail(c, e.message); } }, - async printerlogin(req, res) { - const { - body: { username, topic }, - } = req; - if (username === topic) res.send({ ok: true }); - else bail(res, "unauthorized"); + async printerlogin(c) { + const body = await c.req.json(); + const { username, topic } = body; + if (username === topic) return c.json({ ok: true }); + else return bail(c, "unauthorized"); }, - async acl(req, res) { - const { - body: { username, topic }, - } = req; - if (username === topic) res.send({ ok: true }); - else bail(res, "unauthorized"); + async acl(c) { + const body = await c.req.json(); + const { username, topic } = body; + if (username === topic) return c.json({ ok: true }); + else return bail(c, "unauthorized"); }, - async superuser(req, res) { - const { - body: { username }, - } = req; - if (username === config.mqtt2.username) res.send({ ok: true }); - else bail(res, "unauthorized"); + async superuser(c) { + const body = await c.req.json(); + const { username } = body; + if (username === config.mqtt2.username) return c.json({ ok: true }); + else return bail(c, "unauthorized"); }, - async request(req, res) { - const { email } = req.body; - const { user } = req; + async request(c) { + const body = await c.req.json(); + const { email } = body; + const user = c.get("user"); const { id } = user; try { @@ -777,16 +773,14 @@ export default { link, }); - res.send({ ok: true }); + return c.json({ ok: true }); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async verify(req, res) { - const { - params: { code }, - } = req; + async verify(c) { + const code = c.req.param("code"); try { const { id, email } = await g(`verify:${code}`); if (!id) fail("verification failed"); @@ -796,16 +790,15 @@ export default { await s(`user:${id}`, user); await s(`email:${email.toLowerCase()}`, id); - res.send(pick(user, fields)); + return c.json(pick(user, fields)); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async forgot(req, res) { - const { - body: { email }, - } = req; + async forgot(c) { + const body = await c.req.json(); + const { email } = body; try { const uid = await g(`email:${email.toLowerCase()}`); const user = await g(`user:${uid}`); @@ -821,47 +814,45 @@ export default { }); } - res.send({}); + return c.json({}); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async hidepay(req, res) { - const { - body: { username }, - } = req; + async hidepay(c) { + const body = await c.req.json(); + const { username } = body; const u = await getUser(username); u.hidepay = true; await s(`user:${u.id}`, u); - res.send({}); + return c.json({}); }, - async unlimit(req, res) { - const { - body: { username }, - } = req; + async unlimit(c) { + const body = await c.req.json(); + const { username } = body; const u = await getUser(username); u.unlimited = true; await s(`user:${u.id}`, u); - res.send({}); + return c.json({}); }, - async account(req, res) { - const { id } = req.params; - const { id: uid } = req.user; + async account(c) { + const id = c.req.param("id"); + const { id: uid } = c.get("user"); const pos = await db.lPos(`${uid}:accounts`, id); if (pos == null) fail("account not found"); const account = await g(`account:${id}`); if (account) account.balance = await getBalance(id); - res.send(account); + return c.json(account); }, - async accounts(req, res) { + async accounts(c) { try { - const { user } = req; + const user = c.get("user"); const accounts = []; for (const id of await db.lRange(`${user.id}:accounts`, 0, -1)) { @@ -886,17 +877,18 @@ export default { } } - res.send(accounts.reverse()); + return c.json(accounts.reverse()); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async createAccount(req, res) { + async createAccount(c) { try { - const { fingerprint, pubkey, name, seed, type, arkAddress, accountIndex } = req.body; - const { user } = req; + const body = await c.req.json(); + const { fingerprint, pubkey, name, seed, type, arkAddress, accountIndex } = body; + const user = c.get("user"); const { id: uid } = user; const id = v4(); @@ -917,14 +909,12 @@ export default { await createBalanceAccount(id); - // ARK accounts don't need Bitcoin Core wallet setup if (type === "ark") { const accountIds = await db.lRange(`${user.id}:accounts`, 0, -1); for (const accId of accountIds) { const acc = await g(`account:${accId}`); if (acc?.type === "ark") { - bail(res, "You already have an Ark vault"); - return; + return bail(c, "You already have an Ark vault"); } } const m = db @@ -934,11 +924,9 @@ export default { if (arkAddress) m.set(`arkaddr:${arkAddress}`, JSON.stringify({ aid: id, uid })); await m.exec(); - res.send(account); - return; + return c.json(account); } - // Bitcoin on-chain wallet setup let node = rpc(config[type]); await node.createWallet({ @@ -976,17 +964,18 @@ export default { .lPush(`${user.id}:accounts`, id) .exec(); - res.send(account); + return c.json(account); } catch (e) { - bail(res, e.message); + return bail(c, e.message); } }, - async updateAccount(req, res) { - const { id } = req.params; - const { id: uid } = req.user; + async updateAccount(c) { + const id = c.req.param("id"); + const { id: uid } = c.get("user"); + const body = await c.req.json(); const { name, autowithdraw, threshold, reserve, destination, currency, fingerprint, pubkey } = - req.body; + body; const pos = await db.lPos(`${uid}:accounts`, id); if (pos == null) fail("account not found"); @@ -1017,19 +1006,19 @@ export default { .catch((e) => console.error("importAccountHistory failed:", e.message)); } - res.send(account); + return c.json(account); }, - async deleteAccount(req, res) { + async deleteAccount(c) { try { - const { id } = req.body; - const { id: uid } = req.user; + const body = await c.req.json(); + const { id } = body; + const { id: uid } = c.get("user"); const account = await g(`account:${id}`); const pos = await db.lPos(`${uid}:accounts`, id); if (!(account && pos != null)) fail("account not found"); - // Only unload Bitcoin Core wallet for non-ARK accounts if (account.type !== "ark") { try { const node = rpc({ ...config[account.type], wallet: id }); @@ -1046,25 +1035,27 @@ export default { .del(`${id}:payments`) .exec(); - res.send({ ok: true }); + return c.json({ ok: true }); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async flash(req, res) { - const { ssid, key, token } = req.body; + async flash(c) { + const body = await c.req.json(); + const { ssid, key, token } = body; const cfg = `${ssid.trim()}\n${key.trim()}\n${token.trim()}\n`; await writeFile("./printer/config.txt", cfg, "utf8"); await $`./mklittlefs -c ./printer -p 256 -b 4096 -s 0x20000 ./littlefs.img`; - res.header("Content-Type", "application/octet-stream"); - return res.send(createReadStream("./littlefs.img")); + return new Response(Bun.file("./littlefs.img"), { + headers: { "Content-Type": "application/octet-stream" }, + }); }, - async app(req, res) { - const { pubkey } = req.params; - const { user } = req; + async app(c) { + const pubkey = c.req.param("pubkey"); + const user = c.get("user"); const app = await g(`app:${pubkey}`); if (app.uid !== user.id) fail("unauthorized"); @@ -1083,11 +1074,11 @@ export default { app.nwc = `nostr+walletconnect://${serverPubkey2}?relay=${relay}&secret=${app.secret}&lud16=${lud16}`; app.payments = payments.filter((p) => p); - res.send(app); + return c.json(app); }, - async apps(req, res) { - const { user } = req; + async apps(c) { + const user = c.get("user"); const pubkeys = await db.sMembers(`${user.id}:apps`); const apps = await Promise.all(pubkeys.map((p) => g(`app:${p}`))); @@ -1112,14 +1103,15 @@ export default { }), ); - res.send(apps); + return c.json(apps); }, - async updateApp(req, res) { + async updateApp(c) { try { - let { secret, pubkey, max_amount, max_fee, budget_renewal, name, notify } = req.body; + const body = await c.req.json(); + let { secret, pubkey, max_amount, max_fee, budget_renewal, name, notify } = body; - const { user } = req; + const user = c.get("user"); const uid = user.id; let app = await g(pubkey); @@ -1144,18 +1136,19 @@ export default { await s(`app:${pubkey}`, app); await db.sAdd(`${uid}:apps`, pubkey); - res.send({}); + return c.json({}); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async deleteApp(req, res) { + async deleteApp(c) { try { - const { user } = req; + const user = c.get("user"); const uid = user.id; - const { pubkey } = req.body; + const body = await c.req.json(); + const { pubkey } = body; const app = await g(`app:${pubkey}`); if (app && uid !== app.uid) { warn(app.uid, uid); @@ -1163,110 +1156,118 @@ export default { } await db.sRem(`${uid}:apps`, pubkey); await db.del(`app:${pubkey}`); - res.send({}); + return c.json({}); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async addPin(req, res) { - const { id: uid } = req.user; - const { id } = req.body; + async addPin(c) { + const { id: uid } = c.get("user"); + const body = await c.req.json(); + const { id } = body; await db.sAdd(`${uid}:pins`, id); - res.send({}); + return c.json({}); }, - async deletePin(req, res) { - const { id: uid } = req.user; - const { id } = req.body; + async deletePin(c) { + const { id: uid } = c.get("user"); + const body = await c.req.json(); + const { id } = body; await db.sRem(`${uid}:pins`, id); - res.send({}); + return c.json({}); }, - async trust(req, res) { - const { id } = req.user; - res.send(await db.sMembers(`${id}:trust`)); + async trust(c) { + const { id } = c.get("user"); + return c.json(await db.sMembers(`${id}:trust`)); }, - async addTrust(req, res) { - const { id: uid } = req.user; - const { id } = req.body; + async addTrust(c) { + const { id: uid } = c.get("user"); + const body = await c.req.json(); + const { id } = body; await db.sAdd(`${uid}:trust`, id); - res.send({}); + return c.json({}); }, - async deleteTrust(req, res) { - const { id: uid } = req.user; - const { id } = req.body; + async deleteTrust(c) { + const { id: uid } = c.get("user"); + const body = await c.req.json(); + const { id } = body; await db.sRem(`${uid}:trust`, id); - res.send({}); + return c.json({}); }, - async passkeyRegisterOptions(req, res) { + async passkeyRegisterOptions(c) { try { - const { user } = req; - const origin = req.body.origin || `https://${config.hostname}`; + const user = c.get("user"); + const body = await c.req.json(); + const origin = body.origin || `https://${config.hostname}`; const options = await generatePasskeyRegistration(user, origin); - res.send(options); + return c.json(options); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async passkeyRegisterVerify(req, res) { + async passkeyRegisterVerify(c) { try { - const { user } = req; - const origin = req.body.origin || `https://${config.hostname}`; - const credential = await verifyPasskeyRegistration(user, req.body, origin); + const user = c.get("user"); + const body = await c.req.json(); + const origin = body.origin || `https://${config.hostname}`; + const credential = await verifyPasskeyRegistration(user, body, origin); if (!user.passkeys) user.passkeys = []; user.passkeys.push(credential); await s(`user:${user.id}`, user); - res.send({ ok: true }); + return c.json({ ok: true }); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async passkeyLoginOptions(req, res) { + async passkeyLoginOptions(c) { try { - const origin = req.body.origin || `https://${config.hostname}`; + const body = await c.req.json(); + const origin = body.origin || `https://${config.hostname}`; const options = await generatePasskeyLogin(origin); - res.send(options); + return c.json(options); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async passkeyLoginVerify(req, res) { + async passkeyLoginVerify(c) { try { - const { credential, challengeId, origin: reqOrigin } = req.body; + const body = await c.req.json(); + const { credential, challengeId, origin: reqOrigin } = body; const origin = reqOrigin || `https://${config.hostname}`; const user = await verifyPasskeyLogin(credential, challengeId, origin); const payload = { id: user.id }; const token = jwt.sign(payload, config.jwt); - res.send({ user: pick(user, whitelist), token }); + return c.json({ user: pick(user, whitelist), token }); } catch (e) { console.log(e); - bail(res, e.message); + return bail(c, e.message); } }, - async credits(req, res) { - const { id } = req.user; + async credits(c) { + const { id } = c.get("user"); const bitcoin = await getCredit(id, "bitcoin"); const lightning = await getCredit(id, "lightning"); const liquid = await getCredit(id, "liquid"); - res.send({ bitcoin, lightning, liquid }); + return c.json({ bitcoin, lightning, liquid }); }, - async ro(req, res) { - const { user } = req; + async ro(c) { + const user = c.get("user"); const payload = { id: `${user.id}-ro` }; const token = jwt.sign(payload, config.jwt); - res.send(token); + return c.json(token); }, }; |
