From e690bbd406c17c5fab1e7b7e06933f78e01dee60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E4=B8=AA=E4=B8=8D=E7=9F=A5=E5=90=8D=E3=81=AE?= =?UTF-8?q?=E7=9D=A1=E8=A7=89=E9=AB=98=E6=89=8B?= Date: Fri, 2 Jun 2023 11:04:10 +0800 Subject: [PATCH] feat: support paid (#44) --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- CONFIGURATION.md | 52 +- Dockerfile | 6 +- README.md | 107 +++- deno.jsonc | 43 +- deno.lock | 486 +++++++++++++----- deployment/maintenance.yaml | 2 +- deployment/static-mirroring.yaml | 2 +- deployment/worker.yaml | 2 +- docker-compose.yml | 30 +- docs/apply-for-k8s.md | 3 + docs/apply-for-k8s_zhCN.md | 3 + package.json | 8 +- resources/default-settings.yaml | 8 + resources/index.html | 6 +- resources/invoices.html | 8 +- src/@types/base.ts | 10 +- src/@types/controllers.ts | 4 +- src/@types/database.ts | 4 +- src/@types/event.ts | 3 +- src/@types/invoice.ts | 10 +- src/@types/repositories.ts | 16 +- src/@types/services.ts | 4 +- src/@types/settings.ts | 12 + src/@types/user.ts | 8 +- src/app/maintenance-worker.ts | 8 +- src/cache/client.ts | 17 +- src/config/index.ts | 3 + src/constants/adapter.ts | 4 + .../callbacks/lnbits-callback-controller.ts | 61 ++- .../callbacks/nodeless-callback-controller.ts | 107 ++++ .../callbacks/opennode-callback-controller.ts | 66 +++ .../callbacks/zebedee-callback-controller.ts | 60 ++- .../invoices/get-invoice-controller.ts | 38 ++ .../invoices/post-invoice-controller.ts | 49 +- src/core-services/PubSubService.ts | 49 ++ src/core-services/index.ts | 1 + src/database/models/Events.ts | 17 +- src/database/models/Invoices.ts | 105 ++++ src/database/models/Users.ts | 47 +- src/database/transaction.ts | 58 ++- .../get-invoice-controller-factory.ts | 3 + .../get-invoice-status-controller-factory.ts | 11 + .../lnbits-callback-controller-factory.ts | 12 + .../nodeless-callback-controller-factory.ts | 8 + .../opennode-callback-controller-factory.ts | 9 + .../post-invoice-controller-factory.ts | 18 + .../zebedee-callback-controller-factory.ts | 9 + src/factories/core-services-factory.ts | 11 +- .../get-invoice-status-controller-factory.ts | 11 - .../lnbits-callback-controller-factory.ts | 12 - src/factories/payments-processor-factory.ts | 144 +----- .../lnbits-payments-processor-factory.ts | 40 ++ .../lnurl-payments-processor-factory.ts | 18 + .../nodeless-payments-processor-factory.ts | 33 ++ .../opennode-payments-processor-factory.ts | 41 ++ .../zebedee-payments-processor-factory.ts | 52 ++ .../post-invoice-controller-factory.ts | 18 - src/factories/web-app-factory.ts | 8 +- src/factories/worker-factory.ts | 5 +- .../zebedee-callback-controller-factory.ts | 9 - .../ephemeral-event-strategy.ts | 14 +- .../body-parser-middleware.ts | 48 ++ .../get-invoice-request-handler.ts | 50 -- .../get-invoice-status-request-handler.ts | 13 - .../post-invoice-request-handler.ts | 19 - .../post-lnbits-callback-request-handler.ts | 19 - .../post-zebedee-callback-request-handler.ts | 17 - .../rate-limiter-middleware.ts | 14 +- .../request-handlers/root-request-handler.ts | 2 +- .../with-controller-request-handler.ts | 21 + src/handlers/subscribe-message-handler.ts | 3 +- .../lnbits-payment-processor.ts | 19 +- .../lnurl-payments-processor.ts | 16 +- .../nodeless-payments-processor.ts | 73 +++ .../opennode-payments-processor.ts | 63 +++ src/payments-processors/payments-procesor.ts | 18 - .../zebedee-payments-processor.ts | 13 +- src/repositories/event-repository.ts | 5 +- src/repositories/invoice-repository.ts | 186 +++++-- src/repositories/user-repository.ts | 35 +- src/routes/callbacks/index.ts | 92 +--- src/routes/index.ts | 12 +- src/routes/invoices/index.ts | 14 +- src/services/payments-service.ts | 28 +- src/utils/event.ts | 39 +- src/utils/filter.ts | 2 +- src/utils/http.ts | 124 +++++ src/utils/transform.ts | 112 +++- test/integration/features/helpers.ts | 4 +- test/integration/features/shared.ts | 27 +- .../default-event-strategy.test.ts | 6 +- .../delete-event-strategy.test.ts | 6 +- ...terized-replaceable-event-strategy.test.ts | 6 +- .../replaceable-event-strategy.test.ts | 6 +- .../subscribe-message-handler.test.ts | 10 +- .../unsubscribe-message-handler.test.ts | 4 +- .../repositories/event-repository.test.ts | 2 +- test/unit/utils/filter.test.ts | 23 + 100 files changed, 2210 insertions(+), 958 deletions(-) create mode 100644 src/controllers/callbacks/nodeless-callback-controller.ts create mode 100644 src/controllers/callbacks/opennode-callback-controller.ts create mode 100644 src/controllers/invoices/get-invoice-controller.ts create mode 100644 src/core-services/PubSubService.ts create mode 100644 src/database/models/Invoices.ts create mode 100644 src/factories/controllers/get-invoice-controller-factory.ts create mode 100644 src/factories/controllers/get-invoice-status-controller-factory.ts create mode 100644 src/factories/controllers/lnbits-callback-controller-factory.ts create mode 100644 src/factories/controllers/nodeless-callback-controller-factory.ts create mode 100644 src/factories/controllers/opennode-callback-controller-factory.ts create mode 100644 src/factories/controllers/post-invoice-controller-factory.ts create mode 100644 src/factories/controllers/zebedee-callback-controller-factory.ts delete mode 100644 src/factories/get-invoice-status-controller-factory.ts delete mode 100644 src/factories/lnbits-callback-controller-factory.ts create mode 100644 src/factories/payments-processors/lnbits-payments-processor-factory.ts create mode 100644 src/factories/payments-processors/lnurl-payments-processor-factory.ts create mode 100644 src/factories/payments-processors/nodeless-payments-processor-factory.ts create mode 100644 src/factories/payments-processors/opennode-payments-processor-factory.ts create mode 100644 src/factories/payments-processors/zebedee-payments-processor-factory.ts delete mode 100644 src/factories/post-invoice-controller-factory.ts delete mode 100644 src/factories/zebedee-callback-controller-factory.ts create mode 100644 src/handlers/request-handlers/body-parser-middleware.ts delete mode 100644 src/handlers/request-handlers/get-invoice-request-handler.ts delete mode 100644 src/handlers/request-handlers/get-invoice-status-request-handler.ts delete mode 100644 src/handlers/request-handlers/post-invoice-request-handler.ts delete mode 100644 src/handlers/request-handlers/post-lnbits-callback-request-handler.ts delete mode 100644 src/handlers/request-handlers/post-zebedee-callback-request-handler.ts create mode 100644 src/handlers/request-handlers/with-controller-request-handler.ts create mode 100644 src/payments-processors/nodeless-payments-processor.ts create mode 100644 src/payments-processors/opennode-payments-processor.ts delete mode 100644 src/payments-processors/payments-procesor.ts create mode 100644 test/unit/utils/filter.test.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5d4cfed3..83d09362 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: Bug report about: Create a report to help us improve title: "[BUG] " labels: bug -assignees: Guakamoli +assignees: denostr-protocol --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 13ecc9bd..d5b94117 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,7 +3,7 @@ name: Feature request about: Suggest an idea for this project title: "[REQUEST]" labels: enhancement -assignees: Guakamoli +assignees: denostr-protocol --- diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 63759255..fa041308 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -4,30 +4,34 @@ The following environment variables can be set: -| Name | Description | Default | -| ---------------------- | -------------------------------- | ------------------------------------ | -| SECRET | Long random secret. | abcdefghijklmnopqrstuvwxyz1234567890 | -| RELAY_PORT | Relay's server port | 8008 | -| RELAY_PRIVATE_KEY | Relay's private key in hex | (auto-generated) | -| MONGO_URI | MongoDB URI | | -| MONGO_MIN_POOL_SIZE | Min. connections per worker | 0 | -| MONGO_MAX_POOL_SIZE | Max. connections per worker | 3 | -| MONGO_REPLICA_ENABLED | Read Replica (RR) Toggle | false | -| MONGO_RR_MIN_POOL_SIZE | Min. connections per worker (RR) | 0 | -| MONGO_RR_MAX_POOL_SIZE | Max. connections per worker (RR) | 3 | -| TOR_HOST | Tor Hostname | | -| TOR_CONTROL_PORT | Tor control Port | 9051 | -| TOR_PASSWORD | Tor control password | nostr_ts_relay | -| HIDDEN_SERVICE_PORT | Tor hidden service port | 80 | -| REDIS_HOST | Redis Host | | -| REDIS_PORT | Redis Port | 6379 | -| REDIS_DB | Redis DB | 0 | -| REDIS_USER | Redis User | | -| REDIS_PASS | Redis Password | | -| REDIS_TLS | Redis TLS | false | -| NOSTR_CONFIG_DIR | Configuration directory | /.nostr/ | -| DEBUG | Debugging filter | | -| ZEBEDEE_API_KEY | Zebedee Project API Key | | +| Name | Description | Default | +| -------------------------- | ----------------------------------- | --------------------------------------- | +| SECRET | Long random secret. | abcdefghijklmnopqrstuvwxyz1234567890 | +| RELAY_PORT | Relay's server port | 8008 | +| RELAY_PRIVATE_KEY | Relay's private key in hex | (auto-generated) | +| MONGO_URI | MongoDB URI | | +| MONGO_MIN_POOL_SIZE | Min. connections per worker | 0 | +| MONGO_MAX_POOL_SIZE | Max. connections per worker | 3 | +| MONGO_REPLICA_ENABLED | Read Replica (RR) Toggle | false | +| MONGO_RR_MIN_POOL_SIZE | Min. connections per worker (RR) | 0 | +| MONGO_RR_MAX_POOL_SIZE | Max. connections per worker (RR) | 3 | +| TOR_HOST | Tor Hostname | | +| TOR_CONTROL_PORT | Tor control Port | 9051 | +| TOR_PASSWORD | Tor control password | nostr_ts_relay | +| HIDDEN_SERVICE_PORT | Tor hidden service port | 80 | +| REDIS_HOST | Redis Host | | +| REDIS_PORT | Redis Port | 6379 | +| REDIS_DB | Redis DB | 0 | +| REDIS_USER | Redis User | | +| REDIS_PASS | Redis Password | | +| REDIS_TLS | Redis TLS | false | +| NOSTR_CONFIG_DIR | Configuration directory | /.nostr/ | +| DEBUG | Debugging filter | | +| ZEBEDEE_API_KEY | Zebedee Project API Key | | +| LNBITS_API_KEY | Lnbits Invoice/read Key | | +| OPENNODE_API_KEY | API Key with Invoices Key | | +| NODELESS_API_KEY | Nodeless API key | | +| NODELESS_WEBHOOK_SECRET | Nodeless webhook secret | | # Settings diff --git a/Dockerfile b/Dockerfile index 439b05eb..fa198aee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,10 @@ FROM denoland/deno:alpine-1.31.3 as base LABEL org.opencontainers.image.title=denostr LABEL org.opencontainers.image.description='Deno-based, cloud-native nostr implementation supported by ByteTrade and Revo, forked from nostream.' -LABEL org.opencontainers.image.authors=GUAKAMOLI +LABEL org.opencontainers.image.authors=Denostr LABEL org.opencontainers.image.licenses=MIT -LABEL org.opencontainers.image.url=https://github.com/guakamoli/denostr -LABEL org.opencontainers.image.source=https://github.com/guakamoli/denostr +LABEL org.opencontainers.image.url=https://github.com/denostr-protocol/denostr +LABEL org.opencontainers.image.source=https://github.com/denostr-protocol/denostr # Create the app directory WORKDIR /app diff --git a/README.md b/README.md index 488e0c43..28d3bde3 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,31 @@ -# [Denostr](https://github.com/Guakamoli/denostr) +# [Denostr](https://github.com/denostr-protocol/denostr)

denostr logo

- - GitHub release + + GitHub release - - GitHub issues + + GitHub issues - - GitHub stars + + GitHub stars - GitHub top language - - GitHub forks + GitHub top language + + GitHub forks - - GitHub license + + GitHub license - - Coverage Status + + Coverage Status - - Build status + + Build status

@@ -35,7 +35,7 @@ This is a [nostr](https://github.com/fiatjaf/nostr) relay, written in Typescript This implementation is production-ready. See below for supported features. -The project main repository is available on [GitHub](https://github.com/Guakamoli/denostr). +The project main repository is available on [GitHub](https://github.com/denostr-protocol/denostr). ## Features @@ -70,7 +70,7 @@ Todo - Deno v1.30.x or v1.31.x - Typescript - MongoDB 4.4, 5.0, 6.0 -- Redis (Optional) +- Redis (Standalone Optional, Cluster Required) ### kubernetes setups @@ -78,25 +78,78 @@ Todo ## Full Guide -- [ ] Set up a paid Nostr relay +> NOTE: If the payment is enabled, it is recommended to start another instance with `WORKER_TYPE=maintenance` following the "Quick Start" guide. ### Accepting payments -1. Zebedee +1. Before you begin + - Complete one of the Quick Start guides in this document + - Create a `.env` file + - On `.nostr/settings.yaml` file make the following changes: + - Set `payments.enabled` to `true` + - Set `payments.feeSchedules.admission.enabled` to `true` + - Set `limits.event.pubkey.minBalance` to the minimum balance in msats required to accept events (i.e. `1000000` to require a balance of `1000` sats) + - Choose one of the following payment processors: `lnbits`, `lnurl`, `zebedee` + +2. [LNbits](https://lnbits.com/) + - Complete the step "Before you begin" + - Create a new wallet on you public LNbits instance + - [Demo](https://legend.lnbits.com/) server must not be used for production + - Your instance must be accessible from the internet and have a valid SSL/TLS certificate + - Get wallet "Invoice/read key" (in Api docs section of your wallet) + - set `LNBITS_API_KEY` environment variable with the "Invoice/read key" Key above on your `.env` file + + ``` + LNBITS_API_KEY={YOUR_LNBITS_API_KEY_HERE} + ``` + - On your `.nostr/settings.yaml` file make the following changes: + - Set `payments.processor` to `lnbits` + - set `lnbits.baseURL` to your LNbits instance URL (e.g. `https://{YOUR_LNBITS_DOMAIN_HERE}/`) + - Set `paymentsProcessors.lnbits.callbackBaseURL` to match your callbcak URL (e.g. `https://{YOUR_DOMAIN_HERE}/callbacks/lnbits`) + - Restart Denostr + +3. [Alby](https://getalby.com/) or any LNURL Provider with [LNURL-verify](https://github.com/lnurl/luds/issues/182) support + - Complete the step "Before you begin" + - [Create a new account](https://getalby.com/user/new) if you don't have an LNURL + - On your `.nostr/settings.yaml` file make the following changes: + - Set `payments.processor` to `lnurl` + - Set `lnurl.invoiceURL` to your LNURL (e.g. `https://getalby.com/lnurlp/your-username`) + - Restart Denostr + +4. [ZEBEDEE](https://zebedee.io) + - Complete the step "Before you begin" + - [Sign up for a ZEBEDEE Developer Dashboard account](https://dashboard.zebedee.io/signup), create a new LIVE Project, and get that Project's API Key + - Set `ZEBEDEE_API_KEY` environment variable with the API Key above on your `.env` file + + ``` + ZEBEDEE_API_KEY={YOUR_ZEBEDEE_API_KEY_HERE} + ``` + + - Follow the required steps for all payments processors + - On `.nostr/settings.yaml` file make the following changes: + - `payments.processor` to `zebedee` + - `paymentsProcessors.zebedee.callbackBaseURL` to match your callback URL (e.g. `https://{YOUR_DOMAIN_HERE}/callbacks/zebedee`) + - Restart Denostr + +5. Ensure payments are required for your public key + - Visit https://{YOUR-DOMAIN}/ + - You should be presented with a form requesting an admission fee to be paid + - Fill out the form and take the necessary steps to pay the invoice + - Wait until the screen indicates that payment was received + - Add your relay URL to your favorite Nostr client (wss://{YOUR-DOMAIN}) and wait for it to connect + - Send a couple notes to test + - Go to https://websocketking.com/ and connect to your relay (wss://{YOUR_DOMAIN}) + - Convert your npub to hexadecimal using a [Key Converter](https://damus.io/key/) + - Send the following JSON message: `["REQ", "payment-test", {"authors":["your-pubkey-in-hexadecimal"]}]` + - You should get back the few notes you sent earlier -- You must set `ZEBEDEE_API_KEY` with an API Key from one of your projects in your Zebedee Developer Dashboard. Contact @foxp2zeb on Telegram or npub1rvg76s0gz535txd9ypg2dfqv0x7a80ar6e096j3v343xdxyrt4ksmkxrck on Nostr requesting access to - the Zebedee Developer Dashboard. See the Zebedee full guide on how to set up a paid relay. - -2. Lnbits - -- You must set `LNBITS_API_KEY` with an API Key from one of "Invoice/read key" in your wallet. ### Quick Start (Standalone) Clone repository and enter directory: ```sh -git clone https://github.com/Guakamoli/denostr.git --depth 1 && cd denostr +git clone https://github.com/denostr-protocol/denostr.git --depth 1 && cd denostr ``` Create `.env` file inside denostr project folder diff --git a/deno.jsonc b/deno.jsonc index 2d13a44f..f335400d 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,9 +1,16 @@ { "tasks": { - "dev": "deno run --allow-all --watch src/index.ts", - "start": "deno run --allow-all src/index.ts", + "dev": "deno run -A --unstable --watch src/index.ts", + "start": "deno run -A --unstable src/index.ts", "test": "deno test --no-check -A", "test:unit": "deno test test/unit --no-check -A", + "test:unit:cache": "deno test --no-check -A test/unit/cache", + "test:unit:factories": "deno test --no-check -A test/unit/factories", + "test:unit:handlers": "deno test --no-check -A test/unit/handlers", + "test:unit:repositories": "deno test --no-check -A test/unit/repositories", + "test:unit:schemas": "deno test --no-check -A test/unit/schemas", + "test:unit:tor": "deno test --no-check -A test/unit/tor", + "test:unit:utils": "deno test --no-check -A test/unit/utils", "test:integration": "deno test test/integration --no-check -A", "test:integration:nip01": "deno test test/integration/features/nip-01/nip-01.feature.test.ts --no-check -A", "test:integration:nip09": "deno test test/integration/features/nip-09/nip-09.feature.test.ts --no-check -A", @@ -13,20 +20,32 @@ "git-hooks": "deno run --no-check=remote --allow-run=deno,git --allow-read --allow-write=.git-hooks https://deno.land/x/githooked/mod.ts install", "bump": "deno run --allow-all .bump/bump.ts", "cache": "deno cache src/index.ts --reload", - "force-cache": "deno cache src/index.ts --reload --lock-write" + "cache:force": "deno cache src/index.ts --reload --lock-write" }, "imports": { + "@/": "./src/", + "Buffer": "https://deno.land/std@0.177.1/node/buffer.ts", + "crypto": "https://deno.land/std@0.177.1/node/crypto.ts", + "events": "https://deno.land/std@0.177.1/node/events.ts", + "stream": "https://deno.land/std@0.177.1/node/stream.ts", + "stream/promises": "https://deno.land/std@0.177.1/node/stream/promises.mjs", + "dns": "https://deno.land/std@0.177.1/node/dns.ts", + "net": "https://deno.land/std@0.177.1/node/net.ts", + "tls": "https://deno.land/std@0.177.1/node/tls.ts", + "path": "https://deno.land/std@0.177.1/node/path.ts", + "os": "https://deno.land/std@0.177.1/node/os.ts", + "fs": "https://deno.land/std@0.177.1/node/fs.ts", + "fs/promises": "https://deno.land/std@0.177.1/node/fs/promises.ts", "dotenv": "https://deno.land/x/dotenv@v3.2.0/mod.ts", "ramda": "https://deno.land/x/ramda@v0.27.2/mod.ts", - "redis": "https://deno.land/x/redis@v0.29.2/mod.ts", + "redis": "https://deno.land/x/redis@v0.29.4/mod.ts", + "redis/": "https://deno.land/x/redis@v0.29.4/", "debug": "https://deno.land/x/debuglog@v1.0.0/debug.ts", - "websocket": "./vendor/deno.land/x/websocket@v0.1.4/mod.ts", "jest": "https://deno.land/std@0.180.0/testing/bdd.ts", "oak": "https://deno.land/x/oak@v12.1.0/mod.ts", - "Buffer": "https://deno.land/std@0.139.0/node/buffer.ts", "secp256k1": "https://deno.land/x/secp256k1@1.7.1/mod.ts", - "crypto": "https://deno.land/std@0.177.0/node/crypto.ts", "oak-csp": "https://deno.land/x/oak_csp@1.0.3/mod.ts", + "json2yaml": "https://deno.land/x/json2yaml@v1.0.1/mod.ts", "chai": "https://cdn.skypack.dev/chai@4.3.4?dts", "sinon-chai": "https://cdn.skypack.dev/sinon-chai@3.7.0?dts", "sinon": "https://cdn.skypack.dev/sinon@15.0.1?dts", @@ -34,16 +53,6 @@ "joi": "https://cdn.skypack.dev/joi@17.7.0?dts", "rxjs": "https://cdn.skypack.dev/rxjs@7.8.0?dts", "chai-as-promised": "https://cdn.skypack.dev/chai-as-promised@7.1.1?dts", - "events": "https://deno.land/std@0.177.0/node/events.ts", - "stream": "node:stream", - "stream/promises": "node:stream/promises", - "dns": "node:dns", - "net": "node:net", - "tls": "node:tls", - "path": "node:path", - "os": "node:os", - "fs": "node:fs", - "fs/promises": "node:fs/promises", "bson": "npm:bson@5.3.0", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@1.4.0", "mongodb": "npm:mongodb@5.3.0", diff --git a/deno.lock b/deno.lock index 07dd9945..1c461221 100644 --- a/deno.lock +++ b/deno.lock @@ -1,9 +1,59 @@ { "version": "2", "remote": { + "https://cdn.skypack.dev/-/@sinonjs/fake-timers@v10.0.2-7Q9DvtEGUhxo9tseVUWV/dist=es2019,mode=types/index.d.ts": "f83b320cceccfc48457a818d18fc9a006ab18d0bdd727aa2c2e73dc1b4a45e98", + "https://cdn.skypack.dev/-/@sinonjs/fake-timers@v7.1.2-ixsdnXajQrmeGBLmevQ2/dist=es2019,mode=types/types/fake-timers-src.d.ts": "6b40029289530423f407a22755c85b81740f9acfd88d2b53564f8c1657c26660", + "https://cdn.skypack.dev/-/assertion-error@v1.1.0-kNvqltoxcdROsufUrL09/dist=es2019,mode=imports/optimized/assertion-error.js": "fb392c145699a7beacbabe94b21ee5d0219707929450fffbb18eb83b8318c818", + "https://cdn.skypack.dev/-/chai-as-promised@v7.1.1-V8qEOEdB9XCPaGNrdaUw/dist=es2019,mode=imports/optimized/chai-as-promised.js": "d7cd7f53c84e3959c4d1d428385e7f5a0eee45baa66e4a041732ac31cf4869da", + "https://cdn.skypack.dev/-/chai-as-promised@v7.1.1-V8qEOEdB9XCPaGNrdaUw/dist=es2019,mode=types/index.d.ts": "1fec4bad3aa3ed84c4839b7ed6725872a94dde3db86505445d812e955bcbfa7b", + "https://cdn.skypack.dev/-/chai@v4.3.4-4LD1LjgHWVQjvyMFe7sm/dist=es2019,mode=imports/optimized/chai.js": "2d517f26a740a2c43a158c510ba0c97b1743f1825022583234f7f3b8ec6b7599", + "https://cdn.skypack.dev/-/chai@v4.3.4-4LD1LjgHWVQjvyMFe7sm/dist=es2019,mode=types/index.d.ts": "b6c73080490a06c472c0f05afba3b564b170f39bbaeca57d0b60a6bcd98a840c", + "https://cdn.skypack.dev/-/check-error@v1.0.2-pv7N1FcwI8awT920b4E0/dist=es2019,mode=imports/optimized/check-error.js": "c00358ede04680f37b081564d88b9457e9812bb3c5c7effc90acb090f3bd73f4", + "https://cdn.skypack.dev/-/deep-eql@v3.0.1-EVT6yn6LFH7vvQx4YnOu/dist=es2019,mode=imports/optimized/deep-eql.js": "e8a3877f0ee43db99f1b903a8b527c57827fe2753a0f0fd560563aa2e435a0d3", + "https://cdn.skypack.dev/-/get-func-name@v2.0.0-scW92yE6OFNJbvjxk2uF/dist=es2019,mode=imports/optimized/get-func-name.js": "8926a16a678d5ca12cae705fa8a5be600aaf5355e601d34575f1bd9af64a170c", "https://cdn.skypack.dev/-/joi@v17.7.0-RK0tA5T7StpATAhyOHd0/dist=es2019,mode=imports/optimized/joi.js": "302fe80da79cf00792631c966c91f2067e1fc6c335f70079579323ff28b0f041", "https://cdn.skypack.dev/-/joi@v17.7.0-RK0tA5T7StpATAhyOHd0/dist=es2019,mode=types/lib/index.d.ts": "356aa4e888eedc8b29ae1532aea69c6d819b4795552d34cc9a04f8ac97b7e60c", + "https://cdn.skypack.dev/-/pathval@v1.1.1-4hf44tKt2vdiXpyywiwN/dist=es2019,mode=imports/optimized/pathval.js": "778f821f97d08e946a894e3b94635a63121d8bd5000aeb3d00785318d5ed32de", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/AsyncScheduler-1be47af6.js": "eb94849b5a274c8cd075dec4312cba3998ccaf820149462c490ceebb75fbc987", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/EmptyError-41db9d3f.js": "b9406314bc6936aaf61de500a900f4fd8bf1012bdd8dc301c1ecd324e65057b9", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/Notification-5a9034fd.js": "053ac309d71f0e752343b659fd8440305612bc7ba5665acf3180798fa8ff3d13", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/Observable-7e44db0b.js": "4850765ea35eba1d219484bced5d58c088bb212f2c4de037ded304c66a5a2435", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/OperatorSubscriber-6d72c4ab.js": "81f1ae08d9c0c5ec618f6cfb8dd4abec3b85c411f198694e54f4d624a20e95fc", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/Subject-289b09f9.js": "f226d38bbd77961774a1df0219e0bac5a22b256276167d326b2c480fffab39dd", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/Subscriber-368decef.js": "15405901be614ace9de7787172c8510a903eaa8831d91c407696099d70f1b562", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/Subscription-b18e6761.js": "a0862b17da978e5f9b61ab3d92494368c4c9866eb0b9f487dfdedb2e77c5b37f", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/VirtualTimeScheduler-9142a7bb.js": "1a742b7d31a597e7024a38b26b5a6353956a9fbcf32ca491acf93a010819edc6", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/createErrorClass-d37b97e3.js": "4398cce2f98e5b5a965e278fe97749fe2849bdfc2f7c836af40cd0718ae07b3f", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/dateTimestampProvider-43e4031c.js": "91b5af8cb2394e9168701e79872557ed1da75522642bf0179ae5c1ad26617eb1", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/innerFrom-911ff024.js": "4249e9884ee94c33208868321f903fc7f7e8dc2d49fb9bc79290140af3fc4874", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/lift-8243c0ff.js": "af4bfefee1b2bf408b6530591b8c8cabff34d0160bc56c66ebe6baf6deea6b51", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/map-3df1de07.js": "234ebbf399f55766008765d9e2fb68b998ac54f58330b89d6b8db0489568c65f", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/common/zipWith-ee4749e2.js": "5f2a9a01d80f89e2d74f361e966d16315c57fad4d0999393c395ba448d65fb55", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs.js": "53f80634624765127e4af3087e5c9e998ff04ac68f994b518328d29eb7978fe0", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/AsyncSubject.js": "ddc8e59238dd34ebcfca85a240de7c46a7258288cddb369e4da19cefe7df1360", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/BehaviorSubject.js": "038dbfe6b372347c522eaaae60b0c993aebf2ebc083f99d221474caad14dc427", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/NotificationFactories.js": "bc822ec4b2d05f3726c8bd3735dcb44775ec85aed8435ab473e8d992bd4dfead", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/ReplaySubject.js": "7791dcdb33a9fe41e2692e42485519fd62fe2dd4aacd391449db3b4c209e6eb4", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/Scheduler.js": "ce5d2f71ad5e76ffa039827b091a10da299695f93c46376e46cdc1e082e0a87c", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/config.js": "78900e946906e3f588b78a53d433b9ce8deb63db0fa84c150ffaf2f54ab3f420", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/firstValueFrom.js": "7ddb48503167cfa36f0c184bd8d50b59af83b58afae3b44cde1385667a13a736", + "https://cdn.skypack.dev/-/rxjs@v7.8.0-oE0GhbhAdiS0mpw9Iw6u/dist=es2019,mode=imports/optimized/rxjs/internal/lastValueFrom.js": "7bab19ffb1619b9f4d2f6f1e43e3c4a0e71659dd1c2094a869cdaf3089c57253", + "https://cdn.skypack.dev/-/sinon-chai@v3.7.0-sPO3yztXU6JvAbBcqOA6/dist=es2019,mode=imports/optimized/sinon-chai.js": "da69ffb373302464defc70b76f18a103fedad00eca82751840388a4881d452bb", + "https://cdn.skypack.dev/-/sinon-chai@v3.7.0-sPO3yztXU6JvAbBcqOA6/dist=es2019,mode=types/index.d.ts": "1b96f339dbcb43da644b021168045ac6236520a3f8017c622dcc0503b23e54f5", + "https://cdn.skypack.dev/-/sinon@v11.1.1-a4QDxvQ2C1lyBOKwzLcr/dist=es2019,mode=types/index.d.ts": "194a06aac2c91f395d387a41a7b74a1cf3b153049c68f698f3da5c106bc71968", + "https://cdn.skypack.dev/-/sinon@v15.0.1-qlKtgxOm72taiA3pbkK2/dist=es2019,mode=imports/optimized/sinon.js": "4fa78f658343ba3153a936f7ff0488ad9845324c1854d6d73a0a6d7a5d4bc7d5", + "https://cdn.skypack.dev/-/sinon@v15.0.1-qlKtgxOm72taiA3pbkK2/dist=es2019,mode=types/index.d.ts": "e49a3c9e2a9a7386a21a6c1ebf9d3883b2712c99fc14ecf8ea1433f6e7a5845c", + "https://cdn.skypack.dev/-/tslib@v2.4.1-EnXszYOb70PNwhRTrKtW/dist=es2019,mode=imports/optimized/common/tslib-5f663465.js": "5962d7e4d3723d7189d43ed6143cf8a00fe688624c4e541e0bea70fff1ae0745", + "https://cdn.skypack.dev/-/tslib@v2.4.1-EnXszYOb70PNwhRTrKtW/dist=es2019,mode=imports/optimized/tslib.js": "ce8027a3f38ac0e0d3c85d525de72b2c6934ff125b6a0449038df975dbcc2967", + "https://cdn.skypack.dev/-/type-detect@v4.0.8-3tmjjwwFw2jHiLYy7SbM/dist=es2019,mode=imports/optimized/type-detect.js": "eb7ac639d3de6a0b64aaf9032a5b01ab79afb7c768cc02c07956613ecbd0bd32", + "https://cdn.skypack.dev/chai-as-promised@7.1.1?dts": "830017e3a2f15f0f888639655c861d666ffa3c290b30fb095609c6781cd7b9d3", + "https://cdn.skypack.dev/chai@4.3.4?dts": "19489c45018b0bcfb63455624ade9adf84e2a3c99a08379160d90763ba7e3f41", + "https://cdn.skypack.dev/error/unknown:chai?from=chai-as-promised": "d81c24bdeb6353bd4c69635a787f47511d41c71940b159e88e0e7a15d7573b95", + "https://cdn.skypack.dev/error/unknown:chai?from=sinon-chai": "c5eea0fab0f428ffb949645e4adcb0da74722dc5b483bb13bf48d0723436a185", "https://cdn.skypack.dev/joi@17.7.0?dts": "70e396b701dea0c5a59bf8a5578457cb04a98e5ba37d77b10900c7c7aed8e0e3", + "https://cdn.skypack.dev/rxjs@7.8.0?dts": "e5c758eebc71433a4c75900690399c74ca86bc31085a40c69509f377aeacccc1", + "https://cdn.skypack.dev/sinon-chai@3.7.0?dts": "e1773fa39d5bebee9e9f4a32b62ebc67c7330043a725ecf0e4c9bf5730738db1", + "https://cdn.skypack.dev/sinon@15.0.1?dts": "220487f84273811340f37025cfa8c092492aaa700d9318fe2938f4adb04fea5e", "https://deno.land/std@0.139.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", "https://deno.land/std@0.139.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", "https://deno.land/std@0.139.0/encoding/base64.ts": "c8c16b4adaa60d7a8eee047c73ece26844435e8f7f1328d74593dbb2dd58ea4f", @@ -41,13 +91,6 @@ "https://deno.land/std@0.153.0/testing/_diff.ts": "141f978a283defc367eeee3ff7b58aa8763cf7c8e0c585132eae614468e9d7b8", "https://deno.land/std@0.153.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", "https://deno.land/std@0.153.0/testing/asserts.ts": "d6595cfc330b4233546a047a0d7d57940771aa9d97a172ceb91e84ae6200b3af", - "https://deno.land/std@0.176.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", - "https://deno.land/std@0.176.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", - "https://deno.land/std@0.176.0/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd", - "https://deno.land/std@0.176.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", - "https://deno.land/std@0.176.0/io/buf_reader.ts": "90a7adcb3638d8e1361695cdf844d58bcd97c41711dc6f9f8acc0626ebe097f5", - "https://deno.land/std@0.176.0/io/buf_writer.ts": "759c69d304b04d2909976f2a03a24a107276fbd81ed13593c5c2d43d104b52f3", - "https://deno.land/std@0.176.0/types.d.ts": "220ed56662a0bd393ba5d124aa6ae2ad36a00d2fcbc0e8666a65f4606aaa9784", "https://deno.land/std@0.177.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", "https://deno.land/std@0.177.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", "https://deno.land/std@0.177.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24", @@ -163,7 +206,277 @@ "https://deno.land/std@0.177.0/node/internal_binding/util.ts": "808ff3b92740284184ab824adfc420e75398c88c8bccf5111f0c24ac18c48f10", "https://deno.land/std@0.177.0/node/internal_binding/uv.ts": "eb0048e30af4db407fb3f95563e30d70efd6187051c033713b0a5b768593a3a3", "https://deno.land/std@0.177.0/node/stream.ts": "09e348302af40dcc7dc58aa5e40fdff868d11d8d6b0cfb85cbb9c75b9fe450c7", + "https://deno.land/std@0.177.0/node/stream/promises.mjs": "af39c45adb266b1dc029cb1a3e7278e47f5a2455e8d4cf8fe50752425b5c2d49", "https://deno.land/std@0.177.0/node/string_decoder.ts": "1a17e3572037c512cc5fc4b29076613e90f225474362d18da908cb7e5ccb7e88", + "https://deno.land/std@0.177.1/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.177.1/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.177.1/async/abortable.ts": "73acfb3ed7261ce0d930dbe89e43db8d34e017b063cf0eaa7d215477bf53442e", + "https://deno.land/std@0.177.1/async/deadline.ts": "c5facb0b404eede83e38bd2717ea8ab34faa2ffb20ef87fd261fcba32ba307aa", + "https://deno.land/std@0.177.1/async/debounce.ts": "adab11d04ca38d699444ac8a9d9856b4155e8dda2afd07ce78276c01ea5a4332", + "https://deno.land/std@0.177.1/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", + "https://deno.land/std@0.177.1/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd", + "https://deno.land/std@0.177.1/async/mod.ts": "f04344fa21738e5ad6bea37a6bfffd57c617c2d372bb9f9dcfd118a1b622e576", + "https://deno.land/std@0.177.1/async/mux_async_iterator.ts": "70c7f2ee4e9466161350473ad61cac0b9f115cff4c552eaa7ef9d50c4cbb4cc9", + "https://deno.land/std@0.177.1/async/pool.ts": "fd082bd4aaf26445909889435a5c74334c017847842ec035739b4ae637ae8260", + "https://deno.land/std@0.177.1/async/retry.ts": "5efa3ba450ac0c07a40a82e2df296287b5013755d232049efd7ea2244f15b20f", + "https://deno.land/std@0.177.1/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757", + "https://deno.land/std@0.177.1/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24", + "https://deno.land/std@0.177.1/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "5dedb7f9aa05f0e18ed017691c58df5f4686e4cbbd70368c6f896e5cca03f2b4", + "https://deno.land/std@0.177.1/crypto/_wasm/mod.ts": "e2df88236fc061eac7a89e8cb0b97843f5280b08b2a990e473b7397a3e566003", + "https://deno.land/std@0.177.1/crypto/timing_safe_equal.ts": "8d69ab611c67fe51b6127d97fcfb4d8e7d0e1b6b4f3e0cc4ab86744c3691f965", + "https://deno.land/std@0.177.1/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1", + "https://deno.land/std@0.177.1/encoding/base64url.ts": "3f1178f6446834457b16bfde8b559c1cd3481727fe384d3385e4a9995dc2d851", + "https://deno.land/std@0.177.1/encoding/hex.ts": "50f8c95b52eae24395d3dfcb5ec1ced37c5fe7610ef6fffdcc8b0fdc38e3b32f", + "https://deno.land/std@0.177.1/flags/mod.ts": "d1cdefa18472ef69858a17df5cf7c98445ed27ac10e1460183081303b0ebc270", + "https://deno.land/std@0.177.1/fmt/printf.ts": "e5b426cd6ad13df5d408e9c375c025d59de30e380c5534715bd892df874ab057", + "https://deno.land/std@0.177.1/fs/exists.ts": "b8c8a457b71e9d7f29b9d2f87aad8dba2739cbe637e8926d6ba6e92567875f8e", + "https://deno.land/std@0.177.1/node/_core.ts": "9a58c0ef98ee77e9b8fcc405511d1b37a003a705eb6a9b6e95f75434d8009adc", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/base/buffer.js": "c9364c761681134015ec8ba6f33b39c067d6e5dd59860d55face8d5be8522744", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/base/node.js": "8f7f23bfa300990bbd6db7e7395e9688b54a04e3eb2fab5cab9a9a72e26c525f", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/base/reporter.js": "788aec7662991da549e5f7f3edbc3e3d6c6cecabc894b18d1a705b0f204e06c3", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/constants/der.js": "57181db0519bb3864a6cdf4e7eb9bfeb1bf5f80605187fbe80e27083b473e367", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/decoders/der.js": "fdc4de98c9b0b59db169a2b225895741e2ab34b00e14315ac2ff5e389d6db16e", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/decoders/pem.js": "fd7f0072c193c82959fec0374f4fd3adf3f4ac38594fd404d66b3e8724107151", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/encoders/der.js": "137bc4f8fe66b9950c743025e199789e25342f791e2d52353ceb016ad2854b42", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/encoders/pem.js": "e43bc706973c4c27e1e2f96262daba3d38822cb10f5b494f6944c726ee655160", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/asn1.js/mod.js": "1f88293688296be7a6c735bd8ea39425f5b274b94db1d6b7968dddfb54ac9d37", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/bn.js/bn.js": "f3f3c1dae1aa55de9e6472af1d6bec5ccda4b4890ee5c52a90961137fe99564e", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/aes.js": "698e1ed386b7dff27b2d59fa1c75f506beceec96b78670a15a734e438c08f138", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/auth_cipher.js": "5c245b5685b066356a7c9529a3a441bf5f57823a6946ce1b0ef2e1af32bb76f4", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/decrypter.js": "39152b2b3409893b8548feeab7e5997ceb1595f31df0dedaf765708be8f025c0", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/encrypter.js": "f9cc703d5a7b5255999c1a3600fbf48ff564b65f827744877526803093ceebff", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/ghash.js": "759d80b760f44cd3a454b4f161fd03a7d6c359901446f0a907a6870cb66d6767", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/incr32.js": "2bdea27b74b3990ee56807a1a5abe335f118826beabeeb905459c8768094b28f", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/mod.js": "fe4affebbd210d885b2e5135c668751f9d10bc14aa0cc3905cbfff66f04b4c58", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/cbc.js": "ff24b4506522a724ba7a03c1403ad8938aba45056f9fd47c7f0b4fcb3a640adf", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/cfb.js": "643720a1db969b6bcc896c95523630838a8335513d02f340514fd524bb4113cb", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/cfb1.js": "01c9a46aa3affd84a54ae33652fb0fa0ff7c862be2a459d9cb188cb8e2c4b11e", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/cfb8.js": "97476cee25103e02a02b196d7fe6f28a9f0f9e47ee344687d7492bc7282a59f8", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/ctr.js": "1e3835adb753cfe6761e4df8c43d190e31e1ca6a586fd582747c8255c82ed78d", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/ecb.js": "79677b96d4af50c49f0a4f698e5c7e5a64f1d2926b799e0d2eac2cdd5ec7488c", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/mod.js": "fe3db429b867a0a8066c64d7b33b840a1f24cad9174156384a763733f68cf518", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/modes/ofb.js": "3553308f98d078e2006eac39bb6d91818f8bb376b01d962ae98eabf6ee79ad4e", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/stream_cipher.js": "70f50f37ddec530ae95911ca2f286ebd2ddbd54d914ab0be461ec1dc3c61990f", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_aes/xor.ts": "7132baacdb39ba82c3bfe325a60e68ca87469c0ed0cdd0508caf6f40bab852b8", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/browserify_rsa.js": "96e0e4fee7c2cf75ef86d958c709bfc239297a080fd17ace5ea5ab699a1b6174", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/cipher_base.js": "9ebc6ccc364cf7b23024821054d2e72a2d8da8d8a2a36cacdc5aa6cc6770ef93", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/evp_bytes_to_key.ts": "7c4c27b6e321b2d7065a6703d90264921e9a805d91d9dfdb21103393228024e2", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/parse_asn1/asn1.js": "7d99b6df508164169a33377346e8840d519fe2defccb362a023c92c5bd503433", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/parse_asn1/certificate.js": "5795348417b3ec7aafa4854ba55f364e0148eadfdd29d1566c90e617237621bb", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/parse_asn1/fix_proc.js": "858dd3e6ce264d75822cadc21bb55114f4e4867a706abde1663548aa2710fc1b", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/parse_asn1/mod.js": "ea164fbd497ce3d710426742d4b72f71da8954c4ebaeb7eadc33316c5b0060f1", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/public_encrypt/mgf.js": "dfac5008a550b3e7e6b851c4fb42e984aa9e7fae64707888f47f2aa0991c004d", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/public_encrypt/mod.js": "0704326ff3ee2bb0764a964995d1aa62b1147b714ad5465e878ba4d57731e3db", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/public_encrypt/private_decrypt.js": "8a1d11edb176d95d1e3bdf1aff5c3248a986bf9734d1a6b07508e29132d2f65c", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/public_encrypt/public_encrypt.js": "f88b0e3c228d84096fdbc03e614e86bef86e56013cb9628b2425e31b3b142b2c", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/public_encrypt/with_public.js": "752da754d253b5743d89c0f2432b6eb6f8815b80efd9ee588683e10a13d34400", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/public_encrypt/xor.js": "087ebef8f6fcb8ca4c7216cc22de728d9a61ec27b9a036b900681ff25d6409af", + "https://deno.land/std@0.177.1/node/_crypto/crypto_browserify/randombytes.ts": "23bde8be640e274d7bb88cf10d1da8bba252654252dc6a877fed86a77da5952c", + "https://deno.land/std@0.177.1/node/_events.d.ts": "1347437fd6b084d7c9a4e16b9fe7435f00b030970086482edeeb3b179d0775af", + "https://deno.land/std@0.177.1/node/_events.mjs": "d4ba4e629abe3db9f1b14659fd5c282b7da8b2b95eaf13238eee4ebb142a2448", + "https://deno.land/std@0.177.1/node/_fs/_fs_access.ts": "48a722db00fd34ec567c1d03c47f6b94d07658c658eeb7d9a10c6b823ebdefbd", + "https://deno.land/std@0.177.1/node/_fs/_fs_appendFile.ts": "2e5230c88804f4b5bee29efa1ba723d71a53f9b0f85d5e6372509ba12e9c00c3", + "https://deno.land/std@0.177.1/node/_fs/_fs_chmod.ts": "fcba6aa4fe2d9178746b5b4ae7f42a72a971007c855988f0e26ff8f694c3c212", + "https://deno.land/std@0.177.1/node/_fs/_fs_chown.ts": "6a24414772d689f8e83b6f53f134420dc25d752bd5be56cade39e92f182c9c9a", + "https://deno.land/std@0.177.1/node/_fs/_fs_close.ts": "8fc5819affb69fb5708f3babce49cd673133e939cebef0665099da78a0d0be7a", + "https://deno.land/std@0.177.1/node/_fs/_fs_common.ts": "21caae4ab7c07c66244446c63c50291cc553d1224d3f6a0cd7bea688c6b2a815", + "https://deno.land/std@0.177.1/node/_fs/_fs_constants.ts": "22ce5f8b07fa8fd7ba37718ad85f6655954b7585d21e6d0b9d73676c16ef1b15", + "https://deno.land/std@0.177.1/node/_fs/_fs_copy.ts": "9074e3a1609b9ee10ca1a2d77e94836c57190e791a0878f7e03b2f0e4e0d5dfb", + "https://deno.land/std@0.177.1/node/_fs/_fs_dir.ts": "26c16ef8003772c9cd2439b448530443ea09a1508a6d808a5913576c3d11882b", + "https://deno.land/std@0.177.1/node/_fs/_fs_dirent.ts": "e8c30d8059336cb6b122738c487cb46c1bcfc4c99fd6d64186f04b4e1805be34", + "https://deno.land/std@0.177.1/node/_fs/_fs_exists.ts": "012e8bf6a6a9b53f9e6451db6ddabf1b883a25e6aebb8aadf8958b57efffefd0", + "https://deno.land/std@0.177.1/node/_fs/_fs_fdatasync.ts": "cfe9409aed4bfe707fb497fe5be449a678b4ae454c9068f3720138ff06f7a71f", + "https://deno.land/std@0.177.1/node/_fs/_fs_fstat.ts": "b15968d0f0da997960f0814e52beee35aff5e04519f007c3ac1c431829a03ac4", + "https://deno.land/std@0.177.1/node/_fs/_fs_fsync.ts": "902c1d4ef9b022c61a12c5f85db3ec4e14778019697cf453822313f9eab9516b", + "https://deno.land/std@0.177.1/node/_fs/_fs_ftruncate.ts": "36d76a3d6b325345ba6fbef745ec1a39d6efb4472214ede8421449296fd25711", + "https://deno.land/std@0.177.1/node/_fs/_fs_futimes.ts": "75b9aaa28588d94b9d8be3c5ca4b74595cde342d644afc9c5dda1e1dcc1e604f", + "https://deno.land/std@0.177.1/node/_fs/_fs_link.ts": "5cfa4f02cbedf913d90618c1bf130796bc3cdd7cd0e59cf5defb05619ae10b8a", + "https://deno.land/std@0.177.1/node/_fs/_fs_lstat.ts": "da6a26b4745dbb92eda21f07992d16497a6848fe2ded6a425ade4a2418262b57", + "https://deno.land/std@0.177.1/node/_fs/_fs_mkdir.ts": "94e4341f9bbc3bae9f1474e86621d48101a4a863ce51fd6b1170ef244533c494", + "https://deno.land/std@0.177.1/node/_fs/_fs_mkdtemp.ts": "33658ccb449f90d69305868b718f8fe8d72a2a8e2be7136ebd69ba313fd0b4a9", + "https://deno.land/std@0.177.1/node/_fs/_fs_open.ts": "9f728953c07748a54a73bb9ff0013530e33556a688a359a554d5db5b4ed30d06", + "https://deno.land/std@0.177.1/node/_fs/_fs_opendir.ts": "fe65a45b92b6b970da8f3acec15920cb5669c7a19fd07afa8ebcd248ec69740b", + "https://deno.land/std@0.177.1/node/_fs/_fs_read.ts": "a0223081bc460a8af5d1bb01e59a44182629bf7bff7c583031912abf20ac6b04", + "https://deno.land/std@0.177.1/node/_fs/_fs_readFile.ts": "2c155de6b568a4e5d3d089e58723355fc519de2d2c9422f7dd211cda2c8f36dc", + "https://deno.land/std@0.177.1/node/_fs/_fs_readdir.ts": "85f742c2ad38bebb8ba5dee72b37a966fc4b42b10382a76a60d7a2dda0a6278c", + "https://deno.land/std@0.177.1/node/_fs/_fs_readlink.ts": "d5d9746c1d3c76cce0be5045dbb3bfde100406a98f1d4db8243776a2fc5619af", + "https://deno.land/std@0.177.1/node/_fs/_fs_realpath.ts": "671afd8bc1b33126d56155de3827d6ec55361631eec9f4944d7f91835d897329", + "https://deno.land/std@0.177.1/node/_fs/_fs_rename.ts": "2fd973c38ab5c66d806a954914a2d2b6beec55308b6da0616837ba81946bba3b", + "https://deno.land/std@0.177.1/node/_fs/_fs_rm.ts": "27c01d261a3631729f9406d9dc7be263a7adf240094ba9133da511169785023b", + "https://deno.land/std@0.177.1/node/_fs/_fs_rmdir.ts": "d9a35aa265670aba4a6da10cb151139bd69762ccfb88e27f266c1260c244d3ec", + "https://deno.land/std@0.177.1/node/_fs/_fs_stat.ts": "bf1ca585b624f5b183ff547f02ad40b51d47247a7fd5df84f8c27376e7a7c2d5", + "https://deno.land/std@0.177.1/node/_fs/_fs_symlink.ts": "89752d75dd823be7ea2c0f2ca024b14c954f7d1507360abf883245f4b700464b", + "https://deno.land/std@0.177.1/node/_fs/_fs_truncate.ts": "4333d191574be1d6ab20fdee346c0dd4868e5c9c5e8ee716e3b09bf562aee698", + "https://deno.land/std@0.177.1/node/_fs/_fs_unlink.ts": "6a760088a99c7465d9da3cbd67a456a6207c9764c65926ce1e0d3172aab780a2", + "https://deno.land/std@0.177.1/node/_fs/_fs_utimes.ts": "c433ef58bfd20d84d0f940c17575b496dcd4706e8dc86aea777c73f667164444", + "https://deno.land/std@0.177.1/node/_fs/_fs_watch.ts": "2ed05b68759e1771515efa4c6d19db9c956cfbc79a715d61e4ce8f38ac12c966", + "https://deno.land/std@0.177.1/node/_fs/_fs_write.d.ts": "a405627931c1a5a3160d3f1cf028761d51b50cd632d6602cb0f98c6b39c96b23", + "https://deno.land/std@0.177.1/node/_fs/_fs_write.mjs": "595abc0d7be9ef3709b62bf09972c2836b25c945f4c531a6688b910e428e1b42", + "https://deno.land/std@0.177.1/node/_fs/_fs_writeFile.ts": "c65f61a167e5f80f29a88147012ade2a81233c882e51c6a07f45a153f2316a58", + "https://deno.land/std@0.177.1/node/_fs/_fs_writev.d.ts": "2cd3596fe24579debe43b587d5bb5845f6f0ce3913357376eb279511ce832d15", + "https://deno.land/std@0.177.1/node/_fs/_fs_writev.mjs": "54adae0d5e5148d2ee0690d04f7272dbccd1242ffbdf838778ac514c10197844", + "https://deno.land/std@0.177.1/node/_global.d.ts": "2d88342f38b4083b858998e27c706725fb03a74aa14ef8d985dc18438b5188e4", + "https://deno.land/std@0.177.1/node/_next_tick.ts": "9a3cf107d59b019a355d3cf32275b4c6157282e4b68ea85b46a799cb1d379305", + "https://deno.land/std@0.177.1/node/_process/exiting.ts": "6e336180aaabd1192bf99ffeb0d14b689116a3dec1dfb34a2afbacd6766e98ab", + "https://deno.land/std@0.177.1/node/_process/process.ts": "c96bb1f6253824c372f4866ee006dcefda02b7050d46759736e403f862d91051", + "https://deno.land/std@0.177.1/node/_process/stdio.mjs": "cf17727eac8da3a665851df700b5aca6a12bacc3ebbf33e63e4b919f80ba44a6", + "https://deno.land/std@0.177.1/node/_process/streams.mjs": "408777fba99580567f3ee82ee584ca79012cc550f8dacb8c5ec633b58cd0c1ca", + "https://deno.land/std@0.177.1/node/_stream.d.ts": "112e1a0677cd6db932c3ce0e6e5bbdc7a2ac1874572f449044ecc82afcf5ee2e", + "https://deno.land/std@0.177.1/node/_stream.mjs": "d6e2c86c1158ac65b4c2ca4fa019d7e84374ff12e21e2175345fe68c0823efe3", + "https://deno.land/std@0.177.1/node/_util/_util_callbackify.ts": "a7ffe799ac5f54f3a780ee1c9b190b94dc7dc8afbb430c0e1c73756638d25d64", + "https://deno.land/std@0.177.1/node/_utils.ts": "7fd55872a0cf9275e3c080a60e2fa6d45b8de9e956ebcde9053e72a344185884", + "https://deno.land/std@0.177.1/node/buffer.ts": "85617be2063eccaf177dbb84c7580d1e32023724ed14bd9df4e453b152a26167", + "https://deno.land/std@0.177.1/node/crypto.ts": "2c94fa0f76e90190fbc34df891dc5c284bddb86c932fae8ac11747de3f75293c", + "https://deno.land/std@0.177.1/node/diagnostics_channel.ts": "f1f0abbb0079f7045ed7854ca3a7cda53ef0e722ba09a526a59c21cca23302ed", + "https://deno.land/std@0.177.1/node/dns.ts": "59dded3657ecbe2d2ac5557fa7dee1d35a9616ccefd8f951b89693820804d8b6", + "https://deno.land/std@0.177.1/node/events.ts": "d2de352d509de11a375e2cb397d6b98f5fed4e562fc1d41be33214903a38e6b0", + "https://deno.land/std@0.177.1/node/fs.ts": "de13cb511655b594157b327cd11bb833cc96051409f34148f043e8a8a92d66a1", + "https://deno.land/std@0.177.1/node/fs/promises.ts": "5db686797cec9a6bc7b1460beb7e049ada81a43bbc0ff8231a26442261ec3fd0", + "https://deno.land/std@0.177.1/node/internal/assert.mjs": "1d50c20eeaf16a6d9c1d90347e497669cebc915f5ee238417a73847eb4c2f0de", + "https://deno.land/std@0.177.1/node/internal/async_hooks.ts": "214402ac7a17dfef247cdde223d1b1c3ee0c23b1b9b56eaaff789a3de86b3be9", + "https://deno.land/std@0.177.1/node/internal/buffer.d.ts": "bdfa991cd88cb02fd08bf8235d2618550e3e511c970b2a8f2e1a6885a2793cac", + "https://deno.land/std@0.177.1/node/internal/buffer.mjs": "e92303a3cc6d9aaabcd270a937ad9319825d9ba08cb332650944df4562029b27", + "https://deno.land/std@0.177.1/node/internal/crypto/_keys.ts": "8f3c3b5a141aa0331a53c205e9338655f1b3b307a08085fd6ff6dda6f7c4190b", + "https://deno.land/std@0.177.1/node/internal/crypto/_randomBytes.ts": "36dd164747f73b830ba86562abb160a8ac5bea34aaeb816a67f3005a00d41177", + "https://deno.land/std@0.177.1/node/internal/crypto/_randomFill.ts": "297186f290eba87a1ad7b8aa42a960ff4278a8b6b0c963fa81918c326d5c0b58", + "https://deno.land/std@0.177.1/node/internal/crypto/_randomInt.ts": "6cf19da9684b67520e67a2d99f2581a3f841140842c7ce2e014d166457550fe1", + "https://deno.land/std@0.177.1/node/internal/crypto/certificate.ts": "b4a6695f82e70a42e85247c74a7691ed4b3a904646451af0287e49efe1a28814", + "https://deno.land/std@0.177.1/node/internal/crypto/cipher.ts": "2bae9b4d94c465e4d1c70e5a9e8fd67ce20bcc66fecd2eec6be00d35144ca4eb", + "https://deno.land/std@0.177.1/node/internal/crypto/constants.ts": "544d605703053218499b08214f2e25cf4310651d535b7ab995891c4b7a217693", + "https://deno.land/std@0.177.1/node/internal/crypto/diffiehellman.ts": "9cfb219c5b2936db773f559b6affe6d25b0e40531010389f05df3f05ce7eebf5", + "https://deno.land/std@0.177.1/node/internal/crypto/hash.ts": "d01f5d3ad5477655b432036d2d553c7a0c31a901ac0e1e9e0d8b3975daae7624", + "https://deno.land/std@0.177.1/node/internal/crypto/hkdf.ts": "5bd801234e56468fbd47466f46e88bdadc66432d625e3616abe38878d410bb66", + "https://deno.land/std@0.177.1/node/internal/crypto/keygen.ts": "530cc1a00acf71a43719bb876a2dc563b6196095d080eba77c92c9f39658a5b9", + "https://deno.land/std@0.177.1/node/internal/crypto/keys.ts": "c4dfa5aa3420cf700178b87203593a0989c8a93934bfef2b29adb3399d687958", + "https://deno.land/std@0.177.1/node/internal/crypto/pbkdf2.ts": "0a0a3e0d3d45db0638fe75a4199c7ed7ca2164405750a520e786e4adebdb45a4", + "https://deno.land/std@0.177.1/node/internal/crypto/random.ts": "85f3147e14cb45c18e016da45d319a5c663309411232a956fdc09c2317acdd9f", + "https://deno.land/std@0.177.1/node/internal/crypto/scrypt.ts": "b55a0fcd12b295af4127d05b1c0bc3098b74fc0e3c62321c2a43c20f9ed18209", + "https://deno.land/std@0.177.1/node/internal/crypto/sig.ts": "25819a89d49c1ebfe3baa1f9464501ec599a36cf53e9b600ec0399e568b9dccc", + "https://deno.land/std@0.177.1/node/internal/crypto/types.ts": "52feb182bcbd59206f3e2f4a3cb8a5775d4452c2a8045c3e613e2178d32c2a86", + "https://deno.land/std@0.177.1/node/internal/crypto/util.ts": "db282c0413aeee28bc0665fcfc1c08a65fc96dc12ed4d03282f2da4907fcf0ce", + "https://deno.land/std@0.177.1/node/internal/crypto/x509.ts": "0e8a541c4f58ecb83862c373d3f7d2371aa8f5108f55bc837b190c4ab3408764", + "https://deno.land/std@0.177.1/node/internal/dns/promises.ts": "8119d052137a94dfd666933df33ba79a84cb1cb4249987d63fc6ff2c97344b06", + "https://deno.land/std@0.177.1/node/internal/dns/utils.ts": "c25b905c27e9a1509dc62f54f26f982ecac4fd6befa9df81d6e0aeb14a9bb6a3", + "https://deno.land/std@0.177.1/node/internal/dtrace.ts": "73765e366495f37c2b13dd54f2544d466b4ee9efcb1a68a8934f2d6ae184b7b8", + "https://deno.land/std@0.177.1/node/internal/error_codes.ts": "8495e33f448a484518d76fa3d41d34fc20fe03c14b30130ad8e936b0035d4b8b", + "https://deno.land/std@0.177.1/node/internal/errors.ts": "1c699b8a3cb93174f697a348c004b1c6d576b66688eac8a48ebb78e65c720aae", + "https://deno.land/std@0.177.1/node/internal/fixed_queue.ts": "62bb119afa5b5ae8fc0c7048b50502347bec82e2588017d0b250c4671d6eff8f", + "https://deno.land/std@0.177.1/node/internal/fs/streams.d.ts": "23571ff9af59d86307831b80823e440953f3e57b134ca7ec6e55b60b845d38de", + "https://deno.land/std@0.177.1/node/internal/fs/streams.mjs": "5de00d105009fb8cec6b6d0a6e6e6288ae40879cc64d9bf7a84852220be9fa34", + "https://deno.land/std@0.177.1/node/internal/fs/utils.mjs": "64b6dc17752fa861b46a0876647336ba24efe3b5130bd1826f1f2d59b9b374ed", + "https://deno.land/std@0.177.1/node/internal/hide_stack_frames.ts": "9dd1bad0a6e62a1042ce3a51eb1b1ecee2f246907bff44835f86e8f021de679a", + "https://deno.land/std@0.177.1/node/internal/idna.ts": "034043ac9273eb5ba83112c926dba1777775f1eca40e021c8703cd1720bedd9f", + "https://deno.land/std@0.177.1/node/internal/net.ts": "5538d31b595ac63d4b3e90393168bc65ace2f332c3317cffa2fd780070b2d86c", + "https://deno.land/std@0.177.1/node/internal/normalize_encoding.mjs": "fd1d9df61c44d7196432f6e8244621468715131d18cc79cd299fc78ac549f707", + "https://deno.land/std@0.177.1/node/internal/options.ts": "888f267c3fe8f18dc7b2f2fbdbe7e4a0fd3302ff3e99f5d6645601e924f3e3fb", + "https://deno.land/std@0.177.1/node/internal/primordials.mjs": "a72d86b5aa55d3d50b8e916b6a59b7cc0dc5a31da8937114b4a113ad5aa08c74", + "https://deno.land/std@0.177.1/node/internal/process/per_thread.mjs": "10142bbb13978c2f8f79778ad90f3a67a8ea6d8d2970f3dfc6bf2c6fff0162a2", + "https://deno.land/std@0.177.1/node/internal/querystring.ts": "479f30c136555dc3b6f09af7d0de8a70c753035c1d5b57acc696722028788323", + "https://deno.land/std@0.177.1/node/internal/readline/callbacks.mjs": "bdb129b140c3b21b5e08cdc3d8e43517ad818ac03f75197338d665cca1cbaed3", + "https://deno.land/std@0.177.1/node/internal/readline/utils.mjs": "c3dbf3a97c01ed14052cca3848f09e2fc24818c1822ceed57c33b9f0840f3b87", + "https://deno.land/std@0.177.1/node/internal/stream_base_commons.ts": "c5b60d12da6867953bf09349060db3702778ac72326fc50a3de0b2c4506edf0e", + "https://deno.land/std@0.177.1/node/internal/streams/destroy.mjs": "b665fc71178919a34ddeac8389d162a81b4bc693ff7dc2557fa41b3a91011967", + "https://deno.land/std@0.177.1/node/internal/streams/end-of-stream.mjs": "a4fb1c2e32d58dff440d4e716e2c4daaa403b3095304a028bb428575cfeed716", + "https://deno.land/std@0.177.1/node/internal/streams/utils.mjs": "f2fe2e6bdc506da24c758970890cc2a21642045b129dee618bd3827c60dd9e33", + "https://deno.land/std@0.177.1/node/internal/streams/writable.mjs": "775928726d0483ace8e45a35f30db2019a22dd7b9a81b67b158420e21cc692c5", + "https://deno.land/std@0.177.1/node/internal/timers.mjs": "f174a7a5c24c22460672b30e696d134bb1a37eed40ffc4d24c567c3f5624478b", + "https://deno.land/std@0.177.1/node/internal/url.ts": "7e62e16520de552c130c354d9c725a2f5e2af453ff929a2009fa66ae445bbe14", + "https://deno.land/std@0.177.1/node/internal/util.mjs": "f7fe2e1ca5e66f550ad0856b9f5ee4d666f0c071fe212ea7fc7f37cfa81f97a5", + "https://deno.land/std@0.177.1/node/internal/util/comparisons.ts": "9a7d95401b3d1c99ec5b12250cf6dec75efc75764b4a18be257dd8bfbe67496e", + "https://deno.land/std@0.177.1/node/internal/util/debuglog.ts": "a2392980a65cc6916afc17fa6686242ee0e3b47bd98c792ff59358560b24185e", + "https://deno.land/std@0.177.1/node/internal/util/inspect.mjs": "11d7c9cab514b8e485acc3978c74b837263ff9c08ae4537fa18ad56bae633259", + "https://deno.land/std@0.177.1/node/internal/util/types.ts": "0e587b44ec5e017cf228589fc5ce9983b75beece6c39409c34170cfad49d6417", + "https://deno.land/std@0.177.1/node/internal/validators.mjs": "e02f2b02dd072a5d623970292588d541204dc82207b4c58985d933a5f4b382e6", + "https://deno.land/std@0.177.1/node/internal_binding/_libuv_winerror.ts": "30c9569603d4b97a1f1a034d88a3f74800d5ea1f12fcc3d225c9899d4e1a518b", + "https://deno.land/std@0.177.1/node/internal_binding/_listen.ts": "c6038be47116f7755c01fd98340a0d1e8e66ef874710ab59ed3f5607d50d7a25", + "https://deno.land/std@0.177.1/node/internal_binding/_node.ts": "cb2389b0eab121df99853eb6a5e3a684e4537e065fb8bf2cca0cbf219ce4e32e", + "https://deno.land/std@0.177.1/node/internal_binding/_timingSafeEqual.ts": "7d9732464d3c669ff07713868ce5d25bc974a06112edbfb5f017fc3c70c0853e", + "https://deno.land/std@0.177.1/node/internal_binding/_utils.ts": "7c58a2fbb031a204dee9583ba211cf9c67922112fe77e7f0b3226112469e9fe1", + "https://deno.land/std@0.177.1/node/internal_binding/_winerror.ts": "3e8cfdfe22e89f13d2b28529bab35155e6b1730c0221ec5a6fc7077dc037be13", + "https://deno.land/std@0.177.1/node/internal_binding/ares.ts": "bdd34c679265a6c115a8cfdde000656837a0a0dcdb0e4c258e622e136e9c31b8", + "https://deno.land/std@0.177.1/node/internal_binding/async_wrap.ts": "0dc5ae64eea2c9e57ab17887ef1573922245167ffe38e3685c28d636f487f1b7", + "https://deno.land/std@0.177.1/node/internal_binding/buffer.ts": "31729e0537921d6c730ad0afea44a7e8a0a1044d070ade8368226cb6f7390c8b", + "https://deno.land/std@0.177.1/node/internal_binding/cares_wrap.ts": "9b7247772167f8ed56acd0244a232d9d50e8d7c9cfc379f77f3d54cecc2f32ab", + "https://deno.land/std@0.177.1/node/internal_binding/config.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/connection_wrap.ts": "7dd089ea46de38e4992d0f43a09b586e4cf04878fb06863c1cb8cb2ece7da521", + "https://deno.land/std@0.177.1/node/internal_binding/constants.ts": "21ff9d1ee71d0a2086541083a7711842fc6ae25e264dbf45c73815aadce06f4c", + "https://deno.land/std@0.177.1/node/internal_binding/contextify.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/credentials.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/crypto.ts": "29e8f94f283a2e7d4229d3551369c6a40c2af9737fad948cb9be56bef6c468cd", + "https://deno.land/std@0.177.1/node/internal_binding/errors.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/fs.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/fs_dir.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/fs_event_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/handle_wrap.ts": "adf0b8063da2c54f26edd5e8ec50296a4d38e42716a70a229f14654b17a071d9", + "https://deno.land/std@0.177.1/node/internal_binding/heap_utils.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/http_parser.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/icu.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/inspector.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/js_stream.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/messaging.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/mod.ts": "9fc65f7af1d35e2d3557539a558ea9ad7a9954eefafe614ad82d94bddfe25845", + "https://deno.land/std@0.177.1/node/internal_binding/module_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/native_module.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/natives.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/node_file.ts": "21edbbc95653e45514aff252b6cae7bf127a4338cbc5f090557d258aa205d8a5", + "https://deno.land/std@0.177.1/node/internal_binding/node_options.ts": "0b5cb0bf4379a39278d7b7bb6bb2c2751baf428fe437abe5ed3e8441fae1f18b", + "https://deno.land/std@0.177.1/node/internal_binding/options.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/os.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/performance.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/pipe_wrap.ts": "30e3a63954313f9d5bbc2ac02c7f9be4b1204c493e47f6e1b9c7366994e6ea6d", + "https://deno.land/std@0.177.1/node/internal_binding/process_methods.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/report.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/serdes.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/signal_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/spawn_sync.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/stream_wrap.ts": "452bff74d1db280a0cd78c75a95bb6d163e849e06e9638c4af405d40296bd050", + "https://deno.land/std@0.177.1/node/internal_binding/string_decoder.ts": "54c3c1cbd5a9254881be58bf22637965dc69535483014dab60487e299cb95445", + "https://deno.land/std@0.177.1/node/internal_binding/symbols.ts": "4dee2f3a400d711fd57fa3430b8de1fdb011e08e260b81fef5b81cc06ed77129", + "https://deno.land/std@0.177.1/node/internal_binding/task_queue.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/tcp_wrap.ts": "d298d855e862fc9a5c94e13ad982fde99f6d8a56620a4772681b7226f5a15c91", + "https://deno.land/std@0.177.1/node/internal_binding/timers.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/tls_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/trace_events.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/tty_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/types.ts": "2187595a58d2cf0134f4db6cc2a12bf777f452f52b15b6c3aed73fa072aa5fc3", + "https://deno.land/std@0.177.1/node/internal_binding/udp_wrap.ts": "b77d7024aef1282b9fe6e1f6c8064ab8a7b9ecbae0bc08a36f2b30dcbb1d2752", + "https://deno.land/std@0.177.1/node/internal_binding/url.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/util.ts": "808ff3b92740284184ab824adfc420e75398c88c8bccf5111f0c24ac18c48f10", + "https://deno.land/std@0.177.1/node/internal_binding/uv.ts": "eb0048e30af4db407fb3f95563e30d70efd6187051c033713b0a5b768593a3a3", + "https://deno.land/std@0.177.1/node/internal_binding/v8.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/worker.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/internal_binding/zlib.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", + "https://deno.land/std@0.177.1/node/net.ts": "11987c92efb37b1c01d251caf852f71252205d9177d9521f9c35a226602e4f70", + "https://deno.land/std@0.177.1/node/path.ts": "1c6aa9101554136525b368e8280f0f78136d4071dd71ad3a70477f27d9e4dd91", + "https://deno.land/std@0.177.1/node/path/_constants.ts": "2e2f68b8679cbf0ef118de8e5719e90cfb091de17d4a7c026c911b6772e6a247", + "https://deno.land/std@0.177.1/node/path/_interface.ts": "c67d76726d0f86ea62ec68d17f11d50680c4659a60a0ea6dcd2488109435b4ce", + "https://deno.land/std@0.177.1/node/path/_util.ts": "44deaf5bbd947eafb3439ea7208d0625e231c5f55c421fe83f5ef91218dcd28c", + "https://deno.land/std@0.177.1/node/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.177.1/node/path/glob.ts": "b5fc2aed74aa7511cfd07d52dcd595cc18cd7ca431326a664e735d8905d85ce8", + "https://deno.land/std@0.177.1/node/path/mod.ts": "cad27b16a7a3a8c2bb3ad1ba68a63d11e4fb616d63fd55c95e399a0a3a927be2", + "https://deno.land/std@0.177.1/node/path/posix.ts": "a066e77f554358a82b4a693726faa41932f02f5bcd520f07afb6b2372e62484d", + "https://deno.land/std@0.177.1/node/path/separator.ts": "5cfefe182e88bc8138022475703a9b39b13250c79bf234cdc6e3be9afd639662", + "https://deno.land/std@0.177.1/node/path/win32.ts": "3a1b21948e0063cf1ac1c6834ef3ed633b5405f107be01aadfaedd2088b57eef", + "https://deno.land/std@0.177.1/node/process.ts": "6608012d6d51a17a7346f36079c574b9b9f81f1b5c35436489ad089f39757466", + "https://deno.land/std@0.177.1/node/querystring.ts": "2dce8068cb80ce2bf503aecd888be1b89827288352b6581e0fc401886d56cd86", + "https://deno.land/std@0.177.1/node/stream.ts": "09e348302af40dcc7dc58aa5e40fdff868d11d8d6b0cfb85cbb9c75b9fe450c7", + "https://deno.land/std@0.177.1/node/stream/promises.mjs": "af39c45adb266b1dc029cb1a3e7278e47f5a2455e8d4cf8fe50752425b5c2d49", + "https://deno.land/std@0.177.1/node/string_decoder.ts": "1a17e3572037c512cc5fc4b29076613e90f225474362d18da908cb7e5ccb7e88", + "https://deno.land/std@0.177.1/node/timers.ts": "36992313d4fc894b43f80a12fd35acc4614256fb910d8902d5fc969c1c47cb50", + "https://deno.land/std@0.177.1/node/url.ts": "f8c6656f32728a447705a273e3d8a5118631c0b6560d13fc613901ec9a3f69d0", + "https://deno.land/std@0.177.1/node/util.ts": "4c12edeafde7e50dfe2d4022e383decb422c77858b938b093698cb7250c9e125", + "https://deno.land/std@0.177.1/node/util/types.ts": "461b2e1118fd32456967e14b99f01c892dee1e94d144d6b96e9d94eb086a9574", + "https://deno.land/std@0.177.1/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.177.1/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.177.1/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.177.1/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.177.1/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.177.1/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", + "https://deno.land/std@0.177.1/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.177.1/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.177.1/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.177.1/streams/write_all.ts": "3b2e1ce44913f966348ce353d02fa5369e94115181037cd8b602510853ec3033", + "https://deno.land/std@0.177.1/types.d.ts": "220ed56662a0bd393ba5d124aa6ae2ad36a00d2fcbc0e8666a65f4606aaa9784", "https://deno.land/std@0.178.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", "https://deno.land/std@0.178.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", "https://deno.land/std@0.178.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", @@ -251,6 +564,15 @@ "https://deno.land/std@0.178.0/streams/writer_from_stream_writer.ts": "31126a6bf2e678c5a718011d4831dbe75dbdbd885965d3dbd5dd105e6f20f976", "https://deno.land/std@0.178.0/streams/zip_readable_streams.ts": "9eb82070d83055fe6f077192fb204dc7612695a4b330148e9aa376df1a65e708", "https://deno.land/std@0.178.0/types.d.ts": "220ed56662a0bd393ba5d124aa6ae2ad36a00d2fcbc0e8666a65f4606aaa9784", + "https://deno.land/std@0.180.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.180.0/testing/bdd.ts": "c5ca6d85940dbcc19b4d2bc3608d49ab65d81470aa91306d5efa4b0d5c945731", + "https://deno.land/std@0.187.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.187.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", + "https://deno.land/std@0.187.0/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd", + "https://deno.land/std@0.187.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.187.0/io/buf_reader.ts": "06fff3337091c49e99ebd2dd790c9a90364c087a2953ea081667400fd6c6cebb", + "https://deno.land/std@0.187.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd", + "https://deno.land/std@0.187.0/types.d.ts": "dbaeb2c4d7c526db9828fc8df89d8aecf53b9ced72e0c4568f97ddd8cda616a4", "https://deno.land/x/debuglog@v1.0.0/debug.ts": "032cfda27de5d294218ff7d6611f0b9de7e7c4c3a8a801eeaa49573e15809998", "https://deno.land/x/debuglog@v1.0.0/format.ts": "efc757450d212872880177f3d4284a8bcf11551ec5e33ee2de24977b68242c2b", "https://deno.land/x/debuglog@v1.0.0/utils.ts": "bce2e8ce7956a04e79eb184f475fc8a2dc6a0eace0c8dbc69707498e6f33bd6b", @@ -635,25 +957,25 @@ "https://deno.land/x/ramda@v0.27.2/source/zip.js": "29a04e7dd0a012e391fc595900b4cb0124a13c738f4e6f4489c7173b4a6642dc", "https://deno.land/x/ramda@v0.27.2/source/zipObj.js": "c463fe415ecf647561a12ed9dd3444a0be4dd642e787e156fe8caf7fee0ce4ca", "https://deno.land/x/ramda@v0.27.2/source/zipWith.js": "2081388ae101a01f60589f0a74b7a8e75fdc7a6416c224673f197633750041ec", - "https://deno.land/x/redis@v0.29.2/backoff.ts": "33e4a6e245f8743fbae0ce583993a671a3ac2ecee433a3e7f0bd77b5dd541d84", - "https://deno.land/x/redis@v0.29.2/command.ts": "9d1a679821c9065298fc2b8831b6532605426259b17a1f9d41ee9219180244c7", - "https://deno.land/x/redis@v0.29.2/connection.ts": "335f0aa728b8e0b90353c4015daf5abaa7cae657739eb66cb48324f2d197dcc0", - "https://deno.land/x/redis@v0.29.2/errors.ts": "589c41fc0fbd9fbac896a2517cf461a6af211432dd4bb77e0681f5a256d46e66", - "https://deno.land/x/redis@v0.29.2/executor.ts": "0e78c2756f6d14fc0a001bb7527c50c7055880d8e052a9964ecf070a3045f8c7", - "https://deno.land/x/redis@v0.29.2/mod.ts": "20908f005f5c102525ce6aa9261648c95c5f61c6cf782b2cbb2fce88b1220f69", - "https://deno.land/x/redis@v0.29.2/pipeline.ts": "80cc26a881149264d51dd019f1044c4ec9012399eca9f516057dc81c9b439370", - "https://deno.land/x/redis@v0.29.2/protocol/_util.ts": "0525f7f444a96b92cd36423abdfe221f8d8de4a018dc5cb6750a428a5fc897c2", - "https://deno.land/x/redis@v0.29.2/protocol/command.ts": "610180ecfaad1d99b32c2a1e3fb41521e3374c4eac0314f2b6e27b2cb3d6a4d9", - "https://deno.land/x/redis@v0.29.2/protocol/mod.ts": "f2601df31d8adc71785b5d19f6a7e43dfce94adbb6735c4dafc1fb129169d11a", - "https://deno.land/x/redis@v0.29.2/protocol/reply.ts": "4b7432263924ca611f08af818436eafd9433eef07a835bc0338a08def5db1a30", - "https://deno.land/x/redis@v0.29.2/protocol/types.ts": "6bf515b69712dfd4ab45caf1d0054b8fcdd6e1afb55b2bd9a221e9ecb066f658", - "https://deno.land/x/redis@v0.29.2/pubsub.ts": "58b7431c1383fc20addfc7ab1272b28e35a4d101173ea59ade28612ad7fef573", - "https://deno.land/x/redis@v0.29.2/redis.ts": "dd1aed5b604f2514475dd7f78cc06b1f9f9dc17897f6f3ebda72ef3d5227b3be", - "https://deno.land/x/redis@v0.29.2/stream.ts": "f116d73cfe04590ff9fa8a3c08be8ff85219d902ef2f6929b8c1e88d92a07810", - "https://deno.land/x/redis@v0.29.2/vendor/https/deno.land/std/async/deferred.ts": "ef161b2d69f52e5f2d5250227afb5fcecfa038217a37555868152d3c47c8d61c", - "https://deno.land/x/redis@v0.29.2/vendor/https/deno.land/std/async/delay.ts": "07bb8709c87bf457da6a7460668aa63afcda738719f5941237cb74ef795f49a8", - "https://deno.land/x/redis@v0.29.2/vendor/https/deno.land/std/io/buf_reader.ts": "b8cd05912ca54ebebc77bddbd38612bfff585ca7f053d94038b8c644e6a17e0b", - "https://deno.land/x/redis@v0.29.2/vendor/https/deno.land/std/io/buf_writer.ts": "2137f495110ebfa615bf9ab684ffc5af902a41ab3911cdd997eefc1ae529c54a", + "https://deno.land/x/redis@v0.29.4/backoff.ts": "33e4a6e245f8743fbae0ce583993a671a3ac2ecee433a3e7f0bd77b5dd541d84", + "https://deno.land/x/redis@v0.29.4/command.ts": "9d1a679821c9065298fc2b8831b6532605426259b17a1f9d41ee9219180244c7", + "https://deno.land/x/redis@v0.29.4/connection.ts": "335f0aa728b8e0b90353c4015daf5abaa7cae657739eb66cb48324f2d197dcc0", + "https://deno.land/x/redis@v0.29.4/errors.ts": "589c41fc0fbd9fbac896a2517cf461a6af211432dd4bb77e0681f5a256d46e66", + "https://deno.land/x/redis@v0.29.4/executor.ts": "0e78c2756f6d14fc0a001bb7527c50c7055880d8e052a9964ecf070a3045f8c7", + "https://deno.land/x/redis@v0.29.4/mod.ts": "20908f005f5c102525ce6aa9261648c95c5f61c6cf782b2cbb2fce88b1220f69", + "https://deno.land/x/redis@v0.29.4/pipeline.ts": "80cc26a881149264d51dd019f1044c4ec9012399eca9f516057dc81c9b439370", + "https://deno.land/x/redis@v0.29.4/protocol/_util.ts": "0525f7f444a96b92cd36423abdfe221f8d8de4a018dc5cb6750a428a5fc897c2", + "https://deno.land/x/redis@v0.29.4/protocol/command.ts": "610180ecfaad1d99b32c2a1e3fb41521e3374c4eac0314f2b6e27b2cb3d6a4d9", + "https://deno.land/x/redis@v0.29.4/protocol/mod.ts": "f2601df31d8adc71785b5d19f6a7e43dfce94adbb6735c4dafc1fb129169d11a", + "https://deno.land/x/redis@v0.29.4/protocol/reply.ts": "4b7432263924ca611f08af818436eafd9433eef07a835bc0338a08def5db1a30", + "https://deno.land/x/redis@v0.29.4/protocol/types.ts": "6bf515b69712dfd4ab45caf1d0054b8fcdd6e1afb55b2bd9a221e9ecb066f658", + "https://deno.land/x/redis@v0.29.4/pubsub.ts": "7764195d03a77aa0ee350cf7a955a16e6a023a4ce367f875c6c394e285cbfd9a", + "https://deno.land/x/redis@v0.29.4/redis.ts": "dd1aed5b604f2514475dd7f78cc06b1f9f9dc17897f6f3ebda72ef3d5227b3be", + "https://deno.land/x/redis@v0.29.4/stream.ts": "baed5a4e67d7c5c5a6ed424205c3e984383c5eadf74d7e0e068f8694865595e4", + "https://deno.land/x/redis@v0.29.4/vendor/https/deno.land/std/async/deferred.ts": "028117b747a399f030f6dffd3eb585a8eef5ea813767c5e8968ad3df4d0d5677", + "https://deno.land/x/redis@v0.29.4/vendor/https/deno.land/std/async/delay.ts": "3e062842ab150df23f26d347ae9871e97348868f345fd7f0cf930e0c0f521ee1", + "https://deno.land/x/redis@v0.29.4/vendor/https/deno.land/std/io/buf_reader.ts": "b4adfdae8bdca88f5088be289d47c9a9500a37d9566fc78333074f5c49aa36e9", + "https://deno.land/x/redis@v0.29.4/vendor/https/deno.land/std/io/buf_writer.ts": "12e497b3e1d1634a49744372820bc9831e1f1ad3ea3c349350ab375166b46a23", "https://deno.land/x/secp256k1@1.7.1/index.ts": "20f025d444598aae2c77eebd107bb91a081564caccd548f8ab01c6de47d0beba", "https://deno.land/x/secp256k1@1.7.1/mod.ts": "a39ef9f3c0cd948a8cd040ce7744ef7c2505d83769412f1aad421697766446b6", "https://raw.githubusercontent.com/denolib/camelcase/master/mod.ts": "005e33da0b4abcd8a71b62ff5a288b2d10a6a29bb56726c9ee6dd8e6854b7281" @@ -664,7 +986,6 @@ "axios@1.2.6": "axios@1.2.6", "bech32@2.0.0": "bech32@2.0.0", "js-yaml@4.1.0": "js-yaml@4.1.0", - "knex@2.4.2": "knex@2.4.2", "mongodb@5.3.0": "mongodb@5.3.0", "mongoose@7.1.1": "mongoose@7.1.1" }, @@ -673,8 +994,8 @@ "integrity": "sha512-2i5IL+VRvSOAhyqOI6lVrPzHiLhmC+gZEfG/4z8YA7/zRvQGs75SdF9qsZiNWPgKd1SK06NirpIU+Hd+XWDPfQ==", "dependencies": {} }, - "@types/node@20.2.1": { - "integrity": "sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==", + "@types/node@20.2.5": { + "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==", "dependencies": {} }, "@types/webidl-conversions@7.0.0": { @@ -684,7 +1005,7 @@ "@types/whatwg-url@8.2.2": { "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", "dependencies": { - "@types/node": "@types/node@20.2.1", + "@types/node": "@types/node@20.2.5", "@types/webidl-conversions": "@types/webidl-conversions@7.0.0" } }, @@ -712,20 +1033,12 @@ "integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==", "dependencies": {} }, - "colorette@2.0.19": { - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "dependencies": {} - }, "combined-stream@1.0.8": { "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "delayed-stream@1.0.0" } }, - "commander@9.5.0": { - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dependencies": {} - }, "debug@4.3.4": { "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { @@ -736,14 +1049,6 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dependencies": {} }, - "escalade@3.1.1": { - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dependencies": {} - }, - "esm@3.2.25": { - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dependencies": {} - }, "follow-redirects@1.15.2": { "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "dependencies": {} @@ -756,38 +1061,10 @@ "mime-types": "mime-types@2.1.35" } }, - "function-bind@1.1.1": { - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dependencies": {} - }, - "get-package-type@0.1.0": { - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dependencies": {} - }, - "getopts@2.3.0": { - "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", - "dependencies": {} - }, - "has@1.0.3": { - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "function-bind@1.1.1" - } - }, - "interpret@2.2.0": { - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dependencies": {} - }, "ip@2.0.0": { "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", "dependencies": {} }, - "is-core-module@2.12.1": { - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dependencies": { - "has": "has@1.0.3" - } - }, "js-yaml@4.1.0": { "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { @@ -798,29 +1075,6 @@ "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", "dependencies": {} }, - "knex@2.4.2": { - "integrity": "sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==", - "dependencies": { - "colorette": "colorette@2.0.19", - "commander": "commander@9.5.0", - "debug": "debug@4.3.4", - "escalade": "escalade@3.1.1", - "esm": "esm@3.2.25", - "get-package-type": "get-package-type@0.1.0", - "getopts": "getopts@2.3.0", - "interpret": "interpret@2.2.0", - "lodash": "lodash@4.17.21", - "pg-connection-string": "pg-connection-string@2.5.0", - "rechoir": "rechoir@0.8.0", - "resolve-from": "resolve-from@5.0.0", - "tarn": "tarn@3.0.2", - "tildify": "tildify@2.0.0" - } - }, - "lodash@4.17.21": { - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dependencies": {} - }, "memory-pager@1.5.0": { "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", "dependencies": {} @@ -881,14 +1135,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dependencies": {} }, - "path-parse@1.0.7": { - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dependencies": {} - }, - "pg-connection-string@2.5.0": { - "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==", - "dependencies": {} - }, "proxy-from-env@1.1.0": { "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dependencies": {} @@ -897,24 +1143,6 @@ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dependencies": {} }, - "rechoir@0.8.0": { - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dependencies": { - "resolve": "resolve@1.22.3" - } - }, - "resolve-from@5.0.0": { - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dependencies": {} - }, - "resolve@1.22.3": { - "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==", - "dependencies": { - "is-core-module": "is-core-module@2.12.1", - "path-parse": "path-parse@1.0.7", - "supports-preserve-symlinks-flag": "supports-preserve-symlinks-flag@1.0.0" - } - }, "saslprep@1.0.3": { "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", "dependencies": { @@ -942,18 +1170,6 @@ "memory-pager": "memory-pager@1.5.0" } }, - "supports-preserve-symlinks-flag@1.0.0": { - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dependencies": {} - }, - "tarn@3.0.2": { - "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", - "dependencies": {} - }, - "tildify@2.0.0": { - "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", - "dependencies": {} - }, "tr46@3.0.0": { "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dependencies": { diff --git a/deployment/maintenance.yaml b/deployment/maintenance.yaml index 542b6636..d9dd962c 100644 --- a/deployment/maintenance.yaml +++ b/deployment/maintenance.yaml @@ -21,7 +21,7 @@ spec: - name: ghcr containers: - name: relay - image: ghcr.io/guakamoli/denostr:v0.1.1-maintenance + image: ghcr.io/denostr-protocol/denostr:v0.1.1-maintenance imagePullPolicy: Always resources: requests: diff --git a/deployment/static-mirroring.yaml b/deployment/static-mirroring.yaml index d7579fd7..a1ea7096 100644 --- a/deployment/static-mirroring.yaml +++ b/deployment/static-mirroring.yaml @@ -21,7 +21,7 @@ spec: - name: ghcr containers: - name: relay - image: ghcr.io/guakamoli/denostr:v0.1.1-static-mirroring + image: ghcr.io/denostr-protocol/denostr:v0.1.1-static-mirroring imagePullPolicy: Always resources: requests: diff --git a/deployment/worker.yaml b/deployment/worker.yaml index c803e69f..af09b031 100644 --- a/deployment/worker.yaml +++ b/deployment/worker.yaml @@ -21,7 +21,7 @@ spec: - name: ghcr containers: - name: relay - image: ghcr.io/guakamoli/denostr:v0.1.1-worker + image: ghcr.io/denostr-protocol/denostr:v0.1.1-worker imagePullPolicy: Always resources: requests: diff --git a/docker-compose.yml b/docker-compose.yml index 4d3a8cb9..4b281d65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: - denostr: - image: ghcr.io/guakamoli/denostr:v0.1.1-worker - container_name: denostr + denostr-server0: + image: ghcr.io/denostr-protocol/denostr:v0.1.1-worker + container_name: denostr-server0 environment: RELAY_PORT: 8008 NOSTR_CONFIG_DIR: /app/.nostr @@ -26,6 +26,30 @@ services: restart: on-failure networks: default: + denostr-maintenance: + image: ghcr.io/denostr-protocol/denostr:v0.1.1-maintenance + container_name: denostr-maintenance + environment: + RELAY_PORT: 8008 + NOSTR_CONFIG_DIR: /app/.nostr + SECRET: ${SECRET} + MONGO_URI: "mongodb://denostr-db0:27017,denostr-db1:27017/denostr?authSource=admin&replicaSet=rs0" + MONGO_MIN_POOL_SIZE: 3 + MONGO_MAX_POOL_SIZE: 5 + DEBUG: "*" + user: deno:deno + volumes: + - ${PWD}/.nostr/settings.yaml:/app/.nostr/settings.yaml + depends_on: + denostr-db0: + condition: service_healthy + denostr-db1: + condition: service_healthy + denostr-db-rs-init: + condition: service_completed_successfully + restart: on-failure + networks: + default: denostr-db0: image: mongo:5.0 container_name: denostr-db0 diff --git a/docs/apply-for-k8s.md b/docs/apply-for-k8s.md index d45e673e..45e4492d 100644 --- a/docs/apply-for-k8s.md +++ b/docs/apply-for-k8s.md @@ -30,6 +30,9 @@ As a required dependency for the application, **MongoDB** must be replaced with ```sh kubectl apply -f deployment/worker.yaml + +# Paid enabled, please execute this command +kubectl apply -f deployment/maintenance.yaml ``` Wait a few minutes to ensure that the application is running. diff --git a/docs/apply-for-k8s_zhCN.md b/docs/apply-for-k8s_zhCN.md index 2b261207..394de2fc 100644 --- a/docs/apply-for-k8s_zhCN.md +++ b/docs/apply-for-k8s_zhCN.md @@ -30,6 +30,9 @@ kubectl apply -f deployment/base.yaml ```sh kubectl apply -f deployment/worker.yaml + +# 开启付费功能,请再执行这句命令 +kubectl apply -f deployment/maintenance.yaml ``` 等待几分钟,以确保应用程序正在运行。 diff --git a/package.json b/package.json index 10b6a74d..e6a65a37 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ ], "repository": { "type": "git", - "url": "https://github.com/Guakamoli/denostr.git" + "url": "https://github.com/denostr-protocol/denostr.git" }, "keywords": [ "deno", @@ -31,10 +31,10 @@ "nostr-relay", "typescript" ], - "author": "Guakamoli ", + "author": "denostr-protocol ", "license": "MIT", "bugs": { - "url": "https://github.com/Guakamoli/denostr/issues" + "url": "https://github.com/denostr-protocol/denostr/issues" }, - "homepage": "https://github.com/Guakamoli/denostr" + "homepage": "https://github.com/denostr-protocol/denostr" } diff --git a/resources/default-settings.yaml b/resources/default-settings.yaml index f94d6176..9710b596 100755 --- a/resources/default-settings.yaml +++ b/resources/default-settings.yaml @@ -27,6 +27,14 @@ paymentsProcessors: lnbits: baseURL: https://lnbits.your-domain.com/ callbackBaseURL: https://relay.your-domain.com/callbacks/lnbits + lnurl: + invoiceURL: https://getalby.com/lnurlp/your-username + nodeless: + baseURL: https://nodeless.io + storeId: your-nodeless-io-store-id + opennode: + baseURL: api.opennode.com + callbackBaseURL: https://relay.your-domain.com/callbacks/opennode network: maxPayloadSize: 524288 # Comment the next line if using CloudFlare proxy diff --git a/resources/index.html b/resources/index.html index d6bef0e7..b4e4ca1d 100644 --- a/resources/index.html +++ b/resources/index.html @@ -58,7 +58,7 @@

{{name}}

- -
+
@@ -104,6 +104,7 @@

Invoice expired!

var invoice = "{{invoice}}"; var pubkey = "{{pubkey}}" var expiresAt = "{{expires_at}}" + var processor = "{{processor}}" var timeout var paid = false var fallbackTimeout @@ -140,7 +141,7 @@

Invoice expired!

hide('pending') show('paid') - }, (error) => { + }).catch((error) => { console.error('error fetching status', error) fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime()) }) @@ -253,6 +254,9 @@

Invoice expired!

sendPayment().catch(() => { document.getElementById('sendPaymentBtn').classList.remove('d-none') }) + if (processor === 'zebedee') { + document.getElementById('powered-by-zebedee').classList.remove('d-none') + } diff --git a/src/@types/base.ts b/src/@types/base.ts index dd15478b..a53972c1 100644 --- a/src/@types/base.ts +++ b/src/@types/base.ts @@ -1,6 +1,6 @@ import net from 'net' -import { Knex } from 'npm:knex@2.4.2' +import mongodb from 'mongodb' import mongoose from 'mongoose' import { EventTags } from '../constants/base.ts' @@ -35,16 +35,14 @@ export type Range = Exclude< export type Factory = (input: TInput) => TOutput -export type DatabaseClient = Knex - -export type DatabaseTransaction = any> = Knex.Transaction +export type DatabaseClient = mongoose.Connection +export type ClientSession = mongodb.ClientSession +export type DatabaseTransaction = ClientSession export interface ContextMetadata { remoteAddress: net.SocketAddress } -export type DatabaseClient1 = mongoose.Connection - export interface IRunnable { run(): void close(callback?: (...args: any[]) => void): void diff --git a/src/@types/controllers.ts b/src/@types/controllers.ts index 831cf654..0032288d 100644 --- a/src/@types/controllers.ts +++ b/src/@types/controllers.ts @@ -1,4 +1,4 @@ -import { Context, helpers, Request, Response, RouterContext as RouterContextRaw, Status } from 'oak' +import { Context, FormDataReader, helpers, Request, Response, RouterContext as RouterContextRaw, Status, Router } from 'oak' type RouterContext = RouterContextRaw @@ -11,5 +11,5 @@ export interface IController { } type NextFunction = () => Promise -export { Context, helpers, Request, Response, Status } +export { Context, FormDataReader, helpers, Request, Response, Status, Router } export type { NextFunction, RouterContext } diff --git a/src/@types/database.ts b/src/@types/database.ts index 465ed8da..76f101a7 100644 --- a/src/@types/database.ts +++ b/src/@types/database.ts @@ -3,6 +3,6 @@ import { DatabaseTransaction } from './base.ts' export interface ITransaction { begin(): Promise get transaction(): DatabaseTransaction - commit(): Promise - rollback(): Promise + commit(): Promise + rollback(): Promise } diff --git a/src/@types/event.ts b/src/@types/event.ts index dfbc8fe8..4a0e78ca 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -1,4 +1,5 @@ import { Buffer } from 'Buffer' +import mongoose from 'mongoose' import { ObjectId } from 'mongodb' import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base.ts' @@ -36,7 +37,7 @@ export interface ParameterizedReplaceableEvent extends Event { [EventDeduplicationMetadataKey]: string[] } -export interface DBEvent { +export interface DBEvent extends mongoose.Document { _id: ObjectId event_id: Buffer event_pubkey: Buffer diff --git a/src/@types/invoice.ts b/src/@types/invoice.ts index 5ddcf44e..e4be54a3 100644 --- a/src/@types/invoice.ts +++ b/src/@types/invoice.ts @@ -1,4 +1,6 @@ import { Buffer } from 'Buffer' +import mongoose from 'mongoose' +import { ObjectId } from 'mongodb' import { Pubkey } from './base.ts' @@ -11,6 +13,7 @@ export enum InvoiceUnit { export enum InvoiceStatus { PENDING = 'pending', COMPLETED = 'completed', + EXPIRED = 'expired', } export interface Invoice { @@ -29,7 +32,12 @@ export interface Invoice { verifyURL?: string } -export interface DBInvoice { +export interface LnurlInvoice extends Invoice { + verifyURL: string +} + +export interface DBInvoice extends mongoose.Document { + _id: ObjectId id: string pubkey: Buffer bolt11: string diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index cf7e6331..7d26d05b 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -1,8 +1,8 @@ -import { PassThrough } from 'stream' +import { PassThrough, Stream } from 'stream' import mongoose from 'mongoose' -import { DatabaseClient, EventId, Pubkey } from './base.ts' +import { DatabaseClient, DatabaseTransaction, EventId, Pubkey } from './base.ts' import { DBEvent, Event } from './event.ts' import { Invoice } from './invoice.ts' import { SubscriptionFilter } from './subscription.ts' @@ -17,19 +17,23 @@ export interface IQueryResult extends Pick, keyof Promise & Exp export interface IEventRepository { create(event: Event): Promise upsert(event: Event): Promise - findByFilters(filters: SubscriptionFilter[]): mongoose.Aggregate + findByFilters(filters: SubscriptionFilter[]): { cursor: Promise | Stream} insertStubs(pubkey: string, eventIdsToDelete: EventId[]): Promise deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise } export interface IInvoiceRepository { findById(id: string, client?: DatabaseClient): Promise - upsert(invoice: Partial, client?: DatabaseClient): Promise + upsert(invoice: Partial, session?: DatabaseTransaction): Promise + updateStatus( + invoice: Pick, + session?: DatabaseTransaction, + ): Promise confirmInvoice( invoiceId: string, amountReceived: bigint, confirmedAt: Date, - client?: DatabaseClient, + session?: DatabaseTransaction, ): Promise findPendingInvoices( offset?: number, @@ -43,6 +47,6 @@ export interface IUserRepository { pubkey: Pubkey, client?: mongoose.Connection, ): Promise - upsert(user: Partial, client?: mongoose.Connection): Promise + upsert(user: Partial, session?: DatabaseTransaction): Promise getBalanceByPubkey(pubkey: Pubkey, client?: mongoose.Connection): Promise } diff --git a/src/@types/services.ts b/src/@types/services.ts index 5db241d0..75ff4f4e 100644 --- a/src/@types/services.ts +++ b/src/@types/services.ts @@ -9,9 +9,9 @@ export interface IPaymentsService { description: string, ): Promise updateInvoice(invoice: Partial): Promise - updateInvoiceStatus(invoice: Partial): Promise + updateInvoiceStatus(invoice: Pick): Promise confirmInvoice( - invoice: Pick, + invoice: Pick, ): Promise sendInvoiceUpdateNotification(invoice: Invoice): Promise getPendingInvoices(): Promise diff --git a/src/@types/settings.ts b/src/@types/settings.ts index 543803b1..adebacd9 100644 --- a/src/@types/settings.ts +++ b/src/@types/settings.ts @@ -162,10 +162,22 @@ export interface LNbitsPaymentProcessor { callbackBaseURL: string } +export interface NodelessPaymentsProcessor { + baseURL: string + storeId: string +} + +export interface OpenNodePaymentsProcessor { + baseURL: string + callbackBaseURL: string +} + export interface PaymentsProcessors { lnurl?: LnurlPaymentsProcessor zebedee?: ZebedeePaymentsProcessor lnbits?: LNbitsPaymentProcessor + nodeless?: NodelessPaymentsProcessor + opennode?: OpenNodePaymentsProcessor } export interface Local { diff --git a/src/@types/user.ts b/src/@types/user.ts index 73a6d300..966f463b 100644 --- a/src/@types/user.ts +++ b/src/@types/user.ts @@ -1,4 +1,5 @@ -import { Buffer } from 'Buffer' +import mongoose from 'mongoose' +import { ObjectId } from 'mongodb' import { Pubkey } from './base.ts' @@ -11,8 +12,9 @@ export interface User { updatedAt: Date } -export interface DBUser { - pubkey: Buffer +export interface DBUser extends mongoose.Document { + _id: ObjectId + pubkey: string is_admitted: boolean balance: bigint created_at: Date diff --git a/src/app/maintenance-worker.ts b/src/app/maintenance-worker.ts index ab536e7a..a5742203 100644 --- a/src/app/maintenance-worker.ts +++ b/src/app/maintenance-worker.ts @@ -41,13 +41,17 @@ export class MaintenanceWorker implements IRunnable { let successful = 0 for (const invoice of invoices) { - debug('invoice %s: %o', invoice.id, invoice) try { debug('getting invoice %s from payment processor: %o', invoice.id, invoice) const updatedInvoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(invoice) await delay() debug('updating invoice status %s: %o', updatedInvoice.id, updatedInvoice) - await this.paymentsService.updateInvoiceStatus(updatedInvoice) + + if (typeof updatedInvoice.id !== 'string' || typeof updatedInvoice.status !== 'string') { + continue + } + const { id, status } = updatedInvoice + await this.paymentsService.updateInvoiceStatus({ id, status }) if ( invoice.status !== updatedInvoice.status && diff --git a/src/cache/client.ts b/src/cache/client.ts index a112a084..627351a8 100644 --- a/src/cache/client.ts +++ b/src/cache/client.ts @@ -1,8 +1,8 @@ -import { connect, RedisConnectOptions } from 'redis' +import { connect, RedisConnectOptions, RedisValue } from 'redis' -import { CacheClient } from '../@types/cache.ts' -import Config from '../config/index.ts' -import { createLogger } from '../factories/logger-factory.ts' +import { CacheClient } from '@/@types/cache.ts' +import Config from '@/config/index.ts' +import { createLogger } from '@/factories/logger-factory.ts' const debug = createLogger('cache-client') @@ -23,8 +23,15 @@ export const getCacheClient = async (): Promise => { const config = getCacheConfig() const { password: _, ...loggableConfig } = config debug('config: %o', loggableConfig) - instance = await connect(config) + if (config.hostname) { + instance = await connect(config) + } } return instance } + +export async function publish(channel: string, message: RedisValue) { + const client = await getCacheClient() + return client.publish(channel, message) +} diff --git a/src/config/index.ts b/src/config/index.ts index 786f323d..a706c61c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -14,6 +14,9 @@ const Config = { MONGO_RR_MAX_POOL_SIZE: Number(Deno.env.get('MONGO_RR_MAX_POOL_SIZE') || 3), ZEBEDEE_API_KEY: Deno.env.get('ZEBEDEE_API_KEY'), LNBITS_API_KEY: Deno.env.get('LNBITS_API_KEY'), + NODELESS_API_KEY: Deno.env.get('NODELESS_API_KEY'), + NODELESS_WEBHOOK_SECRET: Deno.env.get('NODELESS_WEBHOOK_SECRET') || '', + OPENNODE_API_KEY: Deno.env.get('OPENNODE_API_KEY'), NOSTR_CONFIG_DIR: Deno.env.get('NOSTR_CONFIG_DIR'), TOR_HOST: Deno.env.get('TOR_HOST'), TOR_CONTROL_PORT: Deno.env.get('TOR_CONTROL_PORT'), diff --git a/src/constants/adapter.ts b/src/constants/adapter.ts index d06f912e..6b2a9b38 100644 --- a/src/constants/adapter.ts +++ b/src/constants/adapter.ts @@ -11,3 +11,7 @@ export enum WebSocketServerAdapterEvent { Broadcast = 'broadcast', Connection = 'connection', } + +export enum PubSubBroadcastEvent { + Ephemeral = 'events.broadcast.ephemeral', +} diff --git a/src/controllers/callbacks/lnbits-callback-controller.ts b/src/controllers/callbacks/lnbits-callback-controller.ts index ebe99552..a75be09a 100644 --- a/src/controllers/callbacks/lnbits-callback-controller.ts +++ b/src/controllers/callbacks/lnbits-callback-controller.ts @@ -1,8 +1,11 @@ -import { IController, Request, Response, RouterContext, Status } from '../../@types/controllers.ts' -import { Invoice, InvoiceStatus } from '../../@types/invoice.ts' -import { IInvoiceRepository } from '../../@types/repositories.ts' -import { IPaymentsService } from '../../@types/services.ts' -import { createLogger } from '../../factories/logger-factory.ts' +import { helpers, IController, Request, Response, RouterContext, Status } from '@/@types/controllers.ts' +import { Invoice, InvoiceStatus } from '@/@types/invoice.ts' +import { IInvoiceRepository } from '@/@types/repositories.ts' +import { IPaymentsService } from '@/@types/services.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { createSettings } from '@/factories/settings-factory.ts' +import { deriveFromSecret, hmacSha256 } from '@/utils/secret.ts' +import { getRemoteAddress } from '@/utils/http.ts' const debug = createLogger('lnbits-callback-controller') @@ -19,14 +22,46 @@ export class LNbitsCallbackController implements IController { ctx: RouterContext, ) { debug('request headers: %o', request.headers) - debug('request body: %o', request.body) + debug('request body: %o', ctx.state.body) - const body = request.body + const settings = createSettings() + const remoteAddress = getRemoteAddress(request, settings) + const paymentProcessor = settings.payments?.processor ?? 'null' + + if (paymentProcessor !== 'lnbits') { + debug('denied request from %s to /callbacks/lnbits which is not the current payment processor', remoteAddress) + response.status = Status.Forbidden + response.body = 'Forbidden' + return + } + + let validationPassed = false + + const requestQuery = helpers.getQuery(ctx) + if (typeof requestQuery.hmac === 'string' && requestQuery.hmac.match(/^[0-9]{1,20}:[0-9a-f]{64}$/)) { + const split = requestQuery.hmac.split(':') + if (hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), split[0]).toString('hex') === split[1]) { + if (parseInt(split[0]) > Date.now()) { + validationPassed = true + } + } + } + + if (!validationPassed) { + debug('unauthorized request from %s to /callbacks/lnbits', remoteAddress) + response.status = Status.Forbidden + response.body = 'Forbidden' + return + } + + const body = ctx.state.body if ( !body || typeof body !== 'object' || typeof body.payment_hash !== 'string' || body.payment_hash.length !== 64 ) { - ctx.throw(Status.BadRequest, 'Malformed body') + ctx.response.status = Status.BadRequest + ctx.response.body = 'Malformed body' + return } const invoice = await this.paymentsService.getInvoiceFromPaymentsProcessor( @@ -37,7 +72,9 @@ export class LNbitsCallbackController implements IController { ) if (!storedInvoice) { - ctx.throw(Status.NotFound, 'No such invoice') + ctx.response.status = Status.NotFound + ctx.response.body = 'No such invoice' + return } try { @@ -53,12 +90,14 @@ export class LNbitsCallbackController implements IController { ) { response.status = Status.OK response.headers.set('content-type', 'text/plain; charset=utf8') - response.body = '' + response.body = 'OK' return } if (storedInvoice.status === InvoiceStatus.COMPLETED) { - ctx.throw(Status.Conflict, 'Invoice is already marked paid') + ctx.response.status = Status.Conflict + ctx.response.body = 'Invoice is already marked paid' + return } invoice.amountPaid = invoice.amountRequested diff --git a/src/controllers/callbacks/nodeless-callback-controller.ts b/src/controllers/callbacks/nodeless-callback-controller.ts new file mode 100644 index 00000000..8852c1cf --- /dev/null +++ b/src/controllers/callbacks/nodeless-callback-controller.ts @@ -0,0 +1,107 @@ +import { always, applySpec, ifElse, is, path, prop, propEq, propSatisfies } from 'ramda' +import { Buffer } from 'Buffer' + +import { IController, Request, Response, RouterContext, Status } from '@/@types/controllers.ts' +import { Invoice, InvoiceStatus } from '@/@types/invoice.ts' +import { IPaymentsService } from '@/@types/services.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { createSettings } from '@/factories/settings-factory.ts' +import { hmacSha256 } from '@/utils/secret.ts' +import Config from '@/config/index.ts' +import { fromNodelessInvoice } from '@/utils/transform.ts' + +const debug = createLogger('nodeless-callback-controller') + +export class NodelessCallbackController implements IController { + public constructor( + private readonly paymentsService: IPaymentsService, + ) {} + + // TODO: Validate + public async handleRequest( + request: Request, + response: Response, + ctx: RouterContext, + ) { + debug('callback request headers: %o', request.headers) + debug('callback request body: %O', ctx.state.body) + + const settings = createSettings() + const paymentProcessor = settings.payments?.processor + + const body = Buffer.from(await request.body({ type: 'bytes' }).value) + const expected = hmacSha256(Config.NODELESS_WEBHOOK_SECRET, body).toString('hex') + const actual = request.headers.get('nodeless-signature') + + if (expected !== actual) { + console.error('nodeless callback request rejected: signature mismatch:', { expected, actual }) + response.status = Status.Forbidden + response.body = 'Forbidden' + return + } + + if (paymentProcessor !== 'nodeless') { + debug('denied request from %s to /callbacks/nodeless which is not the current payment processor') + response.status = Status.Forbidden + response.body = 'Forbidden' + return + } + + const nodelessInvoice = applySpec({ + id: prop('uuid'), + status: prop('status'), + satsAmount: prop('amount'), + metadata: prop('metadata'), + paidAt: ifElse( + propEq('status', 'paid'), + always(new Date().toISOString()), + always(null), + ), + createdAt: ifElse( + propSatisfies(is(String), 'createdAt'), + prop('createdAt'), + path(['metadata', 'createdAt']), + ), + })(ctx.state.body) + + debug('nodeless invoice: %O', nodelessInvoice) + + const invoice = fromNodelessInvoice(nodelessInvoice) + + debug('invoice: %O', invoice) + + let updatedInvoice: Invoice + try { + updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice) + debug('updated invoice: %O', updatedInvoice) + } catch (error) { + console.error(`Unable to persist invoice ${invoice.id}`, error) + + throw error + } + + if ( + updatedInvoice.status !== InvoiceStatus.COMPLETED && + !updatedInvoice.confirmedAt + ) { + response.status = Status.OK + + return + } + + invoice.amountPaid = invoice.amountRequested + updatedInvoice.amountPaid = invoice.amountRequested + + try { + await this.paymentsService.confirmInvoice(invoice) + await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice) + } catch (error) { + console.error(`Unable to confirm invoice ${invoice.id}`, error) + + throw error + } + + response.status = Status.OK + response.body = { status: 'ok' } + } +} diff --git a/src/controllers/callbacks/opennode-callback-controller.ts b/src/controllers/callbacks/opennode-callback-controller.ts new file mode 100644 index 00000000..31a986aa --- /dev/null +++ b/src/controllers/callbacks/opennode-callback-controller.ts @@ -0,0 +1,66 @@ +import { IController, Request, Response, RouterContext, Status } from '@/@types/controllers.ts' +import { Invoice, InvoiceStatus } from '@/@types/invoice.ts' +import { IPaymentsService } from '@/@types/services.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { fromOpenNodeInvoice } from '@/utils/transform.ts' + +const debug = createLogger('opennode-callback-controller') + +export class OpenNodeCallbackController implements IController { + public constructor( + private readonly paymentsService: IPaymentsService, + ) {} + + // TODO: Validate + public async handleRequest( + request: Request, + response: Response, + ctx: RouterContext, + ) { + debug('request headers: %o', request.headers) + debug('request body: %O', ctx.state.body) + + const invoice = fromOpenNodeInvoice(ctx.state.body) as Invoice + + debug('invoice', invoice) + + let updatedInvoice: Invoice + try { + updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice) + } catch (error) { + console.error(`Unable to persist invoice ${invoice.id}`, error) + + throw error + } + + if ( + updatedInvoice.status !== InvoiceStatus.COMPLETED && + !updatedInvoice.confirmedAt + ) { + response.status = Status.OK + + return + } + + invoice.amountPaid = invoice.amountRequested + updatedInvoice.amountPaid = invoice.amountRequested + + try { + await this.paymentsService.confirmInvoice({ + id: invoice.id, + pubkey: invoice.pubkey, + status: updatedInvoice.status, + amountPaid: updatedInvoice.amountRequested, + confirmedAt: updatedInvoice.confirmedAt, + }) + await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice) + } catch (error) { + console.error(`Unable to confirm invoice ${invoice.id}`, error) + + throw error + } + + response.status = Status.OK + response.body = 'OK' + } +} diff --git a/src/controllers/callbacks/zebedee-callback-controller.ts b/src/controllers/callbacks/zebedee-callback-controller.ts index fa386fad..c09485c8 100644 --- a/src/controllers/callbacks/zebedee-callback-controller.ts +++ b/src/controllers/callbacks/zebedee-callback-controller.ts @@ -1,8 +1,10 @@ -import { IController, Request, Response, Status } from '../../@types/controllers.ts' -import { InvoiceStatus } from '../../@types/invoice.ts' -import { IPaymentsService } from '../../@types/services.ts' -import { createLogger } from '../../factories/logger-factory.ts' -import { fromZebedeeInvoice } from '../../utils/transform.ts' +import { IController, Request, Response, RouterContext, Status } from '@/@types/controllers.ts' +import { Invoice, InvoiceStatus } from '@/@types/invoice.ts' +import { IPaymentsService } from '@/@types/services.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { createSettings } from '@/factories/settings-factory.ts' +import { fromZebedeeInvoice } from '@/utils/transform.ts' +import { getRemoteAddress } from '@/utils/http.ts' const debug = createLogger('zebedee-callback-controller') @@ -15,18 +17,38 @@ export class ZebedeeCallbackController implements IController { public async handleRequest( request: Request, response: Response, + ctx: RouterContext, ) { debug('request headers: %o', request.headers) - debug('request body: %O', request.body) + debug('request body: %O', ctx.state.body) - const invoice = fromZebedeeInvoice(request.body) + const settings = createSettings() + + const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {} + const remoteAddress = getRemoteAddress(request, settings) + const paymentProcessor = settings.payments?.processor + + if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) { + debug('unauthorized request from %s to /callbacks/zebedee', remoteAddress) + response.status = Status.Forbidden + response.body = 'Forbidden' + return + } + + if (paymentProcessor !== 'zebedee') { + debug('denied request from %s to /callbacks/zebedee which is not the current payment processor', remoteAddress) + response.status = Status.Forbidden + response.body = 'Forbidden' + return + } + + const invoice = fromZebedeeInvoice(ctx.state.body) debug('invoice', invoice) + let updatedInvoice: Invoice try { - if (invoice.bolt11) { - await this.paymentsService.updateInvoice(invoice) - } + updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice) } catch (error) { console.error(`Unable to persist invoice ${invoice.id}`, error) @@ -34,26 +56,32 @@ export class ZebedeeCallbackController implements IController { } if ( - invoice.status !== InvoiceStatus.COMPLETED && - !invoice.confirmedAt + updatedInvoice.status !== InvoiceStatus.COMPLETED && + !updatedInvoice.confirmedAt ) { response.status = Status.OK - response.body = '' return } invoice.amountPaid = invoice.amountRequested + invoice.status = updatedInvoice.status + updatedInvoice.amountPaid = invoice.amountRequested try { - await this.paymentsService.confirmInvoice(invoice) - await this.paymentsService.sendInvoiceUpdateNotification(invoice) + await this.paymentsService.confirmInvoice({ + id: invoice.id, + pubkey: invoice.pubkey, + status: invoice.status, + confirmedAt: invoice.confirmedAt, + amountPaid: invoice.amountRequested, + }) + await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice) } catch (error) { console.error(`Unable to confirm invoice ${invoice.id}`, error) throw error } response.status = Status.OK - response.headers.set('content-type', 'text/plain; charset=utf8') response.body = 'OK' } } diff --git a/src/controllers/invoices/get-invoice-controller.ts b/src/controllers/invoices/get-invoice-controller.ts new file mode 100644 index 00000000..32309e2c --- /dev/null +++ b/src/controllers/invoices/get-invoice-controller.ts @@ -0,0 +1,38 @@ +import { path, pathEq } from 'ramda' +import { readFileSync } from 'fs' + +import { IController, Request, Response, Status } from '../../@types/controllers.ts' +import { createSettings } from '../../factories/settings-factory.ts' +import { FeeSchedule } from '../../@types/settings.ts' + +let pageCache: string + +export class GetInvoiceController implements IController { + public async handleRequest( + _: Request, + response: Response, + ): Promise { + const settings = createSettings() + + if ( + pathEq(['payments', 'enabled'], true, settings) && + pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings) + ) { + if (!pageCache) { + const name = path(['info', 'name'])(settings) as string + const feeSchedule = path(['payments', 'feeSchedules', 'admission', '0'], settings) as FeeSchedule + pageCache = readFileSync('./resources/index.html', 'utf8') + .replaceAll('{{name}}', name) + .replaceAll('{{processor}}', settings.payments?.processor) + .replaceAll('{{amount}}', (BigInt(feeSchedule.amount) / 1000n).toString()) + } + + response.status = Status.OK + response.headers.set('content-type', 'text/html') + response.body = pageCache + pageCache = '' + } else { + response.status = Status.NotFound + } + } +} diff --git a/src/controllers/invoices/post-invoice-controller.ts b/src/controllers/invoices/post-invoice-controller.ts index 418749f2..efd28ac1 100644 --- a/src/controllers/invoices/post-invoice-controller.ts +++ b/src/controllers/invoices/post-invoice-controller.ts @@ -35,7 +35,7 @@ export class PostInvoiceController implements IController { } const params = helpers.getQuery(ctx) debug('params: %o', params) - debug('body: %o', request.body) + debug('body: %o', ctx.state.body) const currentSettings = this.settings() @@ -45,39 +45,54 @@ export class PostInvoiceController implements IController { const limited = await this.isRateLimited(request, currentSettings) if (limited) { - ctx.throw(Status.TooManyRequests, 'Too many requests') + ctx.response.status = Status.TooManyRequests + ctx.response.body = 'Too many requests' } - if (!request.body || typeof request.body !== 'object') { - ctx.throw(Status.BadRequest, 'Invalid request') + if (!ctx.state.body || typeof ctx.state.body !== 'object') { + ctx.response.status = Status.BadRequest + ctx.response.body = 'Invalid request' + return } - const tosAccepted = request.body?.tosAccepted === 'yes' + const requestBody = ctx.state.body + + const tosAccepted = requestBody?.tosAccepted === 'yes' if (!tosAccepted) { - ctx.throw(Status.BadRequest, 'ToS agreement: not accepted') + ctx.response.status = Status.BadRequest + ctx.response.body = 'ToS agreement: not accepted' + return } - const isAdmissionInvoice = request.body?.feeSchedule === 'admission' + const isAdmissionInvoice = requestBody?.feeSchedule === 'admission' if (!isAdmissionInvoice) { - ctx.throw(Status.BadRequest, 'Invalid fee') + ctx.response.status = Status.BadRequest + ctx.response.body = 'Invalid fee' + return } - const pubkeyRaw = path(['body', 'pubkey'], request) + const pubkeyRaw = path(['pubkey'], requestBody) let pubkey: string if (typeof pubkeyRaw !== 'string') { - ctx.throw(Status.BadRequest, 'Invalid pubkey: missing') + ctx.response.status = Status.BadRequest + ctx.response.body = 'Invalid pubkey: missing' + return } else if (/^[0-9a-f]{64}$/.test(pubkeyRaw)) { pubkey = pubkeyRaw } else if (/^npub1/.test(pubkeyRaw)) { try { pubkey = fromBech32(pubkeyRaw) } catch { - ctx.throw(Status.BadRequest, 'Invalid pubkey: invalid npub') + ctx.response.status = Status.BadRequest + ctx.response.body = 'Invalid pubkey: invalid npub' + return } } else { - ctx.throw(Status.BadRequest, 'Invalid pubkey: unknown format') + ctx.response.status = Status.BadRequest + ctx.response.body = 'Invalid pubkey: unknown format' + return } const isApplicableFee = (feeSchedule: FeeSchedule) => @@ -87,7 +102,8 @@ export class PostInvoiceController implements IController { .filter(isApplicableFee) if (!Array.isArray(admissionFee) || !admissionFee.length) { - ctx.throw(Status.BadRequest, 'No admission fee required') + ctx.response.status = Status.BadRequest + ctx.response.body = 'No admission fee required' return } @@ -96,7 +112,8 @@ export class PostInvoiceController implements IController { if ( user && user.isAdmitted && (!minBalance || user.balance >= minBalance) ) { - ctx.throw(Status.BadRequest, 'User is already admitted.') + ctx.response.status = Status.BadRequest + ctx.response.body = 'User is already admitted.' return } @@ -115,7 +132,8 @@ export class PostInvoiceController implements IController { ) } catch (error) { console.error('Unable to create invoice. Reason:', error) - ctx.throw(Status.BadRequest, 'Unable to create invoice') + ctx.response.status = Status.BadRequest + ctx.response.body = 'Unable to create invoice' return } @@ -131,6 +149,7 @@ export class PostInvoiceController implements IController { expires_at: invoice.expiresAt?.toISOString() ?? '', invoice: invoice.bolt11, amount: amount / 1000n, + processor: currentSettings.payments.processor, } const body = Object diff --git a/src/core-services/PubSubService.ts b/src/core-services/PubSubService.ts new file mode 100644 index 00000000..e717f8e7 --- /dev/null +++ b/src/core-services/PubSubService.ts @@ -0,0 +1,49 @@ +import { createLogger } from '@/factories/logger-factory.ts' +import { ServiceClass } from '@/core-services/index.ts' +import { CacheClient } from '@/@types/cache.ts' +import { PubSubBroadcastEvent } from '@/constants/adapter.ts' +import { ObjectId } from 'mongodb' +import { toDBEvent } from '@/utils/event.ts' + +const debug = createLogger('core-service:pub-sub-service') + +export class PubSubService extends ServiceClass { + protected name = 'PubSubService' + + constructor(private getCacheClient: () => Promise) { + super() + + this.subscribe() + } + + async subscribe() { + const client = await this.getCacheClient() + if (!client) { + return + } + + const sub = await client.subscribe(PubSubBroadcastEvent.Ephemeral) + for await (const data of sub.receive()) { + const { channel, message } = data + if (channel === PubSubBroadcastEvent.Ephemeral) { + try { + const event = toDBEvent(JSON.parse(message)) + debug('parse event to %o', event) + // fake event id + event._id = new ObjectId() + this.emit('events.broadcast', { + clientAction: 'inserted', + data: event, + id: event._id, + }) + } catch (err) { + console.log('pub-sub-service: A possible weird string. Error:', err) + } + } + } + } + + async started(): Promise { + debug('started') + } +} diff --git a/src/core-services/index.ts b/src/core-services/index.ts index e5ca2d99..6c08fc5e 100644 --- a/src/core-services/index.ts +++ b/src/core-services/index.ts @@ -4,3 +4,4 @@ export { LocalBroker } from './core/LocalBroker.ts' export type { IServiceClass, IServiceContext } from './core/types/ServiceClass.ts' export { ServiceClass } from './core/types/ServiceClass.ts' export { WebSocketServerService } from './WebSocketServerService.ts' +export { PubSubService } from './PubSubService.ts' diff --git a/src/database/models/Events.ts b/src/database/models/Events.ts index 2e92e22d..96c017ff 100644 --- a/src/database/models/Events.ts +++ b/src/database/models/Events.ts @@ -1,12 +1,12 @@ import mongoose, { FilterQuery } from 'mongoose' -import { getMasterDbClient, getReadReplicaDbClient } from '../client.ts' +import { getMasterDbClient, getReadReplicaDbClient } from '@/database/client.ts' import { Buffer } from 'Buffer' -import { DBEvent } from '../../@types/event.ts' -import { SubscriptionFilter } from '../../@types/subscription.ts' -import { isGenericTagQuery } from '../../utils/filter.ts' -import { Sort } from '../../constants/base.ts' -import { toBuffer } from '../../utils/transform.ts' +import { DBEvent } from '@/@types/event.ts' +import { SubscriptionFilter } from '@/@types/subscription.ts' +import { isGenericTagQuery } from '@/utils/filter.ts' +import { Sort } from '@/constants/base.ts' +import { toBuffer } from '@/utils/transform.ts' const EventSchema = new mongoose.Schema({ event_id: { @@ -41,7 +41,10 @@ const EventSchema = new mongoose.Schema({ type: Buffer, }, event_deduplication: [mongoose.Schema.Types.Mixed], - first_seen: { type: Date }, + first_seen: { + type: Date, + default: new Date(), + }, deleted_at: { type: String }, expires_at: { type: Number }, }) diff --git a/src/database/models/Invoices.ts b/src/database/models/Invoices.ts new file mode 100644 index 00000000..71151c3a --- /dev/null +++ b/src/database/models/Invoices.ts @@ -0,0 +1,105 @@ +import mongoose from 'mongoose' + +import { getMasterDbClient, getReadReplicaDbClient } from '@/database/client.ts' +import { DBInvoice } from '@/@types/invoice.ts' + +const InvoiceSchema = new mongoose.Schema({ + id: { + type: String, + }, + pubkey: { + type: String, + }, + bolt11: { + type: String, + }, + amount_requested: { + type: BigInt, + }, + amount_paid: { + type: BigInt, + }, + unit: { + type: String, + enum: ['msats', 'sats', 'btc'], + }, + status: { + type: String, + enum: ['pending', 'completed', 'expired'], + default: 'pending', + }, + description: { + type: String, + }, + confirmed_at: { + type: Date, + }, + expires_at: { + type: Date, + }, + updated_at: { + type: Date, + default: new Date(), + }, + created_at: { + type: Date, + default: new Date(), + }, + verify_url: { + type: String, + }, +}, { + id: true, + _id: false, +}) + +InvoiceSchema.index({ 'id': 1 }, { + unique: true, +}) + +InvoiceSchema.index({ 'pubkey': 1 }, { + background: true, +}) + +InvoiceSchema.index({ 'bolt11': 1 }, { + background: true, +}) + +InvoiceSchema.index({ 'amount_requested': 1 }, { + background: true, +}) + +InvoiceSchema.index({ 'amount_paid': 1 }, { + background: true, + sparse: true, +}) + +InvoiceSchema.index({ 'unit': 1 }, { + background: true, +}) + +InvoiceSchema.index({ 'status': 1 }, { + background: true, +}) + +InvoiceSchema.index({ 'created_at': 1 }, { + background: true, +}) + +InvoiceSchema.index({ 'confirmed_at': 1 }, { + background: true, + sparse: true, +}) + +export const InvoicesModelName = 'Invoices' +export const InvoicesCollectionName = 'invoices' + +export const InvoicesModel = (dbClient: mongoose.Connection) => + dbClient.model( + 'Invoices', + InvoiceSchema, + 'invoices', + ) + +export const masterInvoicesModel = InvoicesModel(getMasterDbClient()) +export const readReplicaInvoicesModel = InvoicesModel(getReadReplicaDbClient()) diff --git a/src/database/models/Users.ts b/src/database/models/Users.ts index 861615a1..f630c235 100644 --- a/src/database/models/Users.ts +++ b/src/database/models/Users.ts @@ -1,28 +1,33 @@ import mongoose from 'mongoose' -import { getMasterDbClient, getReadReplicaDbClient } from '../client.ts' -import { Buffer } from 'Buffer' - -export interface UserInput { - pubkey: Buffer - is_admitted: boolean - balance: bigint -} - -export interface UserDocument extends UserInput, mongoose.Document { - created_at: Date - updated_at: Date -} +import { getMasterDbClient, getReadReplicaDbClient } from '@/database/client.ts' +import { DBUser } from '@/@types/user.ts' const UserSchema = new mongoose.Schema({ pubkey: { - type: mongoose.Schema.Types.Buffer, - require: true, + type: String, }, is_admitted: { - type: mongoose.Schema.Types.Buffer, + type: Boolean, + default: false, + }, + tos_accepted_at: { + type: Date, + }, + balance: { + type: BigInt, + default: 0n, + }, + updated_at: { + type: Date, + default: new Date(), }, - balance: { type: Number }, + created_at: { + type: Date, + default: new Date(), + }, +}, { + _id: false, }) UserSchema.index({ 'pubkey': 1 }, { @@ -32,12 +37,18 @@ UserSchema.index({ 'pubkey': 1 }, { UserSchema.index({ 'balance': 1 }, { background: true, }) +UserSchema.index({ 'is_admitted': 1 }, { + background: true, +}) +UserSchema.index({ 'created_at': 1 }, { + background: true, +}) export const UsersModelName = 'Users' export const UsersCollectionName = 'users' export const UsersModel = (dbClient: mongoose.Connection) => - dbClient.model( + dbClient.model( UsersModelName, UserSchema, UsersCollectionName, diff --git a/src/database/transaction.ts b/src/database/transaction.ts index a48a7593..71a62ded 100644 --- a/src/database/transaction.ts +++ b/src/database/transaction.ts @@ -1,39 +1,65 @@ -import { Knex } from 'npm:knex@2.4.2' +import { ClientSessionOptions } from 'mongodb' -import { DatabaseClient, DatabaseTransaction } from '../@types/base.ts' -import { ITransaction } from '../@types/database.ts' +import { DatabaseClient, DatabaseTransaction } from '@/@types/base.ts' +import { ITransaction } from '@/@types/database.ts' export class Transaction implements ITransaction { - private trx: Knex.Transaction + private session!: DatabaseTransaction public constructor( private readonly dbClient: DatabaseClient, ) {} - public async begin(): Promise { - this.trx = await this.dbClient.transaction(null, { - isolationLevel: 'serializable', - }) + public async begin(options?: ClientSessionOptions): Promise { + try { + this.session = await this.dbClient.startSession({ + causalConsistency: true, + defaultTransactionOptions: { + writeConcern: { w: 'majority' }, + readConcern: { level: 'local' }, + readPreference: 'primary', + }, + ...options, + }) + this.session.startTransaction() + } catch (err) { + await this.rollback() + throw err + } } public get transaction(): DatabaseTransaction { - if (!this.trx) { + if (!this.session) { throw new Error('Unable to get transaction: transaction not started.') } - return this.trx + return this.session } - public async commit(): Promise { - if (!this.trx) { + public async commit(): Promise { + if (!this.session) { throw new Error('Unable to get transaction: transaction not started.') } - return this.trx.commit() + + try { + await this.session.commitTransaction() + await this.session.endSession() + } catch (err) { + await this.rollback() + throw err + } } - public async rollback(): Promise { - if (!this.trx) { + public async rollback(): Promise { + if (!this.session) { throw new Error('Unable to get transaction: transaction not started.') } - return this.trx.rollback() + + try { + await this.session.abortTransaction() + await this.session.endSession() + } catch (err) { + console.error('Error rolling back transaction:', err.message) + throw err + } } } diff --git a/src/factories/controllers/get-invoice-controller-factory.ts b/src/factories/controllers/get-invoice-controller-factory.ts new file mode 100644 index 00000000..f761a632 --- /dev/null +++ b/src/factories/controllers/get-invoice-controller-factory.ts @@ -0,0 +1,3 @@ +import { GetInvoiceController } from '@/controllers/invoices/get-invoice-controller.ts' + +export const createGetInvoiceController = () => new GetInvoiceController() diff --git a/src/factories/controllers/get-invoice-status-controller-factory.ts b/src/factories/controllers/get-invoice-status-controller-factory.ts new file mode 100644 index 00000000..f7f711c7 --- /dev/null +++ b/src/factories/controllers/get-invoice-status-controller-factory.ts @@ -0,0 +1,11 @@ +import { GetInvoiceStatusController } from '@/controllers/invoices/get-invoice-status-controller.ts' +import { getReadReplicaDbClient } from '@/database/client.ts' +import { InvoiceRepository } from '@/repositories/invoice-repository.ts' + +export const createGetInvoiceStatusController = () => { + const rrDbClient = getReadReplicaDbClient() + + const invoiceRepository = new InvoiceRepository(rrDbClient) + + return new GetInvoiceStatusController(invoiceRepository) +} diff --git a/src/factories/controllers/lnbits-callback-controller-factory.ts b/src/factories/controllers/lnbits-callback-controller-factory.ts new file mode 100644 index 00000000..468e6bc5 --- /dev/null +++ b/src/factories/controllers/lnbits-callback-controller-factory.ts @@ -0,0 +1,12 @@ +import { createPaymentsService } from '@/factories/payments-service-factory.ts' +import { getMasterDbClient } from '@/database/client.ts' +import { IController } from '@/@types/controllers.ts' +import { InvoiceRepository } from '@/repositories/invoice-repository.ts' +import { LNbitsCallbackController } from '@/controllers/callbacks/lnbits-callback-controller.ts' + +export const createLNbitsCallbackController = (): IController => { + return new LNbitsCallbackController( + createPaymentsService(), + new InvoiceRepository(getMasterDbClient()), + ) +} diff --git a/src/factories/controllers/nodeless-callback-controller-factory.ts b/src/factories/controllers/nodeless-callback-controller-factory.ts new file mode 100644 index 00000000..7d84840d --- /dev/null +++ b/src/factories/controllers/nodeless-callback-controller-factory.ts @@ -0,0 +1,8 @@ +import { createPaymentsService } from '@/factories/payments-service-factory.ts' +import { IController } from '@/@types/controllers.ts' +import { NodelessCallbackController } from '@/controllers/callbacks/nodeless-callback-controller.ts' + +export const createNodelessCallbackController = (): IController => + new NodelessCallbackController( + createPaymentsService(), + ) diff --git a/src/factories/controllers/opennode-callback-controller-factory.ts b/src/factories/controllers/opennode-callback-controller-factory.ts new file mode 100644 index 00000000..0351f9ca --- /dev/null +++ b/src/factories/controllers/opennode-callback-controller-factory.ts @@ -0,0 +1,9 @@ +import { createPaymentsService } from '@/factories/payments-service-factory.ts' +import { IController } from '@/@types/controllers.ts' +import { OpenNodeCallbackController } from '@/controllers/callbacks/opennode-callback-controller.ts' + +export const createOpenNodeCallbackController = (): IController => { + return new OpenNodeCallbackController( + createPaymentsService(), + ) +} diff --git a/src/factories/controllers/post-invoice-controller-factory.ts b/src/factories/controllers/post-invoice-controller-factory.ts new file mode 100644 index 00000000..6a5ed866 --- /dev/null +++ b/src/factories/controllers/post-invoice-controller-factory.ts @@ -0,0 +1,18 @@ +import { createPaymentsService } from '@/factories/payments-service-factory.ts' +import { createSettings } from '@/factories/settings-factory.ts' +import { IController } from '@/@types/controllers.ts' +import { PostInvoiceController } from '@/controllers/invoices/post-invoice-controller.ts' +import { slidingWindowRateLimiterFactory } from '@/factories/rate-limiter-factory.ts' +import { UserRepository } from '@/repositories/user-repository.ts' + +export const createPostInvoiceController = (): IController => { + const userRepository = new UserRepository(createSettings) + const paymentsService = createPaymentsService() + + return new PostInvoiceController( + userRepository, + paymentsService, + createSettings, + slidingWindowRateLimiterFactory, + ) +} diff --git a/src/factories/controllers/zebedee-callback-controller-factory.ts b/src/factories/controllers/zebedee-callback-controller-factory.ts new file mode 100644 index 00000000..7ca6aa5d --- /dev/null +++ b/src/factories/controllers/zebedee-callback-controller-factory.ts @@ -0,0 +1,9 @@ +import { createPaymentsService } from '@/factories/payments-service-factory.ts' +import { IController } from '@/@types/controllers.ts' +import { ZebedeeCallbackController } from '@/controllers/callbacks/zebedee-callback-controller.ts' + +export const createZebedeeCallbackController = (): IController => { + return new ZebedeeCallbackController( + createPaymentsService(), + ) +} diff --git a/src/factories/core-services-factory.ts b/src/factories/core-services-factory.ts index 128b562f..0ca14f02 100644 --- a/src/factories/core-services-factory.ts +++ b/src/factories/core-services-factory.ts @@ -1,10 +1,7 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { api } from '../core-services/index.ts' -import { getMasterDbClient, getReadReplicaDbClient } from '../database/client.ts' -import { DatabaseWatcher } from '../database/DatabaseWatcher.ts' -import { initWatchers } from '../database/watchers.ts' - -// export let watcher: DatabaseWatcher +import { api } from '@/core-services/index.ts' +import { getMasterDbClient, getReadReplicaDbClient } from '@/database/client.ts' +import { DatabaseWatcher } from '@/database/DatabaseWatcher.ts' +import { initWatchers } from '@/database/watchers.ts' export const coreServicesFactory = async () => { const primaryConn = getMasterDbClient() diff --git a/src/factories/get-invoice-status-controller-factory.ts b/src/factories/get-invoice-status-controller-factory.ts deleted file mode 100644 index 07f7db5e..00000000 --- a/src/factories/get-invoice-status-controller-factory.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GetInvoiceStatusController } from '../controllers/invoices/get-invoice-status-controller.ts' -import { getReadReplicaDbClient } from '../database/client.ts' -import { InvoiceRepository } from '../repositories/invoice-repository.ts' - -export const createGetInvoiceStatusController = () => { - const rrDbClient = getReadReplicaDbClient() - - const invoiceRepository = new InvoiceRepository(rrDbClient) - - return new GetInvoiceStatusController(invoiceRepository) -} diff --git a/src/factories/lnbits-callback-controller-factory.ts b/src/factories/lnbits-callback-controller-factory.ts deleted file mode 100644 index 0bbae5cb..00000000 --- a/src/factories/lnbits-callback-controller-factory.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IController } from '../@types/controllers.ts' -import { LNbitsCallbackController } from '../controllers/callbacks/lnbits-callback-controller.ts' -import { getMasterDbClient } from '../database/client.ts' -import { InvoiceRepository } from '../repositories/invoice-repository.ts' -import { createPaymentsService } from './payments-service-factory.ts' - -export const createLNbitsCallbackController = (): IController => { - return new LNbitsCallbackController( - createPaymentsService(), - new InvoiceRepository(getMasterDbClient()), - ) -} diff --git a/src/factories/payments-processor-factory.ts b/src/factories/payments-processor-factory.ts index 5ee4b5dc..adffd66b 100644 --- a/src/factories/payments-processor-factory.ts +++ b/src/factories/payments-processor-factory.ts @@ -1,139 +1,19 @@ -import axios, { CreateAxiosDefaults } from 'axios' -import { path } from 'ramda' - -import { IPaymentsProcessor } from '../@types/clients.ts' -import { Settings } from '../@types/settings.ts' -import Config from '../config/index.ts' -import { LNbitsPaymentsProcesor } from '../payments-processors/lnbits-payment-processor.ts' -import { LnurlPaymentsProcesor } from '../payments-processors/lnurl-payments-processor.ts' -import { NullPaymentsProcessor } from '../payments-processors/null-payments-processor.ts' -import { PaymentsProcessor } from '../payments-processors/payments-procesor.ts' -import { ZebedeePaymentsProcesor } from '../payments-processors/zebedee-payments-processor.ts' -import { createLogger } from './logger-factory.ts' -import { createSettings } from './settings-factory.ts' +import { IPaymentsProcessor } from '@/@types/clients.ts' +import { NullPaymentsProcessor } from '@/payments-processors/null-payments-processor.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { createSettings } from '@/factories/settings-factory.ts' +import { createLNbitsPaymentProcessor } from '@/factories/payments-processors/lnbits-payments-processor-factory.ts' +import { createLnurlPaymentsProcessor } from '@/factories/payments-processors/lnurl-payments-processor-factory.ts' +import { createNodelessPaymentsProcessor } from '@/factories/payments-processors/nodeless-payments-processor-factory.ts' +import { createOpenNodePaymentsProcessor } from '@/factories/payments-processors/opennode-payments-processor-factory.ts' +import { createZebedeePaymentsProcessor } from '@/factories/payments-processors/zebedee-payments-processor-factory.ts' const debug = createLogger('create-payments-processor') -const getZebedeeAxiosConfig = ( - settings: Settings, -): CreateAxiosDefaults => { - if (!Config.ZEBEDEE_API_KEY) { - const error = new Error('ZEBEDEE_API_KEY must be set.') - console.error('Unable to get Zebedee config.', error) - throw error - } - - return { - headers: { - 'content-type': 'application/json', - 'apikey': Config.ZEBEDEE_API_KEY, - }, - baseURL: path(['paymentsProcessors', 'zebedee', 'baseURL'], settings), - maxRedirects: 1, - } -} - -const getLNbitsAxiosConfig = (settings: Settings): CreateAxiosDefaults => { - if (!Config.LNBITS_API_KEY) { - throw new Error('LNBITS_API_KEY must be set to an invoice or admin key.') - } - - return { - headers: { - 'content-type': 'application/json', - 'x-api-key': Config.LNBITS_API_KEY, - }, - baseURL: path(['paymentsProcessors', 'lnbits', 'baseURL'], settings), - maxRedirects: 1, - } -} - -const createLnurlPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { - const invoiceURL = path(['paymentsProcessors', 'lnurl', 'invoiceURL'], settings) as string | undefined - if (typeof invoiceURL === 'undefined') { - throw new Error('Unable to create payments processor: Setting paymentsProcessor.lnurl.invoiceURL is not configured.') - } - - const client = axios.create() - - const app = new LnurlPaymentsProcesor(client, createSettings) - - return new PaymentsProcessor(app) -} - -const createZebedeePaymentsProcessor = ( - settings: Settings, -): IPaymentsProcessor => { - const callbackBaseURL = path([ - 'paymentsProcessors', - 'zebedee', - 'callbackBaseURL', - ], settings) as string | undefined - if ( - typeof callbackBaseURL === 'undefined' || - callbackBaseURL.indexOf('denostr.your-domain.com') >= 0 - ) { - const error = new Error( - 'Setting paymentsProcessor.zebedee.callbackBaseURL is not configured.', - ) - console.error('Unable to create payments processor.', error) - - throw error - } - - if ( - !Array.isArray(settings.paymentsProcessors?.zebedee?.ipWhitelist) || - !settings.paymentsProcessors?.zebedee?.ipWhitelist?.length - ) { - const error = new Error( - 'Setting paymentsProcessor.zebedee.ipWhitelist is empty.', - ) - console.error('Unable to create payments processor.', error) - - throw error - } - - const config = getZebedeeAxiosConfig(settings) - debug('config: %o', config) - const client = axios.create(config) - - const zpp = new ZebedeePaymentsProcesor(client, createSettings) - - return new PaymentsProcessor(zpp) -} - -const createLNbitsPaymentProcessor = ( - settings: Settings, -): IPaymentsProcessor => { - const callbackBaseURL = path([ - 'paymentsProcessors', - 'lnbits', - 'callbackBaseURL', - ], settings) as string | undefined - if ( - typeof callbackBaseURL === 'undefined' || - callbackBaseURL.indexOf('denostr.your-domain.com') >= 0 - ) { - const error = new Error( - 'Setting paymentsProcessor.lnbits.callbackBaseURL is not configured.', - ) - console.error('Unable to create payments processor.', error) - - throw error - } - - const config = getLNbitsAxiosConfig(settings) - debug('config: %o', config) - const client = axios.create(config) - - const pp = new LNbitsPaymentsProcesor(client, createSettings) - - return new PaymentsProcessor(pp) -} - export const createPaymentsProcessor = (): IPaymentsProcessor => { debug('create payments processor') const settings = createSettings() + debug('payments = %o', settings.payments?.processor, settings.payments?.enabled) if (!settings.payments?.enabled) { return new NullPaymentsProcessor() } @@ -145,6 +25,10 @@ export const createPaymentsProcessor = (): IPaymentsProcessor => { return createZebedeePaymentsProcessor(settings) case 'lnbits': return createLNbitsPaymentProcessor(settings) + case 'nodeless': + return createNodelessPaymentsProcessor(settings) + case 'opennode': + return createOpenNodePaymentsProcessor(settings) default: return new NullPaymentsProcessor() } diff --git a/src/factories/payments-processors/lnbits-payments-processor-factory.ts b/src/factories/payments-processors/lnbits-payments-processor-factory.ts new file mode 100644 index 00000000..09621bca --- /dev/null +++ b/src/factories/payments-processors/lnbits-payments-processor-factory.ts @@ -0,0 +1,40 @@ +import { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '@/factories/settings-factory.ts' +import { IPaymentsProcessor } from '@/@types/clients.ts' +import { LNbitsPaymentsProcesor } from '@/payments-processors/lnbits-payment-processor.ts' +import { Settings } from '@/@types/settings.ts' +import Config from '@/config/index.ts' +import { HTTPClient } from '@/utils/http.ts' + +const getLNbitsAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + if (!Config.LNBITS_API_KEY) { + throw new Error('LNBITS_API_KEY must be set to an invoice or admin key.') + } + + return { + headers: { + 'content-type': 'application/json', + 'x-api-key': Config.LNBITS_API_KEY, + }, + baseURL: path(['paymentsProcessors', 'lnbits', 'baseURL'], settings), + maxRedirects: 1, + } +} + +export const createLNbitsPaymentProcessor = (settings: Settings): IPaymentsProcessor => { + const callbackBaseURL = path(['paymentsProcessors', 'lnbits', 'callbackBaseURL'], settings) as string | undefined + if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('relay.your-domain.com') >= 0) { + const error = new Error('Setting paymentsProcessor.lnbits.callbackBaseURL is not configured.') + console.error('Unable to create payments processor.', error) + + throw error + } + + const config = getLNbitsAxiosConfig(settings) + + const client = new HTTPClient(config) + + return new LNbitsPaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/lnurl-payments-processor-factory.ts b/src/factories/payments-processors/lnurl-payments-processor-factory.ts new file mode 100644 index 00000000..844deec8 --- /dev/null +++ b/src/factories/payments-processors/lnurl-payments-processor-factory.ts @@ -0,0 +1,18 @@ +import { path } from 'ramda' + +import { createSettings } from '@/factories/settings-factory.ts' +import { IPaymentsProcessor } from '@/@types/clients.ts' +import { LnurlPaymentsProcesor } from '@/payments-processors/lnurl-payments-processor.ts' +import { Settings } from '@/@types/settings.ts' +import { HTTPClient } from '@/utils/http.ts' + +export const createLnurlPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const invoiceURL = path(['paymentsProcessors', 'lnurl', 'invoiceURL'], settings) as string | undefined + if (typeof invoiceURL === 'undefined') { + throw new Error('Unable to create payments processor: Setting paymentsProcessor.lnurl.invoiceURL is not configured.') + } + + const client = new HTTPClient() + + return new LnurlPaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/nodeless-payments-processor-factory.ts b/src/factories/payments-processors/nodeless-payments-processor-factory.ts new file mode 100644 index 00000000..634d4310 --- /dev/null +++ b/src/factories/payments-processors/nodeless-payments-processor-factory.ts @@ -0,0 +1,33 @@ +import { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '@/factories/settings-factory.ts' +import { IPaymentsProcessor } from '@/@types/clients.ts' +import { NodelessPaymentsProcesor } from '@/payments-processors/nodeless-payments-processor.ts' +import { Settings } from '@/@types/settings.ts' +import Config from '@/config/index.ts' +import { HTTPClient } from '@/utils/http.ts' + +const getNodelessAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + if (!Config.NODELESS_API_KEY) { + const error = new Error('NODELESS_API_KEY must be set.') + console.error('Unable to get Nodeless config.', error) + throw error + } + + return { + headers: { + 'content-type': 'application/json', + 'authorization': `Bearer ${Config.NODELESS_API_KEY}`, + 'accept': 'application/json', + }, + baseURL: path(['paymentsProcessors', 'nodeless', 'baseURL'], settings), + maxRedirects: 1, + } +} + +export const createNodelessPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const client = new HTTPClient(getNodelessAxiosConfig(settings)) + + return new NodelessPaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/opennode-payments-processor-factory.ts b/src/factories/payments-processors/opennode-payments-processor-factory.ts new file mode 100644 index 00000000..c4dc7dd3 --- /dev/null +++ b/src/factories/payments-processors/opennode-payments-processor-factory.ts @@ -0,0 +1,41 @@ +import { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '@/factories/settings-factory.ts' +import { IPaymentsProcessor } from '@/@types/clients.ts' +import { OpenNodePaymentsProcesor } from '@/payments-processors/opennode-payments-processor.ts' +import { Settings } from '@/@types/settings.ts' +import Config from '@/config/index.ts' +import { HTTPClient } from '@/utils/http.ts' + +const getOpenNodeAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + if (!Config.OPENNODE_API_KEY) { + const error = new Error('OPENNODE_API_KEY must be set.') + console.error('Unable to get OpenNode config.', error) + throw error + } + + return { + headers: { + 'content-type': 'application/json', + 'authorization': Config.OPENNODE_API_KEY, + }, + baseURL: path(['paymentsProcessors', 'opennode', 'baseURL'], settings), + maxRedirects: 1, + } +} + +export const createOpenNodePaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const callbackBaseURL = path(['paymentsProcessors', 'opennode', 'callbackBaseURL'], settings) as string | undefined + if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) { + const error = new Error('Setting paymentsProcessor.opennode.callbackBaseURL is not configured.') + console.error('Unable to create payments processor.', error) + + throw error + } + + const config = getOpenNodeAxiosConfig(settings) + const client = new HTTPClient(config) + + return new OpenNodePaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/zebedee-payments-processor-factory.ts b/src/factories/payments-processors/zebedee-payments-processor-factory.ts new file mode 100644 index 00000000..f4a36b99 --- /dev/null +++ b/src/factories/payments-processors/zebedee-payments-processor-factory.ts @@ -0,0 +1,52 @@ +import { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '@/factories/settings-factory.ts' +import { IPaymentsProcessor } from '@/@types/clients.ts' +import { Settings } from '@/@types/settings.ts' +import { ZebedeePaymentsProcesor } from '@/payments-processors/zebedee-payments-processor.ts' +import Config from '@/config/index.ts' +import { HTTPClient } from '@/utils/http.ts' + +const getZebedeeAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + if (!Config.ZEBEDEE_API_KEY) { + const error = new Error('ZEBEDEE_API_KEY must be set.') + console.error('Unable to get Zebedee config.', error) + throw error + } + + return { + headers: { + 'content-type': 'application/json', + 'apikey': Config.ZEBEDEE_API_KEY, + }, + baseURL: path(['paymentsProcessors', 'zebedee', 'baseURL'], settings), + maxRedirects: 1, + } +} + +export const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const callbackBaseURL = path(['paymentsProcessors', 'zebedee', 'callbackBaseURL'], settings) as string | undefined + if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) { + const error = new Error('Setting paymentsProcessor.zebedee.callbackBaseURL is not configured.') + console.error('Unable to create payments processor.', error) + + throw error + } + + if ( + !Array.isArray(settings.paymentsProcessors?.zebedee?.ipWhitelist) || + !settings.paymentsProcessors?.zebedee?.ipWhitelist?.length + ) { + const error = new Error('Setting paymentsProcessor.zebedee.ipWhitelist is empty.') + console.error('Unable to create payments processor.', error) + + throw error + } + + const config = getZebedeeAxiosConfig(settings) + + const client = new HTTPClient(config) + + return new ZebedeePaymentsProcesor(client, createSettings) +} diff --git a/src/factories/post-invoice-controller-factory.ts b/src/factories/post-invoice-controller-factory.ts deleted file mode 100644 index 257ada39..00000000 --- a/src/factories/post-invoice-controller-factory.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IController } from '../@types/controllers.ts' -import { PostInvoiceController } from '../controllers/invoices/post-invoice-controller.ts' -import { UserRepository } from '../repositories/user-repository.ts' -import { createPaymentsService } from './payments-service-factory.ts' -import { slidingWindowRateLimiterFactory } from './rate-limiter-factory.ts' -import { createSettings } from './settings-factory.ts' - -export const createPostInvoiceController = (): IController => { - const userRepository = new UserRepository(createSettings) - const paymentsService = createPaymentsService() - - return new PostInvoiceController( - userRepository, - paymentsService, - createSettings, - slidingWindowRateLimiterFactory, - ) -} diff --git a/src/factories/web-app-factory.ts b/src/factories/web-app-factory.ts index a58dc13e..d8129e60 100644 --- a/src/factories/web-app-factory.ts +++ b/src/factories/web-app-factory.ts @@ -1,11 +1,9 @@ import { Application, etag } from 'oak' import csp from 'oak-csp' -import router from '../routes/index.ts' -import { createLogger } from './logger-factory.ts' -import { createSettings } from './settings-factory.ts' +import router from "@/routes/index.ts" +import { createSettings } from "@/factories/settings-factory.ts" const getDirectives = () => { - const debug = createLogger('web-app-factory') const settings = createSettings() const relayUrl = new URL(settings.info.relay_url) const webRelayUrl = new URL(relayUrl.toString()) @@ -32,7 +30,7 @@ const getDirectives = () => { 'style-src': ['\'self\'', 'https://cdn.jsdelivr.net/npm/'], 'font-src': ['\'self\'', 'https://cdn.jsdelivr.net/npm/'], } - debug('CSP directives: %o', directives) + return directives } diff --git a/src/factories/worker-factory.ts b/src/factories/worker-factory.ts index 5938bf84..73dc8887 100644 --- a/src/factories/worker-factory.ts +++ b/src/factories/worker-factory.ts @@ -1,11 +1,13 @@ import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter.ts' import { AppWorker } from '../app/worker.ts' -import { api, LocalBroker, WebSocketServerService } from '../core-services/index.ts' +import { api, LocalBroker, WebSocketServerService, PubSubService } from '../core-services/index.ts' import { createSettings } from '../factories/settings-factory.ts' import { EventRepository } from '../repositories/event-repository.ts' import { UserRepository } from '../repositories/user-repository.ts' import { createWebApp } from './web-app-factory.ts' import { webSocketAdapterFactory } from './websocket-adapter-factory.ts' +import { getCacheClient } from '../cache/client.ts' +import Config from '@/config/index.ts' export const workerFactory = (): AppWorker => { const eventRepository = new EventRepository(createSettings) @@ -27,6 +29,7 @@ export const workerFactory = (): AppWorker => { ) api.registerService(new WebSocketServerService(adapter)) + api.registerService(new PubSubService(getCacheClient)) const broker = new LocalBroker() broker.onBroadcast((eventName, ...args) => { // TODO diff --git a/src/factories/zebedee-callback-controller-factory.ts b/src/factories/zebedee-callback-controller-factory.ts deleted file mode 100644 index fdc61824..00000000 --- a/src/factories/zebedee-callback-controller-factory.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IController } from '../@types/controllers.ts' -import { ZebedeeCallbackController } from '../controllers/callbacks/zebedee-callback-controller.ts' -import { createPaymentsService } from './payments-service-factory.ts' - -export const createZebedeeCallbackController = (): IController => { - return new ZebedeeCallbackController( - createPaymentsService(), - ) -} diff --git a/src/handlers/event-strategies/ephemeral-event-strategy.ts b/src/handlers/event-strategies/ephemeral-event-strategy.ts index 2f2910b8..4b50a9b4 100644 --- a/src/handlers/event-strategies/ephemeral-event-strategy.ts +++ b/src/handlers/event-strategies/ephemeral-event-strategy.ts @@ -4,6 +4,8 @@ import { IEventStrategy } from '../../@types/message-handlers.ts' import { WebSocketAdapterEvent } from '../../constants/adapter.ts' import { createLogger } from '../../factories/logger-factory.ts' import { createCommandResult } from '../../utils/messages.ts' +import { getCacheClient, publish } from '@/cache/client.ts' +import { PubSubBroadcastEvent } from '@/constants/adapter.ts' const debug = createLogger('ephemeral-event-strategy') @@ -18,6 +20,16 @@ export class EphemeralEventStrategy implements IEventStrategy { + ctx.state.body = {} + const request: Request = ctx.request + const result = request.body() + switch (result.type) { + case 'text': + { + try { + ctx.state.body = JSON.parse(await result.value) + } catch { + ctx.response.status = Status.BadRequest + ctx.response.body = 'Invalid JSON string' + return + } + } + break + case 'json': + { + try { + ctx.state.body = await result.value + } catch { + ctx.response.status = Status.BadRequest + ctx.response.body = 'Invalid JSON string' + return + } + } + break + case 'form': + { + for (const [key, value] of (await result.value).entries()) { + ctx.state.body[key] = value + } + } + break + } + + debug('request received from %s parse body: %O', result.type, ctx.state.body) + + await next() +} diff --git a/src/handlers/request-handlers/get-invoice-request-handler.ts b/src/handlers/request-handlers/get-invoice-request-handler.ts deleted file mode 100644 index 741f7c00..00000000 --- a/src/handlers/request-handlers/get-invoice-request-handler.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { readFileSync } from 'fs' - -import { path, pathEq } from 'ramda' - -import { NextFunction, Response, RouterContext, Status } from '../../@types/controllers.ts' -import { FeeSchedule } from '../../@types/settings.ts' -import { createSettings } from '../../factories/settings-factory.ts' - -let pageCache: string - -export const getInvoiceRequestHandler = async ( - ctx: RouterContext, - next: NextFunction, -) => { - const res: Response = ctx.response - const settings = createSettings() - if ( - pathEq(['payments', 'enabled'], true, settings) && - pathEq( - ['payments', 'feeSchedules', 'admission', '0', 'enabled'], - true, - settings, - ) - ) { - if (!pageCache) { - const name = path(['info', 'name'])(settings) - const feeSchedule = path([ - 'payments', - 'feeSchedules', - 'admission', - '0', - ], settings) - pageCache = readFileSync('./resources/index.html', 'utf8') - .replaceAll('{{name}}', name) - .replaceAll( - '{{amount}}', - (BigInt(feeSchedule.amount) / 1000n).toString(), - ) - } - res.status = Status.OK - res.headers.set('content-type', 'text/html; charset=utf8') - res.body = pageCache - } else { - res.status = Status.NotFound - res.headers.set('content-type', 'text/html; charset=utf8') - res.body = 'not Found' - } - - await next() -} diff --git a/src/handlers/request-handlers/get-invoice-status-request-handler.ts b/src/handlers/request-handlers/get-invoice-status-request-handler.ts deleted file mode 100644 index 8406978f..00000000 --- a/src/handlers/request-handlers/get-invoice-status-request-handler.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextFunction, Request, Response, RouterContext } from '../../@types/controllers.ts' -import { createGetInvoiceStatusController } from '../../factories/get-invoice-status-controller-factory.ts' - -export const getInvoiceStatusRequestHandler = async ( - ctx: RouterContext, - next: NextFunction, -) => { - const req: Request = ctx.request - const res: Response = ctx.response - const controller = createGetInvoiceStatusController() - await controller.handleRequest(req, res, ctx) - await next() -} diff --git a/src/handlers/request-handlers/post-invoice-request-handler.ts b/src/handlers/request-handlers/post-invoice-request-handler.ts deleted file mode 100644 index ab7917ff..00000000 --- a/src/handlers/request-handlers/post-invoice-request-handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextFunction, Request, Response, RouterContext, Status } from '../../@types/controllers.ts' -import { createPostInvoiceController } from '../../factories/post-invoice-controller-factory.ts' - -export const postInvoiceRequestHandler = async ( - ctx: RouterContext, - next: NextFunction, -) => { - const req: Request = ctx.request - const res: Response = ctx.response - const controller = createPostInvoiceController() - - try { - await controller.handleRequest(req, res, ctx) - await next() - } catch (error) { - console.error('Unable handle request.', error) - ctx.throw(Status.InternalServerError, 'Error handling request') - } -} diff --git a/src/handlers/request-handlers/post-lnbits-callback-request-handler.ts b/src/handlers/request-handlers/post-lnbits-callback-request-handler.ts deleted file mode 100644 index 156b698a..00000000 --- a/src/handlers/request-handlers/post-lnbits-callback-request-handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Request, Response, RouterContext, Status } from '../../@types/controllers.ts' -import { createLNbitsCallbackController } from '../../factories/lnbits-callback-controller-factory.ts' - -export const postLNbitsCallbackRequestHandler = async ( - req: Request, - res: Response, - ctx: RouterContext, -) => { - const controller = createLNbitsCallbackController() - - try { - await controller.handleRequest(req, res, ctx) - } catch (error) { - console.error('error while handling LNbits request: %o', error) - res.status = Status.InternalServerError - res.headers.set('content-type', 'text/plain') - res.body = 'Error handling request' - } -} diff --git a/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts b/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts deleted file mode 100644 index 1bc0f80e..00000000 --- a/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Request, Response, Status } from '../../@types/controllers.ts' -import { createZebedeeCallbackController } from '../../factories/zebedee-callback-controller-factory.ts' - -export const postZebedeeCallbackRequestHandler = async ( - req: Request, - res: Response, -) => { - const controller = createZebedeeCallbackController() - - try { - await controller.handleRequest(req, res) - } catch (_) { - res.status = Status.InternalServerError - res.headers.set('content-type', 'text-plain') - res.body = 'Error handling request' - } -} diff --git a/src/handlers/request-handlers/rate-limiter-middleware.ts b/src/handlers/request-handlers/rate-limiter-middleware.ts index f7c013d8..0aa4e763 100644 --- a/src/handlers/request-handlers/rate-limiter-middleware.ts +++ b/src/handlers/request-handlers/rate-limiter-middleware.ts @@ -1,9 +1,9 @@ -import { NextFunction, Request, Response, RouterContext } from '../../@types/controllers.ts' -import { Settings } from '../../@types/settings.ts' -import { createLogger } from '../../factories/logger-factory.ts' -import { slidingWindowRateLimiterFactory } from '../../factories/rate-limiter-factory.ts' -import { createSettings } from '../../factories/settings-factory.ts' -import { getRemoteAddress } from '../../utils/http.ts' +import { NextFunction, Request, Response, RouterContext } from '@/@types/controllers.ts' +import { Settings } from '@/@types/settings.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { slidingWindowRateLimiterFactory } from '@/factories/rate-limiter-factory.ts' +import { createSettings } from '@/factories/settings-factory.ts' +import { getRemoteAddress } from '@/utils/http.ts' const debug = createLogger('rate-limiter-middleware') @@ -15,7 +15,7 @@ export const rateLimiterMiddleware = async ( const request: Request = ctx.request const response: Response = ctx.response const clientAddress = getRemoteAddress(request.originalRequest, currentSettings).split(',')[0] - debug('request received from %s: %O', clientAddress, request.headers) + debug('request received from %s: %o', clientAddress, request.headers) if (await isRateLimited(clientAddress, currentSettings)) { response.destroy() diff --git a/src/handlers/request-handlers/root-request-handler.ts b/src/handlers/request-handlers/root-request-handler.ts index 6e9b929b..2b7d9bfa 100644 --- a/src/handlers/request-handlers/root-request-handler.ts +++ b/src/handlers/request-handlers/root-request-handler.ts @@ -87,6 +87,6 @@ export const rootRequestHandler = async ( response.status = Status.OK response.headers.set('content-type', 'text/plain; charset=utf8') response.body = 'Please use a Nostr client to connect.' - await next() } + await next() } diff --git a/src/handlers/request-handlers/with-controller-request-handler.ts b/src/handlers/request-handlers/with-controller-request-handler.ts new file mode 100644 index 00000000..16afa073 --- /dev/null +++ b/src/handlers/request-handlers/with-controller-request-handler.ts @@ -0,0 +1,21 @@ +import { Factory } from '@/@types/base.ts' +import { IController, NextFunction, RouterContext, Status } from '@/@types/controllers.ts' +import { createLogger } from '@/factories/logger-factory.ts' + +const debug = createLogger('with-controller') + +export const withController = (controllerFactory: Factory) => +async ( + ctx: RouterContext, + next: NextFunction, +) => { + const response = ctx.response + try { + await next() + return await controllerFactory().handleRequest(ctx.request, response, ctx) + } catch (err) { + debug('handleRequest() Error: %o', err) + response.status = Status.InternalServerError + response.body = 'Error handling request' + } +} diff --git a/src/handlers/subscribe-message-handler.ts b/src/handlers/subscribe-message-handler.ts index ebe35e8b..e87054b6 100644 --- a/src/handlers/subscribe-message-handler.ts +++ b/src/handlers/subscribe-message-handler.ts @@ -82,7 +82,7 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable { const isSubscribedToEvent = SubscribeMessageHandler .isClientSubscribedToEvent(filters) - const findEvents = this.eventRepository.findByFilters(filters).cursor() + const findEvents = this.eventRepository.findByFilters(filters).cursor // const abortableFindEvents = addAbortSignal(this.abortController.signal, findEvents) @@ -98,7 +98,6 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable { } catch (error) { if (error instanceof Error && error.name === 'AbortError') { debug('subscription %s aborted: %o', subscriptionId, error) - findEvents.destroy() } else { debug('error streaming events: %o', error) } diff --git a/src/payments-processors/lnbits-payment-processor.ts b/src/payments-processors/lnbits-payment-processor.ts index 36f3b88d..14b0ea3f 100644 --- a/src/payments-processors/lnbits-payment-processor.ts +++ b/src/payments-processors/lnbits-payment-processor.ts @@ -1,5 +1,3 @@ -import { AxiosInstance } from 'axios' - import { Factory } from '../@types/base.ts' import { Pubkey } from '../@types/base.ts' import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients.ts' @@ -7,6 +5,7 @@ import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice.ts' import { Settings } from '../@types/settings.ts' import { createLogger } from '../factories/logger-factory.ts' import { deriveFromSecret, hmacSha256 } from '../utils/secret.ts' +import { HTTPClient } from '@/utils/http.ts' const debug = createLogger('lnbits-payments-processor') @@ -41,16 +40,14 @@ export class LNbitsCreateInvoiceResponse implements CreateInvoiceResponse { export class LNbitsPaymentsProcesor implements IPaymentsProcessor { public constructor( - private httpClient: AxiosInstance, + private httpClient: HTTPClient, private settings: Factory, ) {} public async getInvoice(invoiceId: string): Promise { debug('get invoice: %s', invoiceId) try { - const response = await this.httpClient.get(`/api/v1/payments/${invoiceId}`, { - maxRedirects: 1, - }) + const response = await this.httpClient.get(`/api/v1/payments/${invoiceId}`) const invoice = new LNbitsInvoice() const data = response.data invoice.id = data.details.payment_hash @@ -65,6 +62,7 @@ export class LNbitsPaymentsProcesor implements IPaymentsProcessor { invoice.expiresAt = new Date(data.details.expiry * 1000) invoice.createdAt = new Date(data.details.time * 1000) invoice.updatedAt = new Date() + return invoice } catch (error) { console.error(`Unable to get invoice ${invoiceId}. Reason:`, error) @@ -105,15 +103,10 @@ export class LNbitsPaymentsProcesor implements IPaymentsProcessor { try { debug('request body: %o', body) - const response = await this.httpClient.post('/api/v1/payments', body, { - maxRedirects: 1, - }) - + const response = await this.httpClient.post('/api/v1/payments', body) debug('response: %o', response.data) - const invoiceResponse = await this.httpClient.get(`/api/v1/payments/${encodeURIComponent(response.data.payment_hash)}`, { - maxRedirects: 1, - }) + const invoiceResponse = await this.httpClient.get(`/api/v1/payments/${encodeURIComponent(response.data.payment_hash)}`) debug('invoice data response: %o', invoiceResponse.data) const invoice = new LNbitsCreateInvoiceResponse() diff --git a/src/payments-processors/lnurl-payments-processor.ts b/src/payments-processors/lnurl-payments-processor.ts index 257b480f..2b9fb8ed 100644 --- a/src/payments-processors/lnurl-payments-processor.ts +++ b/src/payments-processors/lnurl-payments-processor.ts @@ -1,21 +1,21 @@ -import { AxiosInstance } from 'axios' -import { Factory } from '../@types/base.ts' +import { Factory } from '@/@types/base.ts' -import { CreateInvoiceRequest, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients.ts' -import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice.ts' -import { createLogger } from '../factories/logger-factory.ts' +import { CreateInvoiceRequest, GetInvoiceResponse, IPaymentsProcessor } from '@/@types/clients.ts' +import { InvoiceStatus, InvoiceUnit, LnurlInvoice } from '@/@types/invoice.ts' +import { createLogger } from '@/factories/logger-factory.ts' import { randomUUID } from 'crypto' -import { Settings } from '../@types/settings.ts' +import { Settings } from '@/@types/settings.ts' +import { HTTPClient } from '@/utils/http.ts' const debug = createLogger('lnurl-payments-processor') export class LnurlPaymentsProcesor implements IPaymentsProcessor { public constructor( - private httpClient: AxiosInstance, + private httpClient: HTTPClient, private settings: Factory, ) {} - public async getInvoice(invoice: Invoice): Promise { + public async getInvoice(invoice: LnurlInvoice): Promise { debug('get invoice: %s', invoice.id) try { diff --git a/src/payments-processors/nodeless-payments-processor.ts b/src/payments-processors/nodeless-payments-processor.ts new file mode 100644 index 00000000..bece33d7 --- /dev/null +++ b/src/payments-processors/nodeless-payments-processor.ts @@ -0,0 +1,73 @@ +import { Factory } from '@/@types/base.ts' +import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '@/@types/clients.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { fromNodelessInvoice } from '@/utils/transform.ts' +import { Settings } from '@/@types/settings.ts' +import { HTTPClient } from '@/utils/http.ts' + +const debug = createLogger('nodeless-payments-processor') + +export class NodelessPaymentsProcesor implements IPaymentsProcessor { + public constructor( + private httpClient: HTTPClient, + private settings: Factory, + ) {} + + public async getInvoice(invoiceId: string): Promise { + debug('get invoice: %s', invoiceId) + + const { storeId } = this.settings().paymentsProcessors.nodeless + + try { + const response = await this.httpClient.get(`/api/v1/store/${storeId}/invoice/${invoiceId}`) + + return fromNodelessInvoice(response.data.data) + } catch (error) { + console.error(`Unable to get invoice ${invoiceId}. Reason:`, error) + + throw error + } + } + + public async createInvoice(request: CreateInvoiceRequest): Promise { + debug('create invoice: %O', request) + const { + amount: amountMsats, + description, + requestId, + } = request + + const amountSats = Number(amountMsats / 1000n) + + const body = { + amount: amountSats, + currency: 'SATS', + metadata: { + description, + requestId, + unit: 'sats', + createdAt: new Date().toISOString(), + }, + } + + const { storeId } = this.settings().paymentsProcessors.nodeless + + try { + debug('request body: %O', body) + const response = await this.httpClient.post(`/api/v1/store/${storeId}/invoice`, body) + + debug('response headers: %O', response.headers) + debug('response data: %O', response.data) + + const result = fromNodelessInvoice(response.data.data) + + debug('invoice: %O', result) + + return result + } catch (error) { + console.error('Unable to request invoice. Reason:', error.message) + + throw error + } + } +} diff --git a/src/payments-processors/opennode-payments-processor.ts b/src/payments-processors/opennode-payments-processor.ts new file mode 100644 index 00000000..601fcfb6 --- /dev/null +++ b/src/payments-processors/opennode-payments-processor.ts @@ -0,0 +1,63 @@ +import { Factory } from '@/@types/base.ts' +import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '@/@types/clients.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { fromOpenNodeInvoice } from '@/utils/transform.ts' +import { Settings } from '@/@types/settings.ts' +import { HTTPClient } from '@/utils/http.ts' + +const debug = createLogger('opennode-payments-processor') + +export class OpenNodePaymentsProcesor implements IPaymentsProcessor { + public constructor( + private httpClient: HTTPClient, + private settings: Factory, + ) {} + + public async getInvoice(invoiceId: string): Promise { + debug('get invoice: %s', invoiceId) + + try { + const response = await this.httpClient.get(`/v2/charge/${invoiceId}`) + + return fromOpenNodeInvoice(response.data.data) + } catch (error) { + console.error(`Unable to get invoice ${invoiceId}. Reason:`, error) + + throw error + } + } + + public async createInvoice(request: CreateInvoiceRequest): Promise { + debug('create invoice: %o', request) + const { + amount: amountMsats, + description, + requestId, + } = request + + const amountSats = Number(amountMsats / 1000n) + + const body = { + amount: amountSats, + description, + order_id: requestId, + callback_url: this.settings().paymentsProcessors?.opennode?.callbackBaseURL, + ttl: 10, + } + + try { + debug('request body: %o', body) + const response = await this.httpClient.post('/v1/charges', body) + + const result = fromOpenNodeInvoice(response.data.data) + + debug('result: %o', result) + + return result + } catch (error) { + console.error('Unable to request invoice. Reason:', error.message) + + throw error + } + } +} diff --git a/src/payments-processors/payments-procesor.ts b/src/payments-processors/payments-procesor.ts deleted file mode 100644 index 0c7a58a8..00000000 --- a/src/payments-processors/payments-procesor.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients.ts' -import { Invoice } from '../@types/invoice.ts' - -export class PaymentsProcessor implements IPaymentsProcessor { - public constructor( - private readonly processor: IPaymentsProcessor, - ) {} - - public async getInvoice(invoice: string | Invoice): Promise { - return this.processor.getInvoice(invoice) - } - - public async createInvoice( - request: CreateInvoiceRequest, - ): Promise { - return this.processor.createInvoice(request) - } -} diff --git a/src/payments-processors/zebedee-payments-processor.ts b/src/payments-processors/zebedee-payments-processor.ts index 8a0fe626..6bc18fbd 100644 --- a/src/payments-processors/zebedee-payments-processor.ts +++ b/src/payments-processors/zebedee-payments-processor.ts @@ -1,16 +1,15 @@ -import { AxiosInstance } from 'axios' - import { Factory } from '../@types/base.ts' import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients.ts' import { Settings } from '../@types/settings.ts' import { createLogger } from '../factories/logger-factory.ts' import { fromZebedeeInvoice } from '../utils/transform.ts' +import { HTTPClient } from '@/utils/http.ts' const debug = createLogger('zebedee-payments-processor') export class ZebedeePaymentsProcesor implements IPaymentsProcessor { public constructor( - private httpClient: AxiosInstance, + private httpClient: HTTPClient, private settings: Factory, ) {} @@ -18,9 +17,7 @@ export class ZebedeePaymentsProcesor implements IPaymentsProcessor { debug('get invoice: %s', invoiceId) try { - const response = await this.httpClient.get(`/v0/charges/${invoiceId}`, { - maxRedirects: 1, - }) + const response = await this.httpClient.get(`/v0/charges/${invoiceId}`) return fromZebedeeInvoice(response.data.data) } catch (error) { @@ -49,9 +46,7 @@ export class ZebedeePaymentsProcesor implements IPaymentsProcessor { try { debug('request body: %o', body) - const response = await this.httpClient.post('/v0/charges', body, { - maxRedirects: 1, - }) + const response = await this.httpClient.post('/v0/charges', body) const result = fromZebedeeInvoice(response.data.data) diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index 8b80b071..3511b751 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -1,4 +1,3 @@ -import mongoose from 'mongoose' import { __, always, applySpec, identity, ifElse, is, isNil, path, paths, pipe, prop, propSatisfies } from 'ramda' import { EventId } from '../@types/base.ts' @@ -24,7 +23,7 @@ const debug = createLogger('event-repository') export class EventRepository implements IEventRepository { constructor(private readonly settings: () => Settings) {} - public findByFilters(filters: SubscriptionFilter[]): mongoose.Aggregate { + public findByFilters(filters: SubscriptionFilter[]): { cursor: Promise } { debug('querying for %o', filters) if (!Array.isArray(filters) || !filters.length) { throw new Error('Filters cannot be empty') @@ -34,7 +33,7 @@ export class EventRepository implements IEventRepository { const maxLimit = subscriptionLimits?.maxLimit ?? 0 // @ts-ignore: Model static method has been added - return readReplicaEventsModel.findBySubscriptionFilter(filters, maxLimit) + return { cursor: readReplicaEventsModel.findBySubscriptionFilter(filters, maxLimit) } } public async create(event: Event): Promise { diff --git a/src/repositories/invoice-repository.ts b/src/repositories/invoice-repository.ts index f75948b2..36fcbc24 100644 --- a/src/repositories/invoice-repository.ts +++ b/src/repositories/invoice-repository.ts @@ -1,10 +1,13 @@ -import { always, applySpec, ifElse, is, omit, pipe, prop, propSatisfies, toString } from 'ramda' +import { always, applySpec, head, ifElse, is, map, omit, pipe, prop, propSatisfies, toString } from 'ramda' -import { DatabaseClient } from '../@types/base.ts' -import { DBInvoice, Invoice, InvoiceStatus } from '../@types/invoice.ts' -import { IInvoiceRepository } from '../@types/repositories.ts' -import { createLogger } from '../factories/logger-factory.ts' -import { fromDBInvoice, toBuffer } from '../utils/transform.ts' +import { DatabaseClient } from '@/@types/base.ts' +import { DBInvoice, Invoice, InvoiceStatus } from '@/@types/invoice.ts' +import { IInvoiceRepository } from '@/@types/repositories.ts' +import { createLogger } from '@/factories/logger-factory.ts' +import { fromDBInvoice } from '@/utils/transform.ts' +import { masterInvoicesModel } from '@/database/models/Invoices.ts' +import { masterUsersModel } from '@/database/models/Users.ts' +import { DatabaseTransaction } from '@/@types/base.ts' const debug = createLogger('invoice-repository') @@ -15,7 +18,7 @@ export class InvoiceRepository implements IInvoiceRepository { invoiceId: string, amountPaid: bigint, confirmedAt: Date, - client: DatabaseClient = this.dbClient, + session: DatabaseTransaction, ): Promise { debug( 'confirming invoice %s at %s: %s', @@ -25,14 +28,39 @@ export class InvoiceRepository implements IInvoiceRepository { ) try { - await client.raw( - 'select confirm_invoice(?, ?, ?)', - [ - invoiceId, - amountPaid.toString(), - confirmedAt.toISOString(), - ], - ) + const invoice = await masterInvoicesModel.findOne({ id: invoiceId }) + if (invoice) { + const options = { ...(session && { session }) } + + await masterInvoicesModel.updateOne( + { id: invoiceId }, + { + $set: { + confirmed_at: confirmedAt, + amount_paid: amountPaid, + updated_at: new Date(), + }, + }, + options, + ) + + let balance = 0n + if (invoice.unit === 'sats') { + balance = amountPaid * 1000n + } else if (invoice.unit === 'msats') { + balance = amountPaid + } else if (invoice.unit === 'btc') { + balance = amountPaid * 100000000n * 1000n + } + + await masterUsersModel.updateOne( + { pubkey: invoice.pubkey }, + { + $inc: { balance }, + }, + options, + ) + } } catch (error) { console.error('Unable to confirm invoice. Reason:', error.message) @@ -42,11 +70,8 @@ export class InvoiceRepository implements IInvoiceRepository { public async findById( id: string, - client: DatabaseClient = this.dbClient, ): Promise { - const [dbInvoice] = await client('invoices') - .where('id', id) - .select() + const dbInvoice = await masterInvoicesModel.findOne({ id }) if (!dbInvoice) { return @@ -58,30 +83,63 @@ export class InvoiceRepository implements IInvoiceRepository { public async findPendingInvoices( offset = 0, limit = 10, - client: DatabaseClient = this.dbClient, ): Promise { - const dbInvoices = await client('invoices') - .where('status', InvoiceStatus.PENDING) - .offset(offset) + const dbInvoices = await masterInvoicesModel + .find({ status: InvoiceStatus.PENDING }) + .skip(offset) .limit(limit) - .select() return dbInvoices.map(fromDBInvoice) } + public updateStatus( + invoice: Invoice, + session?: DatabaseTransaction, + ): Promise { + debug('updating invoice status: %o', invoice) + + const options: any = { ...(session && { session }) } + const query = masterInvoicesModel.updateOne({ + id: invoice.id, + }, { + status: invoice.status, + updated_at: new Date(), + }, options) + + return ignoreUpdateConflicts(query) + + // const query = client('invoices') + // .update({ + // status: invoice.status, + // updated_at: new Date(), + // }) + // .where('id', invoice.id) + // .limit(1) + // .returning(['*']) + + // return { + // then: ( + // onfulfilled: (value: Invoice | undefined) => T1 | PromiseLike, + // onrejected: (reason: any) => T2 | PromiseLike, + // ) => query.then(pipe(map(fromDBInvoice), head)).then(onfulfilled, onrejected), + // catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), + // toString: (): string => query.toString(), + // } as Promise + } + public upsert( invoice: Invoice, - client: DatabaseClient = this.dbClient, ): Promise { debug('upserting invoice: %o', invoice) - const row = applySpec({ + const row: DBInvoice = applySpec({ id: ifElse( propSatisfies(is(String), 'id'), prop('id'), always(crypto.randomUUID()), ), - pubkey: pipe(prop('pubkey'), toBuffer), + // pubkey: pipe(prop('pubkey'), toBuffer), + pubkey: prop('pubkey'), bolt11: prop('bolt11'), amount_requested: pipe(prop('amountRequested'), toString), // amount_paid: ifElse(propSatisfies(is(BigInt), 'amountPaid'), pipe(prop('amountPaid'), toString), always(null)), @@ -97,33 +155,51 @@ export class InvoiceRepository implements IInvoiceRepository { debug('row: %o', row) - const query = client('invoices') - .insert(row) - .onConflict('id') - .merge( - omit([ - 'id', - 'pubkey', - 'bolt11', - 'amount_requested', - 'unit', - 'description', - 'expires_at', - 'created_at', - 'verify_url', - ])(row), - ) - - return { - then: ( - onfulfilled: (value: number) => T1 | PromiseLike, - onrejected: (reason: any) => T2 | PromiseLike, - ) => query.then(prop('rowCount') as () => number).then( - onfulfilled, - onrejected, - ), - catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), - toString: (): string => query.toString(), - } as Promise + const query = masterInvoicesModel.updateOne({ id: row.id }, { $set: row }, { upsert: true }) + + return ignoreUpdateConflicts(query) + + // const query = client('invoices') + // .insert(row) + // .onConflict('id') + // .merge( + // omit([ + // 'id', + // 'pubkey', + // 'bolt11', + // 'amount_requested', + // 'unit', + // 'description', + // 'expires_at', + // 'created_at', + // 'verify_url', + // ])(row), + // ) + + // return { + // then: ( + // onfulfilled: (value: number) => T1 | PromiseLike, + // onrejected: (reason: any) => T2 | PromiseLike, + // ) => query.then(prop('rowCount') as () => number).then( + // onfulfilled, + // onrejected, + // ), + // catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), + // toString: (): string => query.toString(), + // } as Promise + } +} + +async function ignoreUpdateConflicts(query: any) { + try { + const result = await query + debug('ignoreUpdateConflicts result: %o', result) + return result.upsertedCount || result.modifiedCount || 0 + } catch (err) { + debug('ignoreUpdateConflicts error: %o', err) + if (!String(err).indexOf('E11000 duplicate key error collection')) { + console.error(String(err)) + } + return 0 } } diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 249b83a6..5310134b 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -1,10 +1,12 @@ -import { Pubkey } from '../@types/base.ts' +import { always, applySpec, prop } from 'ramda' + +import { DatabaseTransaction, Pubkey } from '@/@types/base.ts' import { IUserRepository } from '../@types/repositories.ts' import { User } from '../@types/user.ts' import { Settings } from '../@types/settings.ts' import { masterUsersModel } from '../database/models/Users.ts' import { createLogger } from '../factories/logger-factory.ts' -import { fromDBUser, toBuffer } from '../utils/transform.ts' +import { fromDBUser } from '../utils/transform.ts' const debug = createLogger('user-repository') @@ -13,11 +15,7 @@ export class UserRepository implements IUserRepository { public async findByPubkey(pubkey: Pubkey): Promise { debug('find by pubkey: %s', pubkey) - const dbuser = await masterUsersModel - .findOne({ - pubkey: toBuffer(pubkey), - }) - + const dbuser = await masterUsersModel.findOne({ pubkey }) if (!dbuser) { return } @@ -25,22 +23,21 @@ export class UserRepository implements IUserRepository { return fromDBUser(dbuser) } - public async upsert(user: User): Promise { + public async upsert(user: User, session?: DatabaseTransaction): Promise { debug('upsert: %o', user) const date = new Date() - const row = { - pubkey: toBuffer(user.pubkey), - is_admitted: user.isAdmitted, - tos_accepted_at: user.tosAcceptedAt, - updated_at: date, - created_at: date, - } + const row = applySpec({ + pubkey: prop('pubkey'), + is_admitted: prop('isAdmitted'), + tos_accepted_at: prop('tosAcceptedAt'), + updated_at: always(date), + created_at: always(date), + })(user) const filter = { pubkey: row.pubkey } - const options = { upsert: true } - + const options: any = { upsert: true, ...(session && { session }) } const result = await masterUsersModel.updateOne(filter, { $set: row }, options) return result.upsertedCount ?? result.modifiedCount @@ -49,12 +46,12 @@ export class UserRepository implements IUserRepository { public async getBalanceByPubkey(pubkey: Pubkey): Promise { debug('get balance for pubkey: %s', pubkey) - const user = await masterUsersModel.findOne({ pubkey: toBuffer(pubkey) }, { balance: 1 }) + const user = await masterUsersModel.findOne({ pubkey }, { balance: 1 }) if (!user) { return 0n } - return BigInt(user.balance) + return user.balance } } diff --git a/src/routes/callbacks/index.ts b/src/routes/callbacks/index.ts index 7b95cf23..09526fa5 100644 --- a/src/routes/callbacks/index.ts +++ b/src/routes/callbacks/index.ts @@ -1,84 +1,18 @@ -import { helpers, Router } from 'oak' - -import type { RouterContext } from '../../@types/controllers.ts' -import { NextFunction, Request, Response, Status } from '../../@types/controllers.ts' -import { createLogger } from '../../factories/logger-factory.ts' -import { createSettings } from '../../factories/settings-factory.ts' -import { postLNbitsCallbackRequestHandler } from '../../handlers/request-handlers/post-lnbits-callback-request-handler.ts' -import { postZebedeeCallbackRequestHandler } from '../../handlers/request-handlers/post-zebedee-callback-request-handler.ts' -import { getRemoteAddress } from '../../utils/http.ts' -import { deriveFromSecret, hmacSha256 } from '../../utils/secret.ts' - -const debug = createLogger('routes-callbacks') +import { Router } from '@/@types/controllers.ts' +import { createLNbitsCallbackController } from '@/factories/controllers/lnbits-callback-controller-factory.ts' +// import { createNodelessCallbackController } from '@/factories/controllers/nodeless-callback-controller-factory.ts' +// import { createOpenNodeCallbackController } from '@/factories/controllers/opennode-callback-controller-factory.ts' +import { createZebedeeCallbackController } from '@/factories/controllers/zebedee-callback-controller-factory.ts' +import { withController } from '@/handlers/request-handlers/with-controller-request-handler.ts' +import { bodyParserMiddleware } from '@/handlers/request-handlers/body-parser-middleware.ts' const router = new Router() -router - .post('/zebedee', async (ctx: RouterContext, next: NextFunction) => { - const req: Request = ctx.request - const res: Response = ctx.response - - const settings = createSettings() - const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {} - const remoteAddress = getRemoteAddress(req, settings) - const paymentProcessor = settings.payments?.processor ?? 'null' - - if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) { - debug( - 'unauthorized request from %s to /callbacks/zebedee', - remoteAddress, - ) - ctx.throw(Status.Forbidden, 'Forbidden') - } +router.use(bodyParserMiddleware) - if (paymentProcessor !== 'zebedee') { - debug( - 'denied request from %s to /callbacks/zebedee which is not the current payment processor', - remoteAddress, - ) - ctx.throw(Status.Forbidden, 'Forbidden') - } - - await postZebedeeCallbackRequestHandler(req, res) - await next() - }) - .post('/lnbits', async (ctx: RouterContext, next) => { - const req: Request = ctx.request - const res: Response = ctx.response - const settings = createSettings() - const remoteAddress = getRemoteAddress(req, settings) - const paymentProcessor = settings.payments?.processor ?? 'null' - - if (paymentProcessor !== 'lnbits') { - debug( - 'denied request from %s to /callbacks/lnbits which is not the current payment processor', - remoteAddress, - ) - ctx.throw(Status.Forbidden, 'Forbidden') - } - - let validationPassed = false - const query = helpers.getQuery(ctx) - if ( - typeof query.hmac === 'string' && - query.hmac.match(/^[0-9]{1,20}:[0-9a-f]{64}$/) - ) { - const split = query.hmac.split(':') - if ( - hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), split[0]) - .toString('hex') === split[1] - ) { - if (parseInt(split[0]) > Date.now()) { - validationPassed = true - } - } - } - - if (!validationPassed) { - debug('unauthorized request from %s to /callbacks/lnbits', remoteAddress) - ctx.throw(Status.Forbidden, 'Forbidden') - } - await postLNbitsCallbackRequestHandler(req, res, ctx) - await next() - }) +router + .post('/zebedee', withController(createZebedeeCallbackController)) + .post('/lnbits', withController(createLNbitsCallbackController)) +// .post('/nodeless', withController(createNodelessCallbackController)) +// .post('/opennode', withController(createOpenNodeCallbackController)) export default router diff --git a/src/routes/index.ts b/src/routes/index.ts index bb407b6b..f453ffbf 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,11 +1,11 @@ import { Router } from 'oak' -import { getHealthRequestHandler } from '../handlers/request-handlers/get-health-request-handler.ts' -import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler.ts' -import { rateLimiterMiddleware } from '../handlers/request-handlers/rate-limiter-middleware.ts' -import { rootRequestHandler } from '../handlers/request-handlers/root-request-handler.ts' -import callbacksRouter from './callbacks/index.ts' -import invoiceRouter from './invoices/index.ts' +import { getHealthRequestHandler } from '@/handlers/request-handlers/get-health-request-handler.ts' +import { getTermsRequestHandler } from '@/handlers/request-handlers/get-terms-request-handler.ts' +import { rateLimiterMiddleware } from '@/handlers/request-handlers/rate-limiter-middleware.ts' +import { rootRequestHandler } from '@/handlers/request-handlers/root-request-handler.ts' +import callbacksRouter from '@/routes/callbacks/index.ts' +import invoiceRouter from '@/routes/invoices/index.ts' const router = new Router() diff --git a/src/routes/invoices/index.ts b/src/routes/invoices/index.ts index 9fd326bd..d2922446 100644 --- a/src/routes/invoices/index.ts +++ b/src/routes/invoices/index.ts @@ -1,14 +1,16 @@ import { Router } from 'oak' -import { getInvoiceRequestHandler } from '../../handlers/request-handlers/get-invoice-request-handler.ts' -import { getInvoiceStatusRequestHandler } from '../../handlers/request-handlers/get-invoice-status-request-handler.ts' -import { postInvoiceRequestHandler } from '../../handlers/request-handlers/post-invoice-request-handler.ts' +import { createGetInvoiceController } from '@/factories/controllers/get-invoice-controller-factory.ts' +import { createGetInvoiceStatusController } from '@/factories/controllers/get-invoice-status-controller-factory.ts' +import { createPostInvoiceController } from '@/factories/controllers/post-invoice-controller-factory.ts' +import { withController } from '@/handlers/request-handlers/with-controller-request-handler.ts' +import { bodyParserMiddleware } from '@/handlers/request-handlers/body-parser-middleware.ts' const invoiceRouter = new Router() invoiceRouter - .get('/', getInvoiceRequestHandler) - .get('/:invoiceId/status', getInvoiceStatusRequestHandler) - .post('/', postInvoiceRequestHandler) + .get('/', withController(createGetInvoiceController)) + .get('/:invoiceId/status', withController(createGetInvoiceStatusController)) + .post('/', bodyParserMiddleware, withController(createPostInvoiceController)) export default invoiceRouter diff --git a/src/services/payments-service.ts b/src/services/payments-service.ts index 0663612d..3d176da5 100644 --- a/src/services/payments-service.ts +++ b/src/services/payments-service.ts @@ -35,11 +35,10 @@ export class PaymentsService implements IPaymentsService { } } - public async getInvoiceFromPaymentsProcessor(invoice: Invoice): Promise> { - debug('get invoice %s from payment processor', invoice.id) + public async getInvoiceFromPaymentsProcessor(invoice: Invoice | string): Promise> { try { return await this.paymentsProcessor.getInvoice( - this.settings().payments?.processor === 'lnurl' ? invoice : invoice.id, + typeof invoice === 'string' || invoice?.verifyURL ? invoice : invoice.id, ) } catch (error) { console.log( @@ -117,17 +116,9 @@ export class PaymentsService implements IPaymentsService { public async updateInvoice(invoice: Partial): Promise { debug('update invoice %s: %o', invoice.id, invoice) try { - await this.invoiceRepository.upsert({ + await this.invoiceRepository.updateStatus({ id: invoice.id, - pubkey: invoice.pubkey, - bolt11: invoice.bolt11, - amountRequested: invoice.amountRequested, - description: invoice.description, - unit: invoice.unit, status: invoice.status, - expiresAt: invoice.expiresAt, - updatedAt: new Date(), - createdAt: invoice.createdAt, }) } catch (error) { console.error('Unable to update invoice. Reason:', error) @@ -135,15 +126,10 @@ export class PaymentsService implements IPaymentsService { } } - public async updateInvoiceStatus(invoice: Partial): Promise { + public async updateInvoiceStatus(invoice: Pick): Promise { debug('update invoice %s: %o', invoice.id, invoice) try { - const fullInvoice = await this.invoiceRepository.findById(invoice.id) - await this.invoiceRepository.upsert({ - ...fullInvoice, - status: invoice.status, - updatedAt: new Date(), - }) + return await this.invoiceRepository.updateStatus(invoice) } catch (error) { console.error('Unable to update invoice. Reason:', error) throw error @@ -153,13 +139,13 @@ export class PaymentsService implements IPaymentsService { public async confirmInvoice( invoice: Invoice, ): Promise { - debug('confirm invoice %s: %o', invoice.id, invoice) + debug('confirm invoice %s: %O', invoice.id, invoice) const transaction = new Transaction(this.dbClient) try { if (!invoice.confirmedAt) { - throw new Error('Invoince confirmation date is not set') + throw new Error('Invoice confirmation date is not set') } if (invoice.status !== InvoiceStatus.COMPLETED) { throw new Error(`Invoice is not complete: ${invoice.status}`) diff --git a/src/utils/event.ts b/src/utils/event.ts index aae32870..0eac6575 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -1,6 +1,7 @@ import { Buffer } from 'Buffer' import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda' import * as secp256k1 from 'secp256k1' +import crypto from 'crypto' import { EventId, Pubkey, Tag } from '../@types/base.ts' import { CanonicalEvent, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event.ts' @@ -13,7 +14,7 @@ import { isGenericTagQuery } from './filter.ts' import { getLeadingZeroBits } from './proof-of-work.ts' import { RuneLike } from './runes/rune-like.ts' import { deriveFromSecret } from './secret.ts' -import { fromBuffer } from './transform.ts' +import { fromBuffer, toBuffer } from './transform.ts' export const serializeEvent = (event: UnidentifiedEvent): CanonicalEvent => [ 0, @@ -34,6 +35,16 @@ export const toNostrEvent: (event: DBEvent) => Event = applySpec({ sig: pipe(prop('event_signature') as () => Buffer, fromBuffer), }) +export const toDBEvent: (event: Event) => DBEvent = applySpec({ + event_id: pipe(prop('id'), toBuffer), + event_kind: pipe(prop('kind'), Number), + event_pubkey: pipe(prop('pubkey'), toBuffer), + event_created_at: pipe(prop('created_at'), Number), + event_content: pipe(prop('content'), String), + event_tags: prop('tags'), + event_signature: pipe(prop('sig'), toBuffer), +}) + export const isEventKindOrRangeMatch = ({ kind }: Event) => (item: EventKinds | EventKindsRange) => typeof item === 'number' ? item === kind : kind >= item[0] && kind <= item[1] export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Event): boolean => { @@ -229,7 +240,7 @@ export const encryptKind4Event = ( .getSharedSecret(senderPrivkey, `02${receiverPubkey}`, true) .subarray(1) - const iv = crypto.getRandomValues(new Uint8Array(16)) + const iv = crypto.randomBytes(16) // deepcode ignore InsecureCipherNoIntegrity: NIP-04 Encrypted Direct Message uses aes-256-cbc const cipher = crypto.createCipheriv( @@ -248,28 +259,8 @@ export const encryptKind4Event = ( } } -export const broadcastEvent = async (/*event: Event*/): Promise => { - return new Promise((resolve /*reject*/) => { - return resolve() - // if (!cluster.isWorker || typeof process.send === 'undefined') { - // return resolve(event) - // } - - // process.send( - // { - // eventName: WebSocketServerAdapterEvent.Broadcast, - // event, - // }, - // undefined, - // undefined, - // (error: Error | null) => { - // if (error) { - // return reject(error) - // } - // resolve(event) - // }, - // ) - }) +export const broadcastEvent = async (event: Event): Promise => { + return event } export const isReplaceableEvent = (event: Event): boolean => { diff --git a/src/utils/filter.ts b/src/utils/filter.ts index 916e5013..2e1d5c94 100644 --- a/src/utils/filter.ts +++ b/src/utils/filter.ts @@ -1 +1 @@ -export const isGenericTagQuery = (key: string) => /^#[a-z]$/.test(key) +export const isGenericTagQuery = (key: string) => /^#[a-zA-Z]$/.test(key) diff --git a/src/utils/http.ts b/src/utils/http.ts index f592baa9..b4192ea0 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -1,4 +1,5 @@ import { Request } from 'oak' +import { CreateAxiosDefaults } from 'axios' import { Settings } from '../@types/settings.ts' @@ -24,3 +25,126 @@ export const getRemoteAddress = ( return result?.split?.(',')?.[0] || '' } + +interface FetchOptions { + output?: 'json' | 'raw' | 'text' + interceptor?: boolean + timeout?: number + debug?: boolean + client?: Deno.HttpClient + proxyURL?: string +} + +export class HTTPClient { + private config: { + baseURL: string + headers?: any + timeout?: number + } + + constructor(config?: CreateAxiosDefaults, private proxyClient?: Deno.HttpClient) { + this.config = { + baseURL: config?.baseURL as string, + headers: { + ...config?.headers, + }, + } + } + + private baseURL(url: string) { + return new URL(url, this.config?.baseURL).toString() + } + + private async do(url: string, init: RequestInit & FetchOptions): Promise { + const { + method = 'GET', + body, + headers = {}, + output = 'json', + interceptor = true, + timeout = 30000, // timeout 30s + debug = false, // show response content + proxyURL, + } = init || {} + let { client: proxyClient } = init || {} + + // timeout handle + const ac = new AbortController() + const timeouttimer = setTimeout(() => ac.abort(`HTTP Request Timeout exceed ${timeout}ms`), timeout) + const options: RequestInit & { client?: Deno.HttpClient } = { + method, + headers: { + ...this.config.headers, + ...headers, + }, + body, + signal: ac.signal, + } + + // use proxy agent. proxyClient > proxyURL > this.proxyCient + if (proxyURL && !proxyClient) { + proxyClient = Deno.createHttpClient({ + proxy: { + url: proxyURL, + }, + }) + } + proxyClient = proxyClient || this.proxyClient + if (proxyClient) { + options.client = proxyClient + options.keepalive = true + } + + const response = await fetch(this.baseURL(url), options) + clearTimeout(timeouttimer) + + if (!response.ok && interceptor) { + if (debug) { + console.log('HTTP Request Response:', await response.text()) + } + throw new Error(`HTTP Request faild status: ${response.status}`) + } + + if (output === 'raw') { + return response + } + + if (output !== 'json') { + return response.text() + } + + return response.json() + } + + async get(url: string, init?: RequestInit & FetchOptions) { + return { + data: await this.do(url, { + method: 'GET', + ...init, + }), + } + } + + async post(url: string, body: Record, init?: RequestInit & FetchOptions) { + return { + data: await this.do(url, { + method: 'POST', + ...init, + body: JSON.stringify(body), + redirect: 'follow', + }), + } + } + + useProxy(client: string | Deno.HttpClient) { + if (typeof client === 'string') { + this.proxyClient = Deno.createHttpClient({ + proxy: { + url: client, + }, + }) + } else { + this.proxyClient = client + } + } +} diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 39fae439..f530d01c 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -1,9 +1,8 @@ import { bech32 } from 'bech32' import { Buffer } from 'Buffer' -import { always, applySpec, ifElse, is, isNil, path, pipe, prop, propSatisfies } from 'ramda' +import { always, applySpec, cond, equals, ifElse, is, isNil, multiply, path, pathSatisfies, pipe, prop, propSatisfies, T } from 'ramda' -import { Invoice } from '../@types/invoice.ts' -import { User } from '../@types/user.ts' +import { Invoice, InvoiceStatus, InvoiceUnit } from '@/@types/invoice.ts' export const toJSON = (input: any) => JSON.stringify(input) @@ -11,13 +10,15 @@ export const toBuffer = (input: any) => Buffer.from(input, 'hex') export const fromBuffer = (input: Buffer) => input.toString('hex') -export const toBigInt = (input: string): bigint => BigInt(input) +export const toBigInt = (input: string | number): bigint => BigInt(input) export const fromBigInt = (input: bigint) => input.toString() -export const fromDBInvoice = applySpec({ +const addTime = (ms: number) => (input: Date) => new Date(input.getTime() + ms) + +export const fromDBInvoice = applySpec({ id: prop('id') as () => string, - pubkey: pipe(prop('pubkey') as () => Buffer, fromBuffer), + pubkey: prop('pubkey'), bolt11: prop('bolt11'), amountRequested: pipe(prop('amount_requested') as () => string, toBigInt), amountPaid: ifElse( @@ -35,8 +36,8 @@ export const fromDBInvoice = applySpec({ verifyURL: prop('verify_url'), }) -export const fromDBUser = applySpec({ - pubkey: pipe(prop('pubkey') as () => Buffer, fromBuffer), +export const fromDBUser = applySpec({ + pubkey: prop('pubkey'), isAdmitted: prop('is_admitted'), balance: prop('balance'), createdAt: prop('created_at'), @@ -58,9 +59,9 @@ export const toBech32 = (prefix: string) => (input: string): string => { return bech32.encode(prefix, bech32.toWords(Buffer.from(input, 'hex'))) } -export const toDate = (input: string) => new Date(input) +export const toDate = (input: string | number) => new Date(input) -export const fromZebedeeInvoice = applySpec({ +export const fromZebedeeInvoice = applySpec({ id: prop('id'), pubkey: prop('internalId'), bolt11: path(['invoice', 'request']), @@ -84,4 +85,95 @@ export const fromZebedeeInvoice = applySpec({ always(null), ), rawRespose: toJSON, +}) as Invoice + +export const fromNodelessInvoice = applySpec({ + id: prop('id'), + pubkey: path(['metadata', 'requestId']), + bolt11: prop('lightningInvoice'), + amountRequested: pipe(prop('satsAmount') as () => number, toBigInt), + description: path(['metadata', 'description']), + unit: path(['metadata', 'unit']), + status: pipe( + prop('status'), + cond([ + [equals('new'), always(InvoiceStatus.PENDING)], + [equals('pending_confirmation'), always(InvoiceStatus.PENDING)], + [equals('underpaid'), always(InvoiceStatus.PENDING)], + [equals('in_flight'), always(InvoiceStatus.PENDING)], + [equals('paid'), always(InvoiceStatus.COMPLETED)], + [equals('overpaid'), always(InvoiceStatus.COMPLETED)], + [equals('expired'), always(InvoiceStatus.EXPIRED)], + ]), + ), + expiresAt: ifElse( + propSatisfies(is(String), 'expiresAt'), + pipe(prop('expiresAt'), toDate), + ifElse( + propSatisfies(is(String), 'createdAt'), + pipe(prop('createdAt'), toDate, addTime(15 * 60000)), + always(null), + ), + ), + confirmedAt: cond([ + [propSatisfies(is(String), 'paidAt'), pipe(prop('paidAt'), toDate)], + [T, always(null)], + ]), + createdAt: ifElse( + propSatisfies(is(String), 'createdAt'), + pipe(prop('createdAt'), toDate), + always(null), + ), + // rawResponse: toJSON, +}) as Invoice + +export const fromOpenNodeInvoice = applySpec({ + id: prop('id'), + pubkey: prop('order_id'), + bolt11: ifElse( + pathSatisfies(is(String), ['lightning_invoice', 'payreq']), + path(['lightning_invoice', 'payreq']), + path(['lightning', 'payreq']), + ), + amountRequested: pipe( + ifElse( + propSatisfies(is(Number), 'amount'), + prop('amount'), + prop('price'), + ) as () => number, + toBigInt, + ), + description: prop('description'), + unit: always(InvoiceUnit.SATS), + status: pipe( + prop('status'), + cond([ + [equals('expired'), always(InvoiceStatus.EXPIRED)], + [equals('refunded'), always(InvoiceStatus.EXPIRED)], + [equals('unpaid'), always(InvoiceStatus.PENDING)], + [equals('processing'), always(InvoiceStatus.PENDING)], + [equals('underpaid'), always(InvoiceStatus.PENDING)], + [equals('paid'), always(InvoiceStatus.COMPLETED)], + ]), + ), + expiresAt: pipe( + cond([ + [pathSatisfies(is(String), ['lightning', 'expires_at']), path(['lightning', 'expires_at'])], + [pathSatisfies(is(Number), ['lightning_invoice', 'expires_at']), pipe(path(['lightning_invoice', 'expires_at']), multiply(1000))], + ]), + toDate, + ), + confirmedAt: cond([ + [propSatisfies(equals('paid'), 'status'), () => new Date()], + [T, always(null)], + ]), + createdAt: pipe( + ifElse( + propSatisfies(is(Number), 'created_at'), + pipe(prop('created_at'), multiply(1000)), + prop('created_at'), + ), + toDate, + ), + rawResponse: toJSON, }) diff --git a/test/integration/features/helpers.ts b/test/integration/features/helpers.ts index cedde1b5..60d50cad 100644 --- a/test/integration/features/helpers.ts +++ b/test/integration/features/helpers.ts @@ -1,5 +1,5 @@ import { Buffer } from 'Buffer' -import { createHash, createHmac } from 'crypto' +import { createHash, createHmac, randomBytes } from 'crypto' import { EventEmitter } from 'events' import { Observable } from 'rxjs' @@ -99,7 +99,7 @@ export async function createEvent( } export function createIdentity(name: string) { - const hmac = createHmac('sha256', Math.random().toString()) + const hmac = createHmac('sha256', randomBytes(10)) hmac.update(name) const privkey = hmac.digest().toString('hex') const pubkey = Buffer.from(secp256k1.getPublicKey(privkey, true)).toString( diff --git a/test/integration/features/shared.ts b/test/integration/features/shared.ts index 3a27591a..7d36b739 100644 --- a/test/integration/features/shared.ts +++ b/test/integration/features/shared.ts @@ -6,7 +6,7 @@ import { assocPath, pipe } from 'ramda' import { fromEvent, map, Observable, ReplaySubject, Subject, takeUntil } from 'rxjs' import Sinon from 'sinon' import { afterAll, beforeAll, describe, it } from 'jest' -import { DatabaseClient1 as DatabaseClient } from '../../../src/@types/base.ts' +import { DatabaseClient } from '../../../src/@types/base.ts' import { Event } from '../../../src/@types/event.ts' import { AppWorker } from '../../../src/app/worker.ts' import Config from '../../../src/config/index.ts' @@ -19,6 +19,7 @@ import { masterEventsModel } from '../../../src/database/models/Events.ts' import { api } from '../../../src/core-services/index.ts' import { DatabaseWatcher } from '../../../src/database/DatabaseWatcher.ts' import { initWatchers } from '../../../src/database/watchers.ts' +import { getCacheClient } from '../../../src/cache/client.ts' export const isDraft = Symbol('draft') @@ -135,8 +136,12 @@ export const startTest = async (pathUrl: string, registerEvent: Function) => { Config.RELAY_PORT = '18808' Config.SECRET = Math.random().toString().repeat(6) - rrDbClient = getReadReplicaDbClient() - await rrDbClient.asPromise() + if (!Config.MONGO_READ_REPLICA_ENABLED) { + rrDbClient = dbClient + } else { + rrDbClient = getReadReplicaDbClient() + await rrDbClient.asPromise() + } watcher = new DatabaseWatcher({ db: dbClient.db, @@ -166,11 +171,15 @@ export const startTest = async (pathUrl: string, registerEvent: Function) => { afterAll(async function () { worker.close(async () => { try { + const cacheClient = await getCacheClient() + if (cacheClient) { + cacheClient.close() + } await watcher.close() - await Promise.all([ - dbClient.destroy(true), - rrDbClient.destroy(true), - ]) + await dbClient.destroy() + if (Config.MONGO_READ_REPLICA_ENABLED) { + await rrDbClient.destroy() + } } catch (e) { console.info(e, 'close error') } @@ -186,7 +195,9 @@ export const startTest = async (pathUrl: string, registerEvent: Function) => { let currentList: string[] = [] for (let line of contentList) { line = line.trim() - if (!line) continue + if (!line) { + continue + } if (line.startsWith('Scenario:')) { currentList = [] scenarioList.push({ list: currentList, line }) diff --git a/test/unit/handlers/event-strategies/default-event-strategy.test.ts b/test/unit/handlers/event-strategies/default-event-strategy.test.ts index 94937b5a..3fd81a68 100644 --- a/test/unit/handlers/event-strategies/default-event-strategy.test.ts +++ b/test/unit/handlers/event-strategies/default-event-strategy.test.ts @@ -2,13 +2,13 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'jest' import Sinon from 'sinon' -import SinonChi from 'sinon-chai' +import SinonChai from 'sinon-chai' chai.use(chaiAsPromised) -chai.use(SinonChi) +chai.use(SinonChai) import { IWebSocketAdapter } from '../../../../src/@types/adapters.ts' -import { DatabaseClient1 as DatabaseClient } from '../../../../src/@types/base.ts' +import { DatabaseClient } from '../../../../src/@types/base.ts' import { Event } from '../../../../src/@types/event.ts' import { IEventStrategy } from '../../../../src/@types/message-handlers.ts' import { MessageType } from '../../../../src/@types/messages.ts' diff --git a/test/unit/handlers/event-strategies/delete-event-strategy.test.ts b/test/unit/handlers/event-strategies/delete-event-strategy.test.ts index a50e6394..5619b123 100644 --- a/test/unit/handlers/event-strategies/delete-event-strategy.test.ts +++ b/test/unit/handlers/event-strategies/delete-event-strategy.test.ts @@ -2,13 +2,13 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'jest' import Sinon from 'sinon' -import SinonChi from 'sinon-chai' +import SinonChai from 'sinon-chai' chai.use(chaiAsPromised) -chai.use(SinonChi) +chai.use(SinonChai) import { IWebSocketAdapter } from '../../../../src/@types/adapters.ts' -import { DatabaseClient1 as DatabaseClient } from '../../../../src/@types/base.ts' +import { DatabaseClient } from '../../../../src/@types/base.ts' import { Event } from '../../../../src/@types/event.ts' import { IEventStrategy } from '../../../../src/@types/message-handlers.ts' import { MessageType } from '../../../../src/@types/messages.ts' diff --git a/test/unit/handlers/event-strategies/parameterized-replaceable-event-strategy.test.ts b/test/unit/handlers/event-strategies/parameterized-replaceable-event-strategy.test.ts index f403a46f..e1a2ccda 100644 --- a/test/unit/handlers/event-strategies/parameterized-replaceable-event-strategy.test.ts +++ b/test/unit/handlers/event-strategies/parameterized-replaceable-event-strategy.test.ts @@ -2,13 +2,13 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'jest' import Sinon from 'sinon' -import SinonChi from 'sinon-chai' +import SinonChai from 'sinon-chai' chai.use(chaiAsPromised) -chai.use(SinonChi) +chai.use(SinonChai) import { IWebSocketAdapter } from '../../../../src/@types/adapters.ts' -import { DatabaseClient1 as DatabaseClient } from '../../../../src/@types/base.ts' +import { DatabaseClient } from '../../../../src/@types/base.ts' import { Event } from '../../../../src/@types/event.ts' import { IEventStrategy } from '../../../../src/@types/message-handlers.ts' import { MessageType } from '../../../../src/@types/messages.ts' diff --git a/test/unit/handlers/event-strategies/replaceable-event-strategy.test.ts b/test/unit/handlers/event-strategies/replaceable-event-strategy.test.ts index 7ddd24f9..73e64a96 100644 --- a/test/unit/handlers/event-strategies/replaceable-event-strategy.test.ts +++ b/test/unit/handlers/event-strategies/replaceable-event-strategy.test.ts @@ -3,13 +3,13 @@ import chaiAsPromised from 'chai-as-promised' import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'jest' import Sinon from 'sinon' -import SinonChi from 'sinon-chai' +import SinonChai from 'sinon-chai' chai.use(chaiAsPromised) -chai.use(SinonChi) +chai.use(SinonChai) import { IWebSocketAdapter } from '../../../../src/@types/adapters.ts' -import { DatabaseClient1 as DatabaseClient } from '../../../../src/@types/base.ts' +import { DatabaseClient } from '../../../../src/@types/base.ts' import { Event } from '../../../../src/@types/event.ts' import { IEventStrategy } from '../../../../src/@types/message-handlers.ts' import { MessageType } from '../../../../src/@types/messages.ts' diff --git a/test/unit/handlers/subscribe-message-handler.test.ts b/test/unit/handlers/subscribe-message-handler.test.ts index e2d81679..e2263db1 100644 --- a/test/unit/handlers/subscribe-message-handler.test.ts +++ b/test/unit/handlers/subscribe-message-handler.test.ts @@ -7,7 +7,7 @@ import chaiAsPromised from 'chai-as-promised' import { afterEach, beforeEach, describe, it } from 'jest' import { always } from 'ramda' import Sinon from 'sinon' -import SinonChi from 'sinon-chai' +import SinonChai from 'sinon-chai' import { IWebSocketAdapter } from '../../../src/@types/adapters.ts' import { Event } from '../../../src/@types/event.ts' @@ -19,7 +19,7 @@ import { WebSocketAdapterEvent } from '../../../src/constants/adapter.ts' import { SubscribeMessageHandler } from '../../../src/handlers/subscribe-message-handler.ts' chai.use(chaiAsPromised) -chai.use(SinonChi) +chai.use(SinonChai) const { expect } = chai @@ -208,10 +208,10 @@ describe({ const promise = fetch() - stream.emit('error', error) - const closeSpy = sandbox.spy() - stream.once('close', closeSpy) + stream.once('error', closeSpy) + + stream.emit('error', error) await expect(promise).to.eventually.be.rejectedWith(error) expect(closeSpy).to.have.been.called diff --git a/test/unit/handlers/unsubscribe-message-handler.test.ts b/test/unit/handlers/unsubscribe-message-handler.test.ts index e0ecb78c..d64fa273 100644 --- a/test/unit/handlers/unsubscribe-message-handler.test.ts +++ b/test/unit/handlers/unsubscribe-message-handler.test.ts @@ -1,14 +1,14 @@ import chai, { expect } from 'chai' import { beforeEach, describe, it } from 'jest' import Sinon from 'sinon' -import SinonChi from 'sinon-chai' +import SinonChai from 'sinon-chai' import { IWebSocketAdapter } from '../../../src/@types/adapters.ts' import { IMessageHandler } from '../../../src/@types/message-handlers.ts' import { MessageType, UnsubscribeMessage } from '../../../src/@types/messages.ts' import { WebSocketAdapterEvent } from '../../../src/constants/adapter.ts' import { UnsubscribeMessageHandler } from '../../../src/handlers/unsubscribe-message-handler.ts' -chai.use(SinonChi) +chai.use(SinonChai) describe('UnsubscribeMessageHandler', () => { let handler: IMessageHandler diff --git a/test/unit/repositories/event-repository.test.ts b/test/unit/repositories/event-repository.test.ts index 9c6601c3..28d23e38 100644 --- a/test/unit/repositories/event-repository.test.ts +++ b/test/unit/repositories/event-repository.test.ts @@ -12,7 +12,7 @@ chai.use(sinonChai) const { expect } = chai -import { DatabaseClient1 as DatabaseClient } from '../../../src/@types/base.ts' +import { DatabaseClient } from '../../../src/@types/base.ts' import { EventTags } from '../../../src/constants/base.ts' // import { ContextMetadataKey, EventDeduplicationMetadataKey } from '../../../src/constants/base.ts' import { EventRepository } from '../../../src/repositories/event-repository.ts' diff --git a/test/unit/utils/filter.test.ts b/test/unit/utils/filter.test.ts new file mode 100644 index 00000000..d06d2b4d --- /dev/null +++ b/test/unit/utils/filter.test.ts @@ -0,0 +1,23 @@ + +import { expect } from 'chai' +import { describe, it } from 'jest' + +import { isGenericTagQuery } from "@/utils/filter.ts" + +describe('isGenericTagQuery', () => { + it('returns true for #a', () => { + expect(isGenericTagQuery('#a')).to.be.true + }) + + it('returns true for #A', () => { + expect(isGenericTagQuery('#A')).to.be.true + }) + + it('returns false for #0', () => { + expect(isGenericTagQuery('#0')).to.be.false + }) + + it('returns false for #abc', () => { + expect(isGenericTagQuery('#abc')).to.be.false + }) +})