update to latest master

This commit is contained in:
Siddharth Singh 2024-07-21 03:40:23 +05:30
commit 2940ae0661
No known key found for this signature in database
29 changed files with 707 additions and 200 deletions

View file

@ -1,4 +1,4 @@
image: denoland/deno:1.44.2 image: denoland/deno:1.45.2
default: default:
interruptible: true interruptible: true
@ -20,7 +20,10 @@ check:
test: test:
stage: test stage: test
script: deno task test script:
- deno task test --coverage=cov_profile
- deno coverage cov_profile
coverage: /All files[^\|]*\|[^\|]*\s+([\d\.]+)/
variables: variables:
DITTO_NSEC: nsec1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4rm7hz DITTO_NSEC: nsec1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4rm7hz
artifacts: artifacts:
@ -39,3 +42,4 @@ postgres:
DITTO_NSEC: nsec1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4rm7hz DITTO_NSEC: nsec1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4rm7hz
DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust

View file

@ -1 +1 @@
deno 1.44.2 deno 1.45.2

View file

@ -7,9 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [1.1.0] - 2024-07-15
### Added
- Prometheus support (`/metrics` endpoint).
- Sort zaps by amount; add pagination.
### Fixed
- Added IP rate-limiting of HTTP requests and WebSocket messages.
- Added database query timeouts.
- Fixed nos2x compatibility.
## [1.0.0] - 2024-06-14 ## [1.0.0] - 2024-06-14
- Initial release - Initial release
[unreleased]: https://gitlab.com/soapbox-pub/ditto/-/compare/v1.0.0...HEAD [unreleased]: https://gitlab.com/soapbox-pub/ditto/-/compare/v1.1.0...HEAD
[1.1.0]: https://gitlab.com/soapbox-pub/ditto/-/compare/v1.0.0...v1.1.0
[1.0.0]: https://gitlab.com/soapbox-pub/ditto/-/tags/v1.0.0 [1.0.0]: https://gitlab.com/soapbox-pub/ditto/-/tags/v1.0.0

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://deno.land/x/deno@v1.41.0/cli/schemas/config-file.v1.json", "$schema": "https://deno.land/x/deno@v1.41.0/cli/schemas/config-file.v1.json",
"version": "1.0.0", "version": "1.1.0",
"tasks": { "tasks": {
"start": "deno run -A src/server.ts", "start": "deno run -A src/server.ts",
"dev": "deno run -A --watch src/server.ts", "dev": "deno run -A --watch src/server.ts",
@ -27,7 +27,7 @@
"@hono/hono": "jsr:@hono/hono@^4.4.6", "@hono/hono": "jsr:@hono/hono@^4.4.6",
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
"@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0",
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.25.0", "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.26.3",
"@scure/base": "npm:@scure/base@^1.1.6", "@scure/base": "npm:@scure/base@^1.1.6",
"@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs",
"@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0",
@ -51,7 +51,8 @@
"iso-639-1": "npm:iso-639-1@2.1.15", "iso-639-1": "npm:iso-639-1@2.1.15",
"isomorphic-dompurify": "npm:isomorphic-dompurify@^2.11.0", "isomorphic-dompurify": "npm:isomorphic-dompurify@^2.11.0",
"kysely": "npm:kysely@^0.27.3", "kysely": "npm:kysely@^0.27.3",
"kysely_deno_postgres": "https://gitlab.com/soapbox-pub/kysely-deno-postgres/-/raw/c6869b9e12d74af78a846ad503d84493f5db9df4/mod.ts", "postgres": "https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/mod.js",
"kysely-postgres-js": "npm:kysely-postgres-js@2.0.0",
"light-bolt11-decoder": "npm:light-bolt11-decoder", "light-bolt11-decoder": "npm:light-bolt11-decoder",
"linkify-plugin-hashtag": "npm:linkify-plugin-hashtag@^4.1.1", "linkify-plugin-hashtag": "npm:linkify-plugin-hashtag@^4.1.1",
"linkify-string": "npm:linkify-string@^4.1.1", "linkify-string": "npm:linkify-string@^4.1.1",
@ -60,7 +61,6 @@
"nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-relaypool": "npm:nostr-relaypool2@0.6.34",
"nostr-tools": "npm:nostr-tools@2.5.1", "nostr-tools": "npm:nostr-tools@2.5.1",
"nostr-wasm": "npm:nostr-wasm@^0.1.0", "nostr-wasm": "npm:nostr-wasm@^0.1.0",
"postgres": "https://deno.land/x/postgres@v0.19.0/mod.ts",
"prom-client": "npm:prom-client@^15.1.2", "prom-client": "npm:prom-client@^15.1.2",
"question-deno": "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/mod.ts", "question-deno": "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/mod.ts",
"tldts": "npm:tldts@^6.0.14", "tldts": "npm:tldts@^6.0.14",

339
deno.lock generated
View file

@ -8,11 +8,11 @@
"jsr:@gleasonator/policy": "jsr:@gleasonator/policy@0.2.0", "jsr:@gleasonator/policy": "jsr:@gleasonator/policy@0.2.0",
"jsr:@gleasonator/policy@0.2.0": "jsr:@gleasonator/policy@0.2.0", "jsr:@gleasonator/policy@0.2.0": "jsr:@gleasonator/policy@0.2.0",
"jsr:@gleasonator/policy@0.4.0": "jsr:@gleasonator/policy@0.4.0", "jsr:@gleasonator/policy@0.4.0": "jsr:@gleasonator/policy@0.4.0",
"jsr:@hono/hono@^4.4.6": "jsr:@hono/hono@4.4.6", "jsr:@hono/hono@^4.4.6": "jsr:@hono/hono@4.5.0",
"jsr:@nostrify/nostrify@^0.22.1": "jsr:@nostrify/nostrify@0.22.5", "jsr:@nostrify/nostrify@^0.22.1": "jsr:@nostrify/nostrify@0.22.5",
"jsr:@nostrify/nostrify@^0.22.4": "jsr:@nostrify/nostrify@0.22.4", "jsr:@nostrify/nostrify@^0.22.4": "jsr:@nostrify/nostrify@0.22.4",
"jsr:@nostrify/nostrify@^0.22.5": "jsr:@nostrify/nostrify@0.22.5", "jsr:@nostrify/nostrify@^0.22.5": "jsr:@nostrify/nostrify@0.22.5",
"jsr:@nostrify/nostrify@^0.25.0": "jsr:@nostrify/nostrify@0.25.0", "jsr:@nostrify/nostrify@^0.26.3": "jsr:@nostrify/nostrify@0.26.3",
"jsr:@soapbox/kysely-deno-sqlite@^2.1.0": "jsr:@soapbox/kysely-deno-sqlite@2.2.0", "jsr:@soapbox/kysely-deno-sqlite@^2.1.0": "jsr:@soapbox/kysely-deno-sqlite@2.2.0",
"jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0",
"jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0", "jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0",
@ -21,8 +21,9 @@
"jsr:@std/assert@^0.225.1": "jsr:@std/assert@0.225.3", "jsr:@std/assert@^0.225.1": "jsr:@std/assert@0.225.3",
"jsr:@std/bytes@^0.224.0": "jsr:@std/bytes@0.224.0", "jsr:@std/bytes@^0.224.0": "jsr:@std/bytes@0.224.0",
"jsr:@std/bytes@^1.0.0-rc.3": "jsr:@std/bytes@1.0.0", "jsr:@std/bytes@^1.0.0-rc.3": "jsr:@std/bytes@1.0.0",
"jsr:@std/bytes@^1.0.1-rc.3": "jsr:@std/bytes@1.0.2",
"jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0",
"jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.0", "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.2",
"jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0",
"jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3", "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3",
"jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3", "jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3",
@ -30,7 +31,7 @@
"jsr:@std/fs@^0.221.0": "jsr:@std/fs@0.221.0", "jsr:@std/fs@^0.221.0": "jsr:@std/fs@0.221.0",
"jsr:@std/fs@^0.229.3": "jsr:@std/fs@0.229.3", "jsr:@std/fs@^0.229.3": "jsr:@std/fs@0.229.3",
"jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0", "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0",
"jsr:@std/io@^0.224": "jsr:@std/io@0.224.1", "jsr:@std/io@^0.224": "jsr:@std/io@0.224.3",
"jsr:@std/media-types@^0.224.1": "jsr:@std/media-types@0.224.1", "jsr:@std/media-types@^0.224.1": "jsr:@std/media-types@0.224.1",
"jsr:@std/path@0.217": "jsr:@std/path@0.217.0", "jsr:@std/path@0.217": "jsr:@std/path@0.217.0",
"jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0", "jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0",
@ -47,6 +48,7 @@
"npm:hono-rate-limiter@^0.3.0": "npm:hono-rate-limiter@0.3.0_hono@4.2.5", "npm:hono-rate-limiter@^0.3.0": "npm:hono-rate-limiter@0.3.0_hono@4.2.5",
"npm:iso-639-1@2.1.15": "npm:iso-639-1@2.1.15", "npm:iso-639-1@2.1.15": "npm:iso-639-1@2.1.15",
"npm:isomorphic-dompurify@^2.11.0": "npm:isomorphic-dompurify@2.11.0", "npm:isomorphic-dompurify@^2.11.0": "npm:isomorphic-dompurify@2.11.0",
"npm:kysely-postgres-js@2.0.0": "npm:kysely-postgres-js@2.0.0_kysely@0.27.3_postgres@3.4.4",
"npm:kysely@0.27.3": "npm:kysely@0.27.3", "npm:kysely@0.27.3": "npm:kysely@0.27.3",
"npm:kysely@^0.27.2": "npm:kysely@0.27.3", "npm:kysely@^0.27.2": "npm:kysely@0.27.3",
"npm:kysely@^0.27.3": "npm:kysely@0.27.3", "npm:kysely@^0.27.3": "npm:kysely@0.27.3",
@ -62,6 +64,7 @@
"npm:nostr-tools@^2.5.0": "npm:nostr-tools@2.5.1", "npm:nostr-tools@^2.5.0": "npm:nostr-tools@2.5.1",
"npm:nostr-tools@^2.7.0": "npm:nostr-tools@2.7.0", "npm:nostr-tools@^2.7.0": "npm:nostr-tools@2.7.0",
"npm:nostr-wasm@^0.1.0": "npm:nostr-wasm@0.1.0", "npm:nostr-wasm@^0.1.0": "npm:nostr-wasm@0.1.0",
"npm:postgres@3.4.4": "npm:postgres@3.4.4",
"npm:prom-client@^15.1.2": "npm:prom-client@15.1.2", "npm:prom-client@^15.1.2": "npm:prom-client@15.1.2",
"npm:tldts@^6.0.14": "npm:tldts@6.1.18", "npm:tldts@^6.0.14": "npm:tldts@6.1.18",
"npm:type-fest@^4.3.0": "npm:type-fest@4.18.2", "npm:type-fest@^4.3.0": "npm:type-fest@4.18.2",
@ -107,6 +110,9 @@
"@hono/hono@4.4.6": { "@hono/hono@4.4.6": {
"integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453" "integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453"
}, },
"@hono/hono@4.5.0": {
"integrity": "4a410f7773ac4b5b0eb4520b26c7ab7795a271d57a9df7fa1953ded6b90ccaf7"
},
"@nostrify/nostrify@0.22.4": { "@nostrify/nostrify@0.22.4": {
"integrity": "1c8a7847e5773213044b491e85fd7cafae2ad194ce59da4d957d2b27c776b42d", "integrity": "1c8a7847e5773213044b491e85fd7cafae2ad194ce59da4d957d2b27c776b42d",
"dependencies": [ "dependencies": [
@ -136,9 +142,10 @@
"npm:zod@^3.23.8" "npm:zod@^3.23.8"
] ]
}, },
"@nostrify/nostrify@0.25.0": { "@nostrify/nostrify@0.26.3": {
"integrity": "98f26f44e95ac87fc91b3f3809d38432e1a7f6aebf10380b2554b6f9526313c6", "integrity": "3e13e30f4fa3f76dcbcf9178630a9b2871186eb1d226d66234c0cdfd4841f548",
"dependencies": [ "dependencies": [
"jsr:@std/crypto@^0.224.0",
"jsr:@std/encoding@^0.224.1", "jsr:@std/encoding@^0.224.1",
"npm:@scure/base@^1.1.6", "npm:@scure/base@^1.1.6",
"npm:@scure/bip32@^1.4.0", "npm:@scure/bip32@^1.4.0",
@ -180,6 +187,9 @@
"@std/bytes@1.0.0": { "@std/bytes@1.0.0": {
"integrity": "9392e72af80adccaa1197912fa19990ed091cb98d5c9c4344b0c301b22d7c632" "integrity": "9392e72af80adccaa1197912fa19990ed091cb98d5c9c4344b0c301b22d7c632"
}, },
"@std/bytes@1.0.2": {
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
},
"@std/crypto@0.224.0": { "@std/crypto@0.224.0": {
"integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d",
"dependencies": [ "dependencies": [
@ -190,6 +200,9 @@
"@std/dotenv@0.224.0": { "@std/dotenv@0.224.0": {
"integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d" "integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d"
}, },
"@std/dotenv@0.224.2": {
"integrity": "29081695357e4534696c9e986b2560be29c141ccf52daa32b6c20ff5b5c64ab9"
},
"@std/encoding@0.221.0": { "@std/encoding@0.221.0": {
"integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45"
}, },
@ -224,6 +237,12 @@
"jsr:@std/bytes@^1.0.0-rc.3" "jsr:@std/bytes@^1.0.0-rc.3"
] ]
}, },
"@std/io@0.224.3": {
"integrity": "b402edeb99c6b3778d9ae3e9927bc9085b170b41e5a09bbb7064ab2ee394ae2f",
"dependencies": [
"jsr:@std/bytes@^1.0.1-rc.3"
]
},
"@std/media-types@0.224.1": { "@std/media-types@0.224.1": {
"integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1" "integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1"
}, },
@ -664,6 +683,13 @@
"xml-name-validator": "xml-name-validator@5.0.0" "xml-name-validator": "xml-name-validator@5.0.0"
} }
}, },
"kysely-postgres-js@2.0.0_kysely@0.27.3_postgres@3.4.4": {
"integrity": "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ==",
"dependencies": {
"kysely": "kysely@0.27.3",
"postgres": "postgres@3.4.4"
}
},
"kysely@0.27.3": { "kysely@0.27.3": {
"integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==",
"dependencies": {} "dependencies": {}
@ -868,6 +894,10 @@
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dependencies": {} "dependencies": {}
}, },
"postgres@3.4.4": {
"integrity": "sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==",
"dependencies": {}
},
"prom-client@15.1.2": { "prom-client@15.1.2": {
"integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==", "integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==",
"dependencies": { "dependencies": {
@ -1126,7 +1156,277 @@
} }
} }
}, },
"redirects": {
"https://github.com/xyzshantaram/postgres.js/raw/master/deno/mod.js": "https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/mod.js"
},
"remote": { "remote": {
"https://deno.land/std@0.132.0/_deno_unstable.ts": "23a1a36928f1b6d3b0170aaa67de09af12aa998525f608ff7331b9fb364cbde6",
"https://deno.land/std@0.132.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
"https://deno.land/std@0.132.0/_util/os.ts": "49b92edea1e82ba295ec946de8ffd956ed123e2948d9bd1d3e901b04e4307617",
"https://deno.land/std@0.132.0/_wasm_crypto/crypto.mjs": "3b383eb715e8bfe61b4450ef0644b2653429c88d494807c86c5235979f62e56b",
"https://deno.land/std@0.132.0/_wasm_crypto/crypto.wasm.mjs": "0ad9ecc0d03ca8a083d9109db22e7507f019f63cf55b82ea618ab58855617577",
"https://deno.land/std@0.132.0/_wasm_crypto/mod.ts": "30a93c8b6b6c5b269e96a3e95d2c045d86a496814a8737443b77cad941d6a0b5",
"https://deno.land/std@0.132.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06",
"https://deno.land/std@0.132.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0",
"https://deno.land/std@0.132.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620",
"https://deno.land/std@0.132.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a",
"https://deno.land/std@0.132.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c",
"https://deno.land/std@0.132.0/async/mod.ts": "2240c6841157738414331f47dee09bb8c0482c5b1980b6e3234dd03515c8132f",
"https://deno.land/std@0.132.0/async/mux_async_iterator.ts": "f4d1d259b0c694d381770ddaaa4b799a94843eba80c17f4a2ec2949168e52d1e",
"https://deno.land/std@0.132.0/async/pool.ts": "97b0dd27c69544e374df857a40902e74e39532f226005543eabacb551e277082",
"https://deno.land/std@0.132.0/async/tee.ts": "1341feb1f5b1a96f8628d0f8fc07d8c43d3813423f18a63bf1b4785568d21b1f",
"https://deno.land/std@0.132.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
"https://deno.land/std@0.132.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
"https://deno.land/std@0.132.0/bytes/mod.ts": "d3b455c0dbd4804644159d1e25946ade5ee385d2359894de49e2c6101b18b7a9",
"https://deno.land/std@0.132.0/encoding/base64.ts": "c8c16b4adaa60d7a8eee047c73ece26844435e8f7f1328d74593dbb2dd58ea4f",
"https://deno.land/std@0.132.0/encoding/base64url.ts": "55f9d13df02efac10c6f96169daa3e702606a64e8aa27c0295f645f198c27130",
"https://deno.land/std@0.132.0/encoding/hex.ts": "7f023e1e51cfd6b189682e602e8640939e7be71a300a2fcf3daf8f84dc609bbc",
"https://deno.land/std@0.132.0/flags/mod.ts": "430cf2d1c26e00286373b2647ebdca637f7558505e88e9c108a4742cd184c916",
"https://deno.land/std@0.132.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
"https://deno.land/std@0.132.0/fmt/printf.ts": "e2c0f72146aed1efecf0c39ab928b26ae493a2278f670a871a0fbdcf36ff3379",
"https://deno.land/std@0.132.0/fs/eol.ts": "b92f0b88036de507e7e6fbedbe8f666835ea9dcbf5ac85917fa1fadc919f83a5",
"https://deno.land/std@0.132.0/fs/exists.ts": "cb734d872f8554ea40b8bff77ad33d4143c1187eac621a55bf37781a43c56f6d",
"https://deno.land/std@0.132.0/hash/sha256.ts": "803846c7a5a8a5a97f31defeb37d72f519086c880837129934f5d6f72102a8e8",
"https://deno.land/std@0.132.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
"https://deno.land/std@0.132.0/node/_buffer.mjs": "f4a7df481d4eed06dc0151b833177d8ef74fc3a96dd4d2b073e690b6ced9474d",
"https://deno.land/std@0.132.0/node/_core.ts": "568d277be2e086af996cbdd599fec569f5280e9a494335ca23ad392b130d7bb9",
"https://deno.land/std@0.132.0/node/_crypto/constants.ts": "49011c87be4e45407ef5e99e96bde3f08656ebd8e6dfc99048c703dd0ce53952",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/base/buffer.js": "73beb8294eb29bd61458bbaaeeb51dfad4ec9c9868a62207a061d908f1637261",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/base/node.js": "4b777980d2a23088698fd2ff065bb311a2c713497d359e674cb6ef6baf267a0f",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/base/reporter.js": "8e4886e8ae311c9a92caf58bbbd8670326ceeae97430f4884e558e4acf8e8598",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/constants/der.js": "354b255479bff22a31d25bf08b217a295071700e37d0991cc05cac9f95e5e7ca",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/decoders/der.js": "c6faf66761daa43fbf79221308443893587c317774047b508a04c570713b76fb",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/decoders/pem.js": "8316ef7ce2ce478bc3dc1e9df1b75225d1eb8fb5d1378f8adf0cf19ecea5b501",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/encoders/der.js": "408336c88d17c5605ea64081261cf42267d8f9fda90098cb560aa6635bb00877",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/encoders/pem.js": "42a00c925b68c0858d6de0ba41ab89935b39fae9117bbf72a9abb2f4b755a2e7",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/asn1.js/mod.js": "7b78859707be10a0a1e4faccdd28cd5a4f71ad74a3e7bebda030757da97cd232",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/bn.js/bn.js": "abd1badd659fd0ae54e6a421a573a25aef4e795edc392178360cf716b144286d",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/aes.js": "1cf4c354c5bb341ffc9ab7207f471229835b021947225bce2e1642f26643847a",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/auth_cipher.js": "19b4dbb903e8406eb733176e6318d5e1a3bd382b67b72f7cf8e1c46cc6321ba4",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/decrypter.js": "05c1676942fd8e95837115bc2d1371bcf62e9bf19f6c3348870961fc64ddad0b",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/encrypter.js": "93ec98ab26fbeb5969eae2943e42fb66780f377b9b0ff0ecc32a9ed11201b142",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/ghash.js": "667b64845764a84f0096ef8cf7debed1a5f15ac9af26b379848237be57da399a",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/incr32.js": "4a7f0107753e4390b4ccc4dbd5200c5527d43f894f768e131903df30a09dfd67",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/mod.js": "d8eb88e7a317467831473621f32e60d7db9d981f6a2ae45d2fb2af170eab2d22",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cbc.js": "9790799cff181a074686c885708cb8eb473aeb3c86ff2e8d0ff911ae6c1e4431",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cfb.js": "a4e36ede6f26d8559d8f0528a134592761c706145a641bd9ad1100763e831cdb",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cfb1.js": "c6372f4973a68ca742682e81d1165e8869aaabf0091a8b963d4d60e5ee8e6f6a",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/cfb8.js": "bd29eebb89199b056ff2441f7fb5e0300f458e13dcaaddbb8bc00cbdb199db67",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/ctr.js": "9c2cbac1fc8f9b58334faacb98e6c57e8c3712f673ea4cf2d528a2894998ab2f",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/ecb.js": "9629d193433688f0cfc432eca52838db0fb28d9eb4f45563df952bde50b59763",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/mod.js": "7d8516ef8a20565539eb17cad5bb70add02ac06d1891e8f47cb981c22821787e",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/modes/ofb.js": "c23abaa6f1ec5343e9d7ba61d702acb3d81a0bd3d34dd2004e36975dc043d6ff",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/stream_cipher.js": "a533a03a2214c6b5934ce85a59eb1e04239fd6f429017c7ca3c443ec7e07e68f",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_aes/xor.ts": "4417711c026eb9a07475067cd31fa601e88c2d6ababd606d33d1e74da6fcfd09",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/browserify_rsa.js": "de8c98d2379a70d8c239b4886e2b3a11c7204eec39ae6b65d978d0d516ee6b08",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/cipher_base.js": "f565ad9daf3b3dd3b68381bed848da94fb093a9e4e5a48c92f47e26cc229df39",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/evp_bytes_to_key.ts": "8bd9fa445576b3e39586bdbef7c907f1dfda201bf22602d2ca1c6d760366311e",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/asn1.js": "4f33b0197ffbe9cff62e5bad266e6b40d55874ea653552bb32ed251ad091f70a",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/certificate.js": "aab306870830a81ad188db8fa8e037d7f5dd6c5abdabbd9739558245d1a12224",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/fix_proc.js": "af3052b76f441878e102ffcfc7420692e65777af765e96f786310ae1acf7f76a",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/parse_asn1/mod.js": "e923a13b27089a99eeb578d2ffb9b4cfe8ce690918fec05d0024fa126f3e1ce3",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/mgf.js": "5b81dc1680829b564fc5a00b842fb9c88916e4639b4fa27fa8bb6b759d272371",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/mod.js": "eb8b64d7a58ee3823c1b642e799cc7ed1257d99f4d4aefa2b4796dd112ec094a",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/private_decrypt.js": "0050df879f7c1338132c45056835f64e32140e2a2d5d03c1366ccce64855f632",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/public_encrypt.js": "0132cb4fb8f72593278474884195b9c52b4e9ba33d8ddd22116d07a07f47005a",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/with_public.js": "7373dac9b53b8331ccf3521c854a131dcb304a2e4d34cd116649118f7919ed0c",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/public_encrypt/xor.js": "900c6fc8b95e1861d796193c41988f5f70a09c7059e42887a243d0113ecaf0fd",
"https://deno.land/std@0.132.0/node/_crypto/crypto_browserify/randombytes.ts": "f465cd8e114a3c110297e0143445b12125d729b25bada5bd88d5b30cf612d7dd",
"https://deno.land/std@0.132.0/node/_crypto/hash.ts": "6a84a079412d09ead27b900590f0bede9924bc7ce522b8b7d55183a2aaf63a68",
"https://deno.land/std@0.132.0/node/_crypto/pbkdf2.ts": "00af38578729b3060371dfee70dae502a5848b4cc4787c48f634195cab1ce89a",
"https://deno.land/std@0.132.0/node/_crypto/randomBytes.ts": "04e276bcbfa55b3502c7169deab3f2bf58bbc5e9727634f8a150eff734338e90",
"https://deno.land/std@0.132.0/node/_crypto/randomFill.ts": "019ff2a8330c3ede6e65af28c5a8e3dee9d404975749c8dadf6ba11ccc28528e",
"https://deno.land/std@0.132.0/node/_crypto/randomInt.ts": "2db981c2baf4ddac07b6da71f90677f4acf4dc2d93f351563fdd084d645b8413",
"https://deno.land/std@0.132.0/node/_crypto/scrypt.ts": "caf07a6b8afa6ac582f80f99ed7dc7fefd5476fcd2aad771b8440e5b883d6d70",
"https://deno.land/std@0.132.0/node/_crypto/timingSafeEqual.ts": "4a4ef17e482889d9d82138d5ffc0e787c32c04b1f12b28d076b1a69ceca46af1",
"https://deno.land/std@0.132.0/node/_crypto/types.ts": "d3fae758c5b62f63d8126c76eec31a5559a2f34305defb5fe2a7d9034057ff54",
"https://deno.land/std@0.132.0/node/_dns/_utils.ts": "42494c8b8fa1c13eb134c5696744f77197717fa857e4d05147f2395a739b0a40",
"https://deno.land/std@0.132.0/node/_events.mjs": "d7d56df4b9f69e445064bad5e5558978fb18c18c439bbb62fa13149b40d7fb99",
"https://deno.land/std@0.132.0/node/_fixed_queue.ts": "455b3c484de48e810b13bdf95cd1658ecb1ba6bcb8b9315ffe994efcde3ba5f5",
"https://deno.land/std@0.132.0/node/_fs/_fs_access.ts": "0700488320d37208d000b94767ab37208d469550767edab69b65b09a330f245d",
"https://deno.land/std@0.132.0/node/_fs/_fs_appendFile.ts": "5dca59d7f2ec33316d75da3d6a12d39f5c35b429bddf83f4b2c030b3a289d4b3",
"https://deno.land/std@0.132.0/node/_fs/_fs_chmod.ts": "8fc25677b82a2643686e6f8270a8f1bee87dd60986334591450699e199dac7d5",
"https://deno.land/std@0.132.0/node/_fs/_fs_chown.ts": "57858c54d376648fc3c8cf5a8ad4f7f19fb153b75fac3ed41df0332d757e7de9",
"https://deno.land/std@0.132.0/node/_fs/_fs_close.ts": "785a9d1a6d615e8aa9f5a4ac50c9a131931f8b0e17b3d4671cd1fd25a5c10f2b",
"https://deno.land/std@0.132.0/node/_fs/_fs_common.ts": "6a373d1583d9ec5cc7a8ff1072d77dc999e35282a320b7477038a2b209c304d3",
"https://deno.land/std@0.132.0/node/_fs/_fs_constants.ts": "5c20b190fc6b7cfdaf12a30ba545fc787db2c7bbe87ed5b890da99578116a339",
"https://deno.land/std@0.132.0/node/_fs/_fs_copy.ts": "675eb02a2dfc20dab1186bf6ed0a33b1abae656f440bc0a3ce74f385e0052eef",
"https://deno.land/std@0.132.0/node/_fs/_fs_dir.ts": "8a05f72e32dd568b41ef45f8f55f1f54e9a306a7588475fa7014289cd12872d9",
"https://deno.land/std@0.132.0/node/_fs/_fs_dirent.ts": "649c0a794e7b8d930cdd7e6a168b404aa0448bf784e0cfbe1bd6d25b99052273",
"https://deno.land/std@0.132.0/node/_fs/_fs_exists.ts": "83e9ca6ea1ab3c6c7c3fc45f3c1287ee88839f08140ac11056441537450055bb",
"https://deno.land/std@0.132.0/node/_fs/_fs_fdatasync.ts": "bbd078fea6c62c64d898101d697aefbfbb722797a75e328a82c2a4f2e7eb963d",
"https://deno.land/std@0.132.0/node/_fs/_fs_fstat.ts": "559ff6ff094337db37b0f3108aeaecf42672795af45b206adbd90105afebf9c6",
"https://deno.land/std@0.132.0/node/_fs/_fs_fsync.ts": "590be69ce5363dd4f8867f244cfabe8df89d40f86bbbe44fd00d69411d0b798e",
"https://deno.land/std@0.132.0/node/_fs/_fs_ftruncate.ts": "8eb2a9fcf026bd9b85dc07a22bc452c48db4be05ab83f5f2b6a0549e15c1f75f",
"https://deno.land/std@0.132.0/node/_fs/_fs_futimes.ts": "c753cb9e9f129a11d1110ed43905b8966ac2a1d362ed69d5a34bb44513b00082",
"https://deno.land/std@0.132.0/node/_fs/_fs_link.ts": "3f9ccce31c2e56284fbcf2c65ec2e6fed1d9e67a9997410223486ac5092888e3",
"https://deno.land/std@0.132.0/node/_fs/_fs_lstat.ts": "571cea559d270e3b2e7fc585b0eb051899f6d0e54b1786f5e2cee3e9f71e7f27",
"https://deno.land/std@0.132.0/node/_fs/_fs_mkdir.ts": "68421a23b6d3c2d0142a6d0b3ccdd87903f9c8f98d6754aba554ab4c6b435bb8",
"https://deno.land/std@0.132.0/node/_fs/_fs_mkdtemp.ts": "86eaec96c63ea178c749fa856115a345e9797baecad22297b9ef98e3d62b90e2",
"https://deno.land/std@0.132.0/node/_fs/_fs_open.ts": "b1ca72addd2723b2a5a876378e72609fbe168adad2006f5d7b4f1868beef65ca",
"https://deno.land/std@0.132.0/node/_fs/_fs_read.ts": "3b4ef96aad20f3f29a859125ebeac8c9461574743f70c2a7ef301b8505f7d036",
"https://deno.land/std@0.132.0/node/_fs/_fs_readFile.ts": "3eae6c930e08c1279d400c0f5a008e6d96949ff3a4f5bf7d43e1b94b94ce3854",
"https://deno.land/std@0.132.0/node/_fs/_fs_readdir.ts": "a546f01387b7c49ddc1bd78d0e123a9668c710c56cffb4d9577ef46703cab463",
"https://deno.land/std@0.132.0/node/_fs/_fs_readlink.ts": "00553cd155f3bea565ffe43d7f0c10d75e895455562e1e8ea153e8f4e7ac04c7",
"https://deno.land/std@0.132.0/node/_fs/_fs_realpath.ts": "3ec236e4ad3c171203043422939973b6a948200ec4802425db41fa60c860dde9",
"https://deno.land/std@0.132.0/node/_fs/_fs_rename.ts": "3be71e8f43275c349b7abb9343b6e6764df09fabcbd2d316f8ac170ea556c645",
"https://deno.land/std@0.132.0/node/_fs/_fs_rm.ts": "a9328f99d925d7c74d31361d466ca33475aa7c6d1d6f037a49ce1ed996f0a0b4",
"https://deno.land/std@0.132.0/node/_fs/_fs_rmdir.ts": "b74007891357e709b37e6721eb355a1c4f25575995bb7c961a3c40f03ebc624c",
"https://deno.land/std@0.132.0/node/_fs/_fs_stat.ts": "bd47ce0bfc2b867392abc6ec95878ab4f6dddb94af73903d6fa1a02ba3e26af8",
"https://deno.land/std@0.132.0/node/_fs/_fs_streams.ts": "0e54bd4e41b462a701d6729ea17db01624aa48109e402fea8eecf13be324cf16",
"https://deno.land/std@0.132.0/node/_fs/_fs_symlink.ts": "0bddc37c5092f847634bd41cee0b643b9c03fc541c0e635cf35da1fcb4d0f7fa",
"https://deno.land/std@0.132.0/node/_fs/_fs_truncate.ts": "e2d380f7a81f69c4d4db30c442558ba8d8dea561e5097af41022bb5724e494e5",
"https://deno.land/std@0.132.0/node/_fs/_fs_unlink.ts": "c537ca98e507972d65f0b113a179b5f5083f0da3e6f9fae29895fd2a9660c18a",
"https://deno.land/std@0.132.0/node/_fs/_fs_utimes.ts": "c4446b7e39bf6977eca4364360501a97b96db9ea41e0cdf49abddab73481a175",
"https://deno.land/std@0.132.0/node/_fs/_fs_watch.ts": "2338de777458021d39cb9f0a5f3ea1bd9109a7ca2c2ad6ec41029df1753838f8",
"https://deno.land/std@0.132.0/node/_fs/_fs_write.mjs": "8c130b8b9522e1e4b08e687eb27939240260c115fda1e38e99c57b4f3af6481f",
"https://deno.land/std@0.132.0/node/_fs/_fs_writeFile.ts": "79d176021c8ceae0d956763a33834166ebc3f1691ed9219a21674b2374f115c3",
"https://deno.land/std@0.132.0/node/_fs/_fs_writev.mjs": "274df0a109010862c8f8b320dc7784de9bd9425fe2a6afd05f1f06f547a25cba",
"https://deno.land/std@0.132.0/node/_next_tick.ts": "64c361f6bca21df2a72dd77b84bd49d80d97a694dd3080703bc78f52146351d1",
"https://deno.land/std@0.132.0/node/_options.ts": "27f3c1269a700d330cc046cf748aa9178b8fc39d1473de625688e07cb0eb9d28",
"https://deno.land/std@0.132.0/node/_process/exiting.ts": "bc9694769139ffc596f962087155a8bfef10101d03423b9dcbc51ce6e1f88fce",
"https://deno.land/std@0.132.0/node/_process/process.ts": "84644b184053835670f79652d1ce3312c9ad079c211e6207ebefeedf159352a3",
"https://deno.land/std@0.132.0/node/_process/stdio.mjs": "971c3b086040d8521562155db13f22f9971d5c42c852b2081d4d2f0d8b6ab6bd",
"https://deno.land/std@0.132.0/node/_process/streams.mjs": "555062e177ad05f887147651fdda25fa55098475fcf142c8d162b8fe14097bbb",
"https://deno.land/std@0.132.0/node/_stream.mjs": "07f6cbabaad0382fb4b9a25e70ac3093a44022b859247f64726746e6373f1c91",
"https://deno.land/std@0.132.0/node/_util/_util_callbackify.ts": "79928ad80df3e469f7dcdb198118a7436d18a9f6c08bd7a4382332ad25a718cf",
"https://deno.land/std@0.132.0/node/_utils.ts": "c2c352e83c4c96f5ff994b1c8246bff2abcb21bfc3f1c06162cb3af1d201e615",
"https://deno.land/std@0.132.0/node/buffer.ts": "fbecbf3f237fa49bec96e97ecf56a7b92d48037b3d11219288e68943cc921600",
"https://deno.land/std@0.132.0/node/crypto.ts": "fffbc3fc3dcc16ea986d3e89eed5f70db7dfef2c18d1205a8c8fe5327ee0192d",
"https://deno.land/std@0.132.0/node/dns.ts": "ae2abd1bc8ac79543fe4d702f2aa3607101dc788b6eeba06e06436cb42ee3779",
"https://deno.land/std@0.132.0/node/events.ts": "a1d40fc0dbccc944379ef968b80ea08f9fce579e88b5057fdb64e4f0812476dd",
"https://deno.land/std@0.132.0/node/fs.ts": "21a3189c460bd37ac3f6734e040587125b7c8435c0a9da4e6c57544a3aca81c2",
"https://deno.land/std@0.132.0/node/internal/assert.mjs": "118327c8866266534b30d3a36ad978204af7336dc2db3158b8167192918d4e06",
"https://deno.land/std@0.132.0/node/internal/async_hooks.ts": "8eca5b80f58ffb259e9b3a73536dc2fe2e67d07fd24bfe2aee325a4aa435edb3",
"https://deno.land/std@0.132.0/node/internal/blob.mjs": "52080b2f40b114203df67f8a6650f9fe3c653912b8b3ef2f31f029853df4db53",
"https://deno.land/std@0.132.0/node/internal/buffer.mjs": "6662fe7fe517329453545be34cea27a24f8ccd6d09afd4f609f11ade2b6dfca7",
"https://deno.land/std@0.132.0/node/internal/crypto/keys.ts": "16ce7b15a9fc5e4e3dee8fde75dae12f3d722558d5a1a6e65a9b4f86d64a21e9",
"https://deno.land/std@0.132.0/node/internal/crypto/util.mjs": "1de55a47fdbed6721b467a77ba48fdd1550c10b5eee77bbdb602eaffee365a5e",
"https://deno.land/std@0.132.0/node/internal/dtrace.ts": "50dd0e77b0269e47ff673bdb9ad0ef0ea3a3c53ac30c1695883ce4748e04ca14",
"https://deno.land/std@0.132.0/node/internal/error_codes.ts": "ac03c4eae33de3a69d6c98e8678003207eecf75a6900eb847e3fea3c8c9e6d8f",
"https://deno.land/std@0.132.0/node/internal/errors.ts": "25f91691225b001660e6e64745ecd336fbf562cf0185e8896ff013c2d0226794",
"https://deno.land/std@0.132.0/node/internal/fs/streams.ts": "c925db185efdf56c35cde8270c07d61698b80603a90e07caf1cb4ff80abf195b",
"https://deno.land/std@0.132.0/node/internal/fs/utils.mjs": "2a571ecbd169b444f07b7193306f108fdcb4bfd9b394b33716ad05edf30e899e",
"https://deno.land/std@0.132.0/node/internal/hide_stack_frames.ts": "a91962ec84610bc7ec86022c4593cdf688156a5910c07b5bcd71994225c13a03",
"https://deno.land/std@0.132.0/node/internal/idna.ts": "a8bdd28431f06630d8aad85d3cb8fd862459107af228c8805373ad2080f1c587",
"https://deno.land/std@0.132.0/node/internal/net.ts": "1239886cd2508a68624c2dae8abf895e8aa3bb15a748955349f9ac5539032238",
"https://deno.land/std@0.132.0/node/internal/normalize_encoding.mjs": "3779ec8a7adf5d963b0224f9b85d1bc974a2ec2db0e858396b5d3c2c92138a0a",
"https://deno.land/std@0.132.0/node/internal/process/per_thread.mjs": "a42b1dcfb009ad5039144a999a35a429e76112f9322febbe353eda9d1879d936",
"https://deno.land/std@0.132.0/node/internal/querystring.ts": "c3b23674a379f696e505606ddce9c6feabe9fc497b280c56705c340f4028fe74",
"https://deno.land/std@0.132.0/node/internal/stream_base_commons.ts": "934a9e69f46f2de644956edfa9fb040af7861e326fe5325dab38ef9caf2940bc",
"https://deno.land/std@0.132.0/node/internal/streams/_utils.ts": "77fceaa766679847e4d4c3c96b2573c00a790298d90551e8e4df1d5e0fdaad3b",
"https://deno.land/std@0.132.0/node/internal/streams/add-abort-signal.mjs": "5623b83fa64d439cc4a1f09ae47ec1db29512cc03479389614d8f62a37902f5e",
"https://deno.land/std@0.132.0/node/internal/streams/buffer_list.mjs": "c6a7b29204fae025ff5e9383332acaea5d44bc7c522a407a79b8f7a6bc6c312d",
"https://deno.land/std@0.132.0/node/internal/streams/compose.mjs": "b522daab35a80ae62296012a4254fd7edfc0366080ffe63ddda4e38fe6b6803e",
"https://deno.land/std@0.132.0/node/internal/streams/destroy.mjs": "9c9bbeb172a437041d529829f433df72cf0b63ae49f3ee6080a55ffbef7572ad",
"https://deno.land/std@0.132.0/node/internal/streams/duplex.mjs": "b014087cd04f79b8a4028d8b9423b987e07bbfacf3b5df518cb752ac3657580f",
"https://deno.land/std@0.132.0/node/internal/streams/end-of-stream.mjs": "38be76eaceac231dfde643e72bc0940625446bf6d1dbd995c91c5ba9fd59b338",
"https://deno.land/std@0.132.0/node/internal/streams/from.mjs": "134255c698ed63b33199911eb8e042f8f67e9682409bb11552e6120041ed1872",
"https://deno.land/std@0.132.0/node/internal/streams/legacy.mjs": "6ea28db95d4503447473e62f0b23ff473bfe1751223c33a3c5816652e93b257a",
"https://deno.land/std@0.132.0/node/internal/streams/passthrough.mjs": "a51074193b959f3103d94de41e23a78dfcff532bdba53af9146b86340d85eded",
"https://deno.land/std@0.132.0/node/internal/streams/pipeline.mjs": "9890b121759ede869174ef70c011fde964ca94d81f2ed97b8622d7cb17b49285",
"https://deno.land/std@0.132.0/node/internal/streams/readable.mjs": "a70c41171ae25c556b52785b0c178328014bd33d8c0e4d229d4adaac7414b6ca",
"https://deno.land/std@0.132.0/node/internal/streams/state.mjs": "9ef917392a9d8005a6e038260c5fd31518d2753aea0bc9e39824c199310434cb",
"https://deno.land/std@0.132.0/node/internal/streams/transform.mjs": "3b361abad2ac78f7ccb6f305012bafdc0e983dfa4bb6ecddb4626e34a781a5f5",
"https://deno.land/std@0.132.0/node/internal/streams/utils.mjs": "06c21d0db0d51f1bf1e3225a661c3c29909be80355d268e64ee5922fc5eb6c5e",
"https://deno.land/std@0.132.0/node/internal/streams/writable.mjs": "ad4e2b176ffdf752c8e678ead3a514679a5a8d652f4acf797115dceb798744d5",
"https://deno.land/std@0.132.0/node/internal/timers.mjs": "b43e24580cec2dd50f795e4342251a79515c0db21630c25b40fdc380a78b74e7",
"https://deno.land/std@0.132.0/node/internal/url.ts": "eacef0ace4f4c5394e9818a81499f4871b2a993d1bd3b902047e44a381ef0e22",
"https://deno.land/std@0.132.0/node/internal/util.mjs": "2f0c8ff553c175ea6e4ed13d7cd7cd6b86dc093dc2f783c6c3dfc63f60a0943e",
"https://deno.land/std@0.132.0/node/internal/util/comparisons.ts": "680b55fe8bdf1613633bc469fa0440f43162c76dbe36af9aa2966310e1bb9f6e",
"https://deno.land/std@0.132.0/node/internal/util/debuglog.ts": "99e91bdf26f6c67861031f684817e1705a5bc300e81346585b396f413387edfb",
"https://deno.land/std@0.132.0/node/internal/util/inspect.mjs": "d1c2569c66a3dab45eec03208f22ad4351482527859c0011a28a6c797288a0aa",
"https://deno.land/std@0.132.0/node/internal/util/types.ts": "b2dacb8f1f5d28a51c4da5c5b75172b7fcf694073ce95ca141323657e18b0c60",
"https://deno.land/std@0.132.0/node/internal/validators.mjs": "a7e82eafb7deb85c332d5f8d9ffef052f46a42d4a121eada4a54232451acc49a",
"https://deno.land/std@0.132.0/node/internal_binding/_libuv_winerror.ts": "801e05c2742ae6cd42a5f0fd555a255a7308a65732551e962e5345f55eedc519",
"https://deno.land/std@0.132.0/node/internal_binding/_node.ts": "e4075ba8a37aef4eb5b592c8e3807c39cb49ca8653faf8e01a43421938076c1b",
"https://deno.land/std@0.132.0/node/internal_binding/_utils.ts": "1c50883b5751a9ea1b38951e62ed63bacfdc9d69ea665292edfa28e1b1c5bd94",
"https://deno.land/std@0.132.0/node/internal_binding/_winerror.ts": "8811d4be66f918c165370b619259c1f35e8c3e458b8539db64c704fbde0a7cd2",
"https://deno.land/std@0.132.0/node/internal_binding/async_wrap.ts": "f06b8a403ad871248eb064190d27bf6fefdbe948991e71a18d7077390d5773f9",
"https://deno.land/std@0.132.0/node/internal_binding/buffer.ts": "722c62b85f966e0777b2d98c021b60e75d7f2c2dabc43413ef37d60dbd13a5d9",
"https://deno.land/std@0.132.0/node/internal_binding/cares_wrap.ts": "25b7b5d56612b2985260b673021828d6511a1c83b4c1927f5732cad2f2a718af",
"https://deno.land/std@0.132.0/node/internal_binding/config.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/connection_wrap.ts": "0380444ee94d5bd7b0b09921223d16729c9762a94e80b7f51eda49c7f42e6d0a",
"https://deno.land/std@0.132.0/node/internal_binding/constants.ts": "aff06aac49eda4234bd3a2b0b8e1fbfc67824e281c532ff9960831ab503014cc",
"https://deno.land/std@0.132.0/node/internal_binding/contextify.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/credentials.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/crypto.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/errors.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/fs.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/fs_dir.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/fs_event_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/handle_wrap.ts": "e59df84b1fb1b9823b09774b3e512d9c0029b4557400d09dd02cd7661c2c4830",
"https://deno.land/std@0.132.0/node/internal_binding/heap_utils.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/http_parser.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/icu.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/inspector.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/js_stream.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/messaging.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/mod.ts": "f68e74e8eed84eaa6b0de24f0f4c47735ed46866d7ee1c5a5e7c0667b4f0540f",
"https://deno.land/std@0.132.0/node/internal_binding/module_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/native_module.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/natives.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/node_file.ts": "c96ee0b2af319a3916de950a6c4b0d5fb00d09395c51cd239c54d95d62567aaf",
"https://deno.land/std@0.132.0/node/internal_binding/node_options.ts": "3cd5706153d28a4f5944b8b162c1c61b7b8e368a448fb1a2cff9f7957d3db360",
"https://deno.land/std@0.132.0/node/internal_binding/options.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/os.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/performance.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/pipe_wrap.ts": "00e942327f8e1c4b74a5888a82f0e16ba775cd09af804f96b6f6849b7eab1719",
"https://deno.land/std@0.132.0/node/internal_binding/process_methods.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/report.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/serdes.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/signal_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/spawn_sync.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/stream_wrap.ts": "d6e96f4b89d82ad5cc9b243c3d3880c9d85086165da54a7d85821a63491e5abf",
"https://deno.land/std@0.132.0/node/internal_binding/string_decoder.ts": "5cb1863763d1e9b458bc21d6f976f16d9c18b3b3f57eaf0ade120aee38fba227",
"https://deno.land/std@0.132.0/node/internal_binding/symbols.ts": "51cfca9bb6132d42071d4e9e6b68a340a7f274041cfcba3ad02900886e972a6c",
"https://deno.land/std@0.132.0/node/internal_binding/task_queue.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/tcp_wrap.ts": "10c64d5e092a8bff99cfe05adea716e4e52f4158662a5821790953e47e2cc21c",
"https://deno.land/std@0.132.0/node/internal_binding/timers.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/tls_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/trace_events.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/tty_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/types.ts": "4c26fb74ba2e45de553c15014c916df6789529a93171e450d5afb016b4c765e7",
"https://deno.land/std@0.132.0/node/internal_binding/udp_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/url.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/util.ts": "90364292e2bd598ab5d105b48ca49817b6708f2d1d9cbaf08b2b3ab5ca4c90a7",
"https://deno.land/std@0.132.0/node/internal_binding/uv.ts": "3821bc5e676d6955d68f581988c961d77dd28190aba5a9c59f16001a4deb34ba",
"https://deno.land/std@0.132.0/node/internal_binding/v8.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/worker.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/internal_binding/zlib.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4",
"https://deno.land/std@0.132.0/node/net.ts": "dfcb7e412abb3d5c55edde7d823b0ccb9601f7d40555ae3c862810c78b176185",
"https://deno.land/std@0.132.0/node/os.ts": "943d3294a7a00f39491148cd2097cdbf101233a421262223bb20ae702c059df5",
"https://deno.land/std@0.132.0/node/path.ts": "c65858e9cbb52dbc0dd348eefcdc41e82906c39cfa7982f2d4d805e828414b8c",
"https://deno.land/std@0.132.0/node/path/_constants.ts": "bd26f24a052b7d6b746151f4a236d29ab3c2096883bb6449c2fa499494406672",
"https://deno.land/std@0.132.0/node/path/_interface.ts": "6034ee29f6f295460ec82db1a94df9269aecbb0eceb81be72e9d843f8e8a97e6",
"https://deno.land/std@0.132.0/node/path/_util.ts": "9d4735fc05f8f1fb94406450e84e23fd201dc3fef5298b009e44cfa4e797b8f0",
"https://deno.land/std@0.132.0/node/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4",
"https://deno.land/std@0.132.0/node/path/glob.ts": "d6b64a24f148855a6e8057a171a2f9910c39e492e4ccec482005205b28eb4533",
"https://deno.land/std@0.132.0/node/path/mod.ts": "62e21dc6e1fe2e9742fce85de631a7b067d968544fe66954578e6d73c97369a2",
"https://deno.land/std@0.132.0/node/path/posix.ts": "9dd5fc83c4ae0e0b700bef43c88c67e276840c187a66d4d6a661440cf1fecc52",
"https://deno.land/std@0.132.0/node/path/separator.ts": "c908c9c28ebe7f1fea67daaccf84b63af90d882fe986f9fa03af9563a852723a",
"https://deno.land/std@0.132.0/node/path/win32.ts": "f869ee449b6dee69b13e2d1f8f7f1d01c7ae1e67fa573eab789429929f7a3864",
"https://deno.land/std@0.132.0/node/process.ts": "699f47f2f177556e17e2f7d0dcd3705ff5065cbdf72029e534c1540404d6f501",
"https://deno.land/std@0.132.0/node/querystring.ts": "967b8a7b00a73ebe373666deb3a7e501f164bac27bb342fde7221ecbb3522689",
"https://deno.land/std@0.132.0/node/stream.ts": "d127faa074a9e3886e4a01dcfe9f9a6a4b5641f76f6acc356e8ded7da5dc2c81",
"https://deno.land/std@0.132.0/node/stream/promises.mjs": "b263c09f2d6bd715dc514fab3f99cca84f442e2d23e87adbe76e32ea46fc87e6",
"https://deno.land/std@0.132.0/node/string_decoder.ts": "51ce85a173d2e36ac580d418bb48b804adb41732fc8bd85f7d5d27b7accbc61f",
"https://deno.land/std@0.132.0/node/timers.ts": "2d66fcd21e37acf76c3a699a97230da57cc21382c8e885b3c5377b37efd0f06c",
"https://deno.land/std@0.132.0/node/url.ts": "bc0bde2774854b6a377c4c61fa73e5a28283cbeb7f8703479f44e471219c33a8",
"https://deno.land/std@0.132.0/node/util.ts": "7fd6933b37af89a8e64d73dc6ee1732455a59e7e6d0965311fbd73cd634ea630",
"https://deno.land/std@0.132.0/node/util/types.mjs": "f9288198cacd374b41bae7e92a23179d3160f4c0eaf14e19be3a4e7057219a60",
"https://deno.land/std@0.132.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
"https://deno.land/std@0.132.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
"https://deno.land/std@0.132.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b",
"https://deno.land/std@0.132.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
"https://deno.land/std@0.132.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
"https://deno.land/std@0.132.0/path/mod.ts": "4275129bb766f0e475ecc5246aa35689eeade419d72a48355203f31802640be7",
"https://deno.land/std@0.132.0/path/posix.ts": "663e4a6fe30a145f56aa41a22d95114c4c5582d8b57d2d7c9ed27ad2c47636bb",
"https://deno.land/std@0.132.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
"https://deno.land/std@0.132.0/path/win32.ts": "e7bdf63e8d9982b4d8a01ef5689425c93310ece950e517476e22af10f41a136e",
"https://deno.land/std@0.132.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21",
"https://deno.land/std@0.132.0/testing/_diff.ts": "9d849cd6877694152e01775b2d93f9d6b7aef7e24bfe3bfafc4d7a1ac8e9f392",
"https://deno.land/std@0.132.0/testing/asserts.ts": "b0ef969032882b1f7eb1c7571e313214baa1485f7b61cf35807b2434e254365c",
"https://deno.land/std@0.160.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", "https://deno.land/std@0.160.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
"https://deno.land/std@0.160.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", "https://deno.land/std@0.160.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934",
"https://deno.land/std@0.160.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", "https://deno.land/std@0.160.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06",
@ -1417,6 +1717,30 @@
"https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/password.ts": "3c578bd21e4fd283431aa940357f40fed2e26d3de12ad129a696d7fe38ae744d", "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/password.ts": "3c578bd21e4fd283431aa940357f40fed2e26d3de12ad129a696d7fe38ae744d",
"https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/text-util.ts": "37c0437d2030c0b6255f10afec7ccfcb6b195e9a0a011bb7956595142c3d7383", "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/text-util.ts": "37c0437d2030c0b6255f10afec7ccfcb6b195e9a0a011bb7956595142c3d7383",
"https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/util.ts": "a8285450db7b56a3e507f478aaad68927ecb1ee545449cb869ccc4aace13fada", "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/util.ts": "a8285450db7b56a3e507f478aaad68927ecb1ee545449cb869ccc4aace13fada",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/mod.js": "cb68f17d6d90df318934deccdb469d740be0888e7a597a9e7eea7100ce36a252",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/polyfills.js": "318eb01f2b4cc33a46c59f3ddc11f22a56d6b1db8b7719b2ad7decee63a5bd47",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/bytes.js": "f2de43bdc8fa5dc4b169f2c70d5d8b053a3dea8f85ef011d7b27dec69e14ebb7",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/connection.js": "c63d53a0f35a7eb2670befef551f23fe914bbe9f0590de974e3e210c50527a29",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/errors.js": "85cfbed9a5ab0db41ab8e97b806c881af29807dfe99bc656fdf1a18c1c13b6c6",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/index.js": "119a320c6cc8d0e3927ec1384322494613980716f9fb4f67c82e3cb708a23d0a",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/large.js": "f3e770cdb7cc695f7b50687b4c6c4b7252129515486ec8def98b7582ee7c54ef",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/query.js": "67c45a5151032aa46b587abc15381fe4efd97c696e5c1b53082b8161309c4ee2",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/queue.js": "709624843223ea842bf095f6934080f19f1a059a51cbbf82e9827f3bb1bf2ca7",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/result.js": "001ff5e0c8d634674f483d07fbcd620a797e3101f842d6c20ca3ace936260465",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/subscribe.js": "9e4d0c3e573a6048e77ee2f15abbd5bcd17da9ca85a78c914553472c6d6c169b",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/17469a9e5f025d112206c583a29275e93dfc1431/deno/src/types.js": "471f4a6c35412aa202a7c177c0a7e5a7c3bd225f01bbde67c947894c1b8bf6ed",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/mod.js": "cb68f17d6d90df318934deccdb469d740be0888e7a597a9e7eea7100ce36a252",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/polyfills.js": "318eb01f2b4cc33a46c59f3ddc11f22a56d6b1db8b7719b2ad7decee63a5bd47",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/bytes.js": "f2de43bdc8fa5dc4b169f2c70d5d8b053a3dea8f85ef011d7b27dec69e14ebb7",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/connection.js": "c63d53a0f35a7eb2670befef551f23fe914bbe9f0590de974e3e210c50527a29",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/errors.js": "85cfbed9a5ab0db41ab8e97b806c881af29807dfe99bc656fdf1a18c1c13b6c6",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/index.js": "b6fa0bd613aedc45b93ee5be4d133e8f1418b5d25fc041c56b124f28b432ce5a",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/large.js": "f3e770cdb7cc695f7b50687b4c6c4b7252129515486ec8def98b7582ee7c54ef",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/query.js": "67c45a5151032aa46b587abc15381fe4efd97c696e5c1b53082b8161309c4ee2",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/queue.js": "15e6345adb6708bf3b99ad39fc2231c2fb61de5f6cba4b7a7a6be881482a4ec3",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/result.js": "001ff5e0c8d634674f483d07fbcd620a797e3101f842d6c20ca3ace936260465",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/subscribe.js": "9e4d0c3e573a6048e77ee2f15abbd5bcd17da9ca85a78c914553472c6d6c169b",
"https://raw.githubusercontent.com/xyzshantaram/postgres.js/master/deno/src/types.js": "471f4a6c35412aa202a7c177c0a7e5a7c3bd225f01bbde67c947894c1b8bf6ed",
"https://unpkg.com/nostr-relaypool2@0.6.34/lib/nostr-relaypool.worker.js": "a336e5c58b1e6946ae8943eb4fef21b810dc2a5a233438cff92b883673e29c96" "https://unpkg.com/nostr-relaypool2@0.6.34/lib/nostr-relaypool.worker.js": "a336e5c58b1e6946ae8943eb4fef21b810dc2a5a233438cff92b883673e29c96"
}, },
"workspace": { "workspace": {
@ -1424,7 +1748,7 @@
"jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
"jsr:@db/sqlite@^0.11.1", "jsr:@db/sqlite@^0.11.1",
"jsr:@hono/hono@^4.4.6", "jsr:@hono/hono@^4.4.6",
"jsr:@nostrify/nostrify@^0.25.0", "jsr:@nostrify/nostrify@^0.26.3",
"jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "jsr:@soapbox/kysely-deno-sqlite@^2.1.0",
"jsr:@soapbox/stickynotes@^0.4.0", "jsr:@soapbox/stickynotes@^0.4.0",
"jsr:@std/assert@^0.225.1", "jsr:@std/assert@^0.225.1",
@ -1446,6 +1770,7 @@
"npm:hono-rate-limiter@^0.3.0", "npm:hono-rate-limiter@^0.3.0",
"npm:iso-639-1@2.1.15", "npm:iso-639-1@2.1.15",
"npm:isomorphic-dompurify@^2.11.0", "npm:isomorphic-dompurify@^2.11.0",
"npm:kysely-postgres-js@2.0.0",
"npm:kysely@^0.27.3", "npm:kysely@^0.27.3",
"npm:light-bolt11-decoder", "npm:light-bolt11-decoder",
"npm:linkify-plugin-hashtag@^4.1.1", "npm:linkify-plugin-hashtag@^4.1.1",

View file

@ -0,0 +1,34 @@
{
"kind": 10002,
"id": "68fc04e23b07219f153a10947663b9dd7b271acbc03b82200e364e35de3e0bdd",
"pubkey": "0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd",
"created_at": 1714969354,
"tags": [
[
"r",
"wss://gleasonator.dev/relay"
],
[
"r",
"wss://nosdrive.app/relay"
],
[
"r",
"wss://relay.mostr.pub/"
],
[
"r",
"wss://relay.primal.net/"
],
[
"r",
"wss://relay.snort.social/"
],
[
"r",
"wss://relay.damus.io/"
]
],
"content": "",
"sig": "cb7b1a75fe015d5c9481651379365bd5d098665b1bc7a453522177e2686eaa83581ec36f7a17429aad2541dad02c2c81023b81612f87f28fc57447fef1efab13"
}

View file

@ -1,4 +1,11 @@
import { Conf } from '@/config.ts';
import { DittoDB } from '@/db/DittoDB.ts'; import { DittoDB } from '@/db/DittoDB.ts';
import { sleep } from '@/test.ts';
if (Deno.env.get('CI') && Conf.db.dialect === 'postgres') {
console.info('Waiting 1 second for postgres to start...');
await sleep(1_000);
}
const kysely = await DittoDB.getInstance(); const kysely = await DittoDB.getInstance();
await kysely.destroy(); await kysely.destroy();

23
src/RelayError.test.ts Normal file
View file

@ -0,0 +1,23 @@
import { assertThrows } from '@std/assert';
import { RelayError } from '@/RelayError.ts';
Deno.test('Construct a RelayError from the reason message', () => {
assertThrows(
() => {
throw RelayError.fromReason('duplicate: already exists');
},
RelayError,
'duplicate: already exists',
);
});
Deno.test('Throw a new RelayError if the OK message is false', () => {
assertThrows(
() => {
RelayError.assert(['OK', 'yolo', false, 'error: bla bla bla']);
},
RelayError,
'error: bla bla bla',
);
});

View file

@ -98,6 +98,21 @@ class Conf {
} }
return undefined; return undefined;
}, },
/** Database query timeout configurations. */
timeouts: {
/** Default query timeout when another setting isn't more specific. */
get default(): number {
return Number(Deno.env.get('DB_TIMEOUT_DEFAULT') || 5_000);
},
/** Timeout used for queries made through the Nostr relay. */
get relay(): number {
return Number(Deno.env.get('DB_TIMEOUT_RELAY') || 1_000);
},
/** Timeout used for timelines such as home, notifications, hashtag, etc. */
get timelines(): number {
return Number(Deno.env.get('DB_TIMEOUT_TIMELINES') || 15_000);
},
},
}; };
/** Character limit to enforce for posts made through Mastodon API. */ /** Character limit to enforce for posts made through Mastodon API. */
static get postCharLimit(): number { static get postCharLimit(): number {

View file

@ -209,7 +209,7 @@ const accountStatusesController: AppController = async (c) => {
filter['#t'] = [tagged]; filter['#t'] = [tagged];
} }
const opts = { signal, limit, timeout: 10_000 }; const opts = { signal, limit, timeout: Conf.db.timeouts.timelines };
const events = await store.query([filter], opts) const events = await store.query([filter], opts)
.then((events) => hydrateEvents({ events, store, signal })) .then((events) => hydrateEvents({ events, store, signal }))

View file

@ -78,7 +78,7 @@ async function renderNotifications(
const store = c.get('store'); const store = c.get('store');
const pubkey = await c.get('signer')?.getPublicKey()!; const pubkey = await c.get('signer')?.getPublicKey()!;
const { signal } = c.req.raw; const { signal } = c.req.raw;
const opts = { signal, limit: params.limit, timeout: 15_000 }; const opts = { signal, limit: params.limit, timeout: Conf.db.timeouts.timelines };
const events = await store const events = await store
.query(filters, opts) .query(filters, opts)

View file

@ -97,8 +97,8 @@ const createStatusController: AppController = async (c) => {
const root = ancestor.tags.find((tag) => tag[0] === 'e' && tag[3] === 'root')?.[1] ?? ancestor.id; const root = ancestor.tags.find((tag) => tag[0] === 'e' && tag[3] === 'root')?.[1] ?? ancestor.id;
tags.push(['e', root, 'root']); tags.push(['e', root, Conf.relay, 'root']);
tags.push(['e', data.in_reply_to_id, 'reply']); tags.push(['e', data.in_reply_to_id, Conf.relay, 'reply']);
} }
if (data.quote_id) { if (data.quote_id) {
@ -202,7 +202,7 @@ const deleteStatusController: AppController = async (c) => {
if (event.pubkey === pubkey) { if (event.pubkey === pubkey) {
await createEvent({ await createEvent({
kind: 5, kind: 5,
tags: [['e', id]], tags: [['e', id, Conf.relay]],
}, c); }, c);
const author = await getAuthor(event.pubkey); const author = await getAuthor(event.pubkey);
@ -260,8 +260,8 @@ const favouriteController: AppController = async (c) => {
kind: 7, kind: 7,
content: '+', content: '+',
tags: [ tags: [
['e', target.id], ['e', target.id, Conf.relay],
['p', target.pubkey], ['p', target.pubkey, Conf.relay],
], ],
}, c); }, c);
@ -302,7 +302,10 @@ const reblogStatusController: AppController = async (c) => {
const reblogEvent = await createEvent({ const reblogEvent = await createEvent({
kind: 6, kind: 6,
tags: [['e', event.id], ['p', event.pubkey]], tags: [
['e', event.id, Conf.relay],
['p', event.pubkey, Conf.relay],
],
}, c); }, c);
await hydrateEvents({ await hydrateEvents({
@ -337,7 +340,7 @@ const unreblogStatusController: AppController = async (c) => {
await createEvent({ await createEvent({
kind: 5, kind: 5,
tags: [['e', repostEvent.id]], tags: [['e', repostEvent.id, Conf.relay]],
}, c); }, c);
return c.json(await renderStatus(event, { viewerPubkey: pubkey })); return c.json(await renderStatus(event, { viewerPubkey: pubkey }));
@ -389,7 +392,7 @@ const bookmarkController: AppController = async (c) => {
if (event) { if (event) {
await updateListEvent( await updateListEvent(
{ kinds: [10003], authors: [pubkey], limit: 1 }, { kinds: [10003], authors: [pubkey], limit: 1 },
(tags) => addTag(tags, ['e', eventId]), (tags) => addTag(tags, ['e', eventId, Conf.relay]),
c, c,
); );
@ -416,7 +419,7 @@ const unbookmarkController: AppController = async (c) => {
if (event) { if (event) {
await updateListEvent( await updateListEvent(
{ kinds: [10003], authors: [pubkey], limit: 1 }, { kinds: [10003], authors: [pubkey], limit: 1 },
(tags) => deleteTag(tags, ['e', eventId]), (tags) => deleteTag(tags, ['e', eventId, Conf.relay]),
c, c,
); );
@ -443,7 +446,7 @@ const pinController: AppController = async (c) => {
if (event) { if (event) {
await updateListEvent( await updateListEvent(
{ kinds: [10001], authors: [pubkey], limit: 1 }, { kinds: [10001], authors: [pubkey], limit: 1 },
(tags) => addTag(tags, ['e', eventId]), (tags) => addTag(tags, ['e', eventId, Conf.relay]),
c, c,
); );
@ -472,7 +475,7 @@ const unpinController: AppController = async (c) => {
if (event) { if (event) {
await updateListEvent( await updateListEvent(
{ kinds: [10001], authors: [pubkey], limit: 1 }, { kinds: [10001], authors: [pubkey], limit: 1 },
(tags) => deleteTag(tags, ['e', eventId]), (tags) => deleteTag(tags, ['e', eventId, Conf.relay]),
c, c,
); );
@ -516,7 +519,7 @@ const zapController: AppController = async (c) => {
lnurl = getLnurl(meta); lnurl = getLnurl(meta);
if (target && lnurl) { if (target && lnurl) {
tags.push( tags.push(
['e', target.id], ['e', target.id, Conf.relay],
['p', target.pubkey], ['p', target.pubkey],
['amount', amount.toString()], ['amount', amount.toString()],
['relays', Conf.relay], ['relays', Conf.relay],

View file

@ -10,9 +10,10 @@ import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
import { getFeedPubkeys } from '@/queries.ts'; import { getFeedPubkeys } from '@/queries.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { bech32ToPubkey } from '@/utils.ts'; import { bech32ToPubkey, Time } from '@/utils.ts';
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
import { renderNotification } from '@/views/mastodon/notifications.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts';
import TTLCache from '@isaacs/ttlcache';
const debug = Debug('ditto:streaming'); const debug = Debug('ditto:streaming');
@ -37,6 +38,11 @@ const streamSchema = z.enum([
type Stream = z.infer<typeof streamSchema>; type Stream = z.infer<typeof streamSchema>;
const LIMITER_WINDOW = Time.minutes(5);
const LIMITER_LIMIT = 100;
const limiter = new TTLCache<string, number>();
const streamingController: AppController = async (c) => { const streamingController: AppController = async (c) => {
const upgrade = c.req.header('upgrade'); const upgrade = c.req.header('upgrade');
const token = c.req.header('sec-websocket-protocol'); const token = c.req.header('sec-websocket-protocol');
@ -52,6 +58,14 @@ const streamingController: AppController = async (c) => {
return c.json({ error: 'Invalid access token' }, 401); return c.json({ error: 'Invalid access token' }, 401);
} }
const ip = c.req.header('x-real-ip');
if (ip) {
const count = limiter.get(ip) ?? 0;
if (count > LIMITER_LIMIT) {
return c.json({ error: 'Rate limit exceeded' }, 429);
}
}
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 }); const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 });
const store = await Storages.db(); const store = await Storages.db();
@ -122,6 +136,23 @@ const streamingController: AppController = async (c) => {
} }
}; };
socket.onmessage = (e) => {
if (ip) {
const count = limiter.get(ip) ?? 0;
limiter.set(ip, count + 1, { ttl: LIMITER_WINDOW });
if (count > LIMITER_LIMIT) {
socket.close(1008, 'Rate limit exceeded');
return;
}
}
if (typeof e.data !== 'string') {
socket.close(1003, 'Invalid message');
return;
}
};
socket.onclose = () => { socket.onclose = () => {
streamingConnectionsGauge.dec(); streamingConnectionsGauge.dec();
controller.abort(); controller.abort();

View file

@ -60,7 +60,7 @@ const suggestedTimelineController: AppController = async (c) => {
async function renderStatuses(c: AppContext, filters: NostrFilter[]) { async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
const { signal } = c.req.raw; const { signal } = c.req.raw;
const store = c.get('store'); const store = c.get('store');
const opts = { signal, timeout: 10_000 }; const opts = { signal, timeout: Conf.db.timeouts.timelines };
const events = await store const events = await store
.query(filters, opts) .query(filters, opts)

View file

@ -1,6 +1,11 @@
import { ErrorHandler } from '@hono/hono'; import { ErrorHandler } from '@hono/hono';
import { HTTPException } from '@hono/hono/http-exception';
export const errorHandler: ErrorHandler = (err, c) => { export const errorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
console.error(err); console.error(err);
if (err.message === 'canceling statement due to statement timeout') { if (err.message === 'canceling statement due to statement timeout') {

View file

@ -1,3 +1,4 @@
import TTLCache from '@isaacs/ttlcache';
import { import {
NostrClientCLOSE, NostrClientCLOSE,
NostrClientCOUNT, NostrClientCOUNT,
@ -9,17 +10,24 @@ import {
} from '@nostrify/nostrify'; } from '@nostrify/nostrify';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { Conf } from '@/config.ts';
import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts';
import { relayConnectionsGauge, relayEventCounter, relayMessageCounter } from '@/metrics.ts'; import { relayConnectionsGauge, relayEventCounter, relayMessageCounter } from '@/metrics.ts';
import * as pipeline from '@/pipeline.ts'; import * as pipeline from '@/pipeline.ts';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { Time } from '@/utils/time.ts';
/** Limit of initial events returned for a subscription. */ /** Limit of initial events returned for a subscription. */
const FILTER_LIMIT = 100; const FILTER_LIMIT = 100;
const LIMITER_WINDOW = Time.minutes(1);
const LIMITER_LIMIT = 300;
const limiter = new TTLCache<string, number>();
/** Set up the Websocket connection. */ /** Set up the Websocket connection. */
function connectStream(socket: WebSocket) { function connectStream(socket: WebSocket, ip: string | undefined) {
const controllers = new Map<string, AbortController>(); const controllers = new Map<string, AbortController>();
socket.onopen = () => { socket.onopen = () => {
@ -27,6 +35,21 @@ function connectStream(socket: WebSocket) {
}; };
socket.onmessage = (e) => { socket.onmessage = (e) => {
if (ip) {
const count = limiter.get(ip) ?? 0;
limiter.set(ip, count + 1, { ttl: LIMITER_WINDOW });
if (count > LIMITER_LIMIT) {
socket.close(1008, 'Rate limit exceeded');
return;
}
}
if (typeof e.data !== 'string') {
socket.close(1003, 'Invalid message');
return;
}
const result = n.json().pipe(n.clientMsg()).safeParse(e.data); const result = n.json().pipe(n.clientMsg()).safeParse(e.data);
if (result.success) { if (result.success) {
relayMessageCounter.inc({ verb: result.data[0] }); relayMessageCounter.inc({ verb: result.data[0] });
@ -73,7 +96,7 @@ function connectStream(socket: WebSocket) {
const pubsub = await Storages.pubsub(); const pubsub = await Storages.pubsub();
try { try {
for (const event of await store.query(filters, { limit: FILTER_LIMIT, timeout: 1000 })) { for (const event of await store.query(filters, { limit: FILTER_LIMIT, timeout: Conf.db.timeouts.relay })) {
send(['EVENT', subId, event]); send(['EVENT', subId, event]);
} }
} catch (e) { } catch (e) {
@ -128,7 +151,7 @@ function connectStream(socket: WebSocket) {
/** Handle COUNT. Return the number of events matching the filters. */ /** Handle COUNT. Return the number of events matching the filters. */
async function handleCount([_, subId, ...filters]: NostrClientCOUNT): Promise<void> { async function handleCount([_, subId, ...filters]: NostrClientCOUNT): Promise<void> {
const store = await Storages.db(); const store = await Storages.db();
const { count } = await store.count(filters, { timeout: 100 }); const { count } = await store.count(filters, { timeout: Conf.db.timeouts.relay });
send(['COUNT', subId, { count, approximate: false }]); send(['COUNT', subId, { count, approximate: false }]);
} }
@ -152,8 +175,16 @@ const relayController: AppController = (c, next) => {
return c.text('Please use a Nostr client to connect.', 400); return c.text('Please use a Nostr client to connect.', 400);
} }
const ip = c.req.header('x-real-ip');
if (ip) {
const count = limiter.get(ip) ?? 0;
if (count > LIMITER_LIMIT) {
return c.json({ error: 'Rate limit exceeded' }, 429);
}
}
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { idleTimeout: 30 }); const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { idleTimeout: 30 });
connectStream(socket); connectStream(socket, ip);
return response; return response;
}; };

View file

@ -39,14 +39,14 @@ export class DittoDB {
static get poolSize(): number { static get poolSize(): number {
if (Conf.db.dialect === 'postgres') { if (Conf.db.dialect === 'postgres') {
return DittoPostgres.getPool().size; return DittoPostgres.poolSize;
} }
return 1; return 1;
} }
static get availableConnections(): number { static get availableConnections(): number {
if (Conf.db.dialect === 'postgres') { if (Conf.db.dialect === 'postgres') {
return DittoPostgres.getPool().available; return DittoPostgres.availableConnections;
} }
return 1; return 1;
} }

View file

@ -1,6 +1,6 @@
import { Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely'; import { Kysely } from 'kysely';
import { PostgreSQLDriver } from 'kysely_deno_postgres'; import { PostgresJSDialect, PostgresJSDialectConfig } from 'kysely-postgres-js';
import { Pool } from 'postgres'; import postgres from 'postgres';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoTables } from '@/db/DittoTables.ts'; import { DittoTables } from '@/db/DittoTables.ts';
@ -8,37 +8,31 @@ import { KyselyLogger } from '@/db/KyselyLogger.ts';
export class DittoPostgres { export class DittoPostgres {
static db: Kysely<DittoTables> | undefined; static db: Kysely<DittoTables> | undefined;
static pool: Pool | undefined; static postgres?: postgres.Sql;
static getPool(): Pool {
if (!this.pool) {
this.pool = new Pool(Conf.databaseUrl, Conf.pg.poolSize, true);
}
return this.pool;
}
// deno-lint-ignore require-await // deno-lint-ignore require-await
static async getInstance(): Promise<Kysely<DittoTables>> { static async getInstance(): Promise<Kysely<DittoTables>> {
if (!this.postgres) {
this.postgres = postgres(Conf.databaseUrl, { max: Conf.pg.poolSize });
}
if (!this.db) { if (!this.db) {
this.db = new Kysely({ this.db = new Kysely({
dialect: { dialect: new PostgresJSDialect({
createAdapter() { postgres: this.postgres as unknown as PostgresJSDialectConfig['postgres'],
return new PostgresAdapter(); }),
},
createDriver() {
return new PostgreSQLDriver(DittoPostgres.getPool());
},
createIntrospector(db: Kysely<unknown>) {
return new PostgresIntrospector(db);
},
createQueryCompiler() {
return new PostgresQueryCompiler();
},
},
log: KyselyLogger, log: KyselyLogger,
}); });
} }
return this.db; return this.db;
} }
static get poolSize() {
return this.postgres?.connections.open ?? 0;
}
static get availableConnections(): number {
return this.postgres?.connections.idle ?? 0;
}
} }

View file

@ -0,0 +1,39 @@
import { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createIndex('nostr_events_created_at_kind')
.on('nostr_events')
.ifNotExists()
.columns(['created_at desc', 'id asc', 'kind'])
.execute();
await db.schema
.createIndex('nostr_events_kind_pubkey_created_at')
.on('nostr_events')
.ifNotExists()
.columns(['kind', 'pubkey', 'created_at desc', 'id asc'])
.execute();
await db.schema.dropIndex('idx_events_created_at_kind').execute();
await db.schema.dropIndex('idx_events_kind_pubkey_created_at').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropIndex('nostr_events_created_at_kind').execute();
await db.schema.dropIndex('nostr_events_kind_pubkey_created_at').execute();
await db.schema
.createIndex('idx_events_created_at_kind')
.on('nostr_events')
.ifNotExists()
.columns(['created_at desc', 'kind'])
.execute();
await db.schema
.createIndex('idx_events_kind_pubkey_created_at')
.on('nostr_events')
.ifNotExists()
.columns(['kind', 'pubkey', 'created_at desc'])
.execute();
}

View file

@ -1,4 +1,5 @@
// deno-lint-ignore-file require-await // deno-lint-ignore-file require-await
import { HTTPException } from '@hono/hono/http-exception';
import { NConnectSigner, NostrEvent, NostrSigner } from '@nostrify/nostrify'; import { NConnectSigner, NostrEvent, NostrSigner } from '@nostrify/nostrify';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
@ -27,30 +28,78 @@ export class ConnectSigner implements NostrSigner {
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> { async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
const signer = await this.signer; const signer = await this.signer;
return signer.signEvent(event); try {
return await signer.signEvent(event);
} catch (e) {
if (e.name === 'AbortError') {
throw new HTTPException(408, { message: 'The event was not signed quickly enough' });
} else {
throw e;
}
}
} }
readonly nip04 = { readonly nip04 = {
encrypt: async (pubkey: string, plaintext: string): Promise<string> => { encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
const signer = await this.signer; const signer = await this.signer;
return signer.nip04.encrypt(pubkey, plaintext); try {
return await signer.nip04.encrypt(pubkey, plaintext);
} catch (e) {
if (e.name === 'AbortError') {
throw new HTTPException(408, {
message: 'Text was not encrypted quickly enough',
});
} else {
throw e;
}
}
}, },
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => { decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
const signer = await this.signer; const signer = await this.signer;
return signer.nip04.decrypt(pubkey, ciphertext); try {
return await signer.nip04.decrypt(pubkey, ciphertext);
} catch (e) {
if (e.name === 'AbortError') {
throw new HTTPException(408, {
message: 'Text was not decrypted quickly enough',
});
} else {
throw e;
}
}
}, },
}; };
readonly nip44 = { readonly nip44 = {
encrypt: async (pubkey: string, plaintext: string): Promise<string> => { encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
const signer = await this.signer; const signer = await this.signer;
return signer.nip44.encrypt(pubkey, plaintext); try {
return await signer.nip44.encrypt(pubkey, plaintext);
} catch (e) {
if (e.name === 'AbortError') {
throw new HTTPException(408, {
message: 'Text was not encrypted quickly enough',
});
} else {
throw e;
}
}
}, },
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => { decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
const signer = await this.signer; const signer = await this.signer;
return signer.nip44.decrypt(pubkey, ciphertext); try {
return await signer.nip44.decrypt(pubkey, ciphertext);
} catch (e) {
if (e.name === 'AbortError') {
throw new HTTPException(408, {
message: 'Text was not decrypted quickly enough',
});
} else {
throw e;
}
}
}, },
}; };

View file

@ -7,7 +7,7 @@ export class ReadOnlySigner implements NostrSigner {
async signEvent(): Promise<NostrEvent> { async signEvent(): Promise<NostrEvent> {
throw new HTTPException(401, { throw new HTTPException(401, {
message: 'Log out and back in', message: 'Log in with Nostr Connect to sign events',
}); });
} }

View file

@ -1,18 +1,17 @@
// deno-lint-ignore-file require-await // deno-lint-ignore-file require-await
import { RelayPoolWorker } from 'nostr-relaypool';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoDB } from '@/db/DittoDB.ts'; import { DittoDB } from '@/db/DittoDB.ts';
import { AdminStore } from '@/storages/AdminStore.ts'; import { AdminStore } from '@/storages/AdminStore.ts';
import { EventsDB } from '@/storages/EventsDB.ts'; import { EventsDB } from '@/storages/EventsDB.ts';
import { PoolStore } from '@/storages/pool-store.ts';
import { SearchStore } from '@/storages/search-store.ts'; import { SearchStore } from '@/storages/search-store.ts';
import { InternalRelay } from '@/storages/InternalRelay.ts'; import { InternalRelay } from '@/storages/InternalRelay.ts';
import { NPool, NRelay1 } from '@nostrify/nostrify';
import { getRelays } from '@/utils/outbox.ts';
export class Storages { export class Storages {
private static _db: Promise<EventsDB> | undefined; private static _db: Promise<EventsDB> | undefined;
private static _admin: Promise<AdminStore> | undefined; private static _admin: Promise<AdminStore> | undefined;
private static _client: Promise<PoolStore> | undefined; private static _client: Promise<NPool> | undefined;
private static _pubsub: Promise<InternalRelay> | undefined; private static _pubsub: Promise<InternalRelay> | undefined;
private static _search: Promise<SearchStore> | undefined; private static _search: Promise<SearchStore> | undefined;
@ -44,7 +43,7 @@ export class Storages {
} }
/** Relay pool storage. */ /** Relay pool storage. */
public static async client(): Promise<PoolStore> { public static async client(): Promise<NPool> {
if (!this._client) { if (!this._client) {
this._client = (async () => { this._client = (async () => {
const db = await this.db(); const db = await this.db();
@ -56,7 +55,7 @@ export class Storages {
const tags = relayList?.tags ?? []; const tags = relayList?.tags ?? [];
const activeRelays = tags.reduce((acc, [name, url, marker]) => { const activeRelays = tags.reduce((acc, [name, url, marker]) => {
if (name === 'r' && !marker) { if (name === 'r' && (!marker || marker === 'write')) {
acc.push(url); acc.push(url);
} }
return acc; return acc;
@ -64,22 +63,25 @@ export class Storages {
console.log(`pool: connecting to ${activeRelays.length} relays.`); console.log(`pool: connecting to ${activeRelays.length} relays.`);
const worker = new Worker('https://unpkg.com/nostr-relaypool2@0.6.34/lib/nostr-relaypool.worker.js', { return new NPool({
type: 'module', open(url) {
}); return new NRelay1(url, {
// Skip event verification (it's done in the pipeline).
verifyEvent: () => true,
});
},
reqRouter: async (filters) => {
return new Map(activeRelays.map((relay) => {
return [relay, filters];
}));
},
eventRouter: async (event) => {
const relaySet = await getRelays(await Storages.db(), event.pubkey);
relaySet.delete(Conf.relay);
// @ts-ignore Wrong types. const relays = [...relaySet].slice(0, 4);
const pool = new RelayPoolWorker(worker, activeRelays, { return relays;
autoReconnect: true, },
// The pipeline verifies events.
skipVerification: true,
// The logging feature overwhelms the CPU and creates too many logs.
logErrorsAndNotices: false,
});
return new PoolStore({
pool,
relays: activeRelays,
}); });
})(); })();
} }

View file

@ -64,7 +64,7 @@ class EventsDB implements NStore {
await this.deleteEventsAdmin(event); await this.deleteEventsAdmin(event);
try { try {
await this.store.event(event, { ...opts, timeout: opts.timeout ?? 1000 }); await this.store.event(event, { ...opts, timeout: opts.timeout ?? Conf.db.timeouts.default });
} catch (e) { } catch (e) {
if (e.message === 'Cannot add a deleted event') { if (e.message === 'Cannot add a deleted event') {
throw new RelayError('blocked', 'event deleted by user'); throw new RelayError('blocked', 'event deleted by user');
@ -164,7 +164,7 @@ class EventsDB implements NStore {
this.console.debug('REQ', JSON.stringify(filters)); this.console.debug('REQ', JSON.stringify(filters));
return this.store.query(filters, { ...opts, timeout: opts.timeout ?? 1000 }); return this.store.query(filters, { ...opts, timeout: opts.timeout ?? Conf.db.timeouts.default });
} }
/** Delete events based on filters from the database. */ /** Delete events based on filters from the database. */
@ -172,7 +172,7 @@ class EventsDB implements NStore {
if (!filters.length) return Promise.resolve(); if (!filters.length) return Promise.resolve();
this.console.debug('DELETE', JSON.stringify(filters)); this.console.debug('DELETE', JSON.stringify(filters));
return this.store.remove(filters, { ...opts, timeout: opts.timeout ?? 3000 }); return this.store.remove(filters, { ...opts, timeout: opts.timeout ?? Conf.db.timeouts.default });
} }
/** Get number of events that would be returned by filters. */ /** Get number of events that would be returned by filters. */
@ -185,7 +185,7 @@ class EventsDB implements NStore {
this.console.debug('COUNT', JSON.stringify(filters)); this.console.debug('COUNT', JSON.stringify(filters));
return this.store.count(filters, { ...opts, timeout: opts.timeout ?? 500 }); return this.store.count(filters, { ...opts, timeout: opts.timeout ?? Conf.db.timeouts.default });
} }
/** Return only the tags that should be indexed. */ /** Return only the tags that should be indexed. */

View file

@ -1,103 +0,0 @@
import {
NostrEvent,
NostrFilter,
NostrRelayCLOSED,
NostrRelayEOSE,
NostrRelayEVENT,
NRelay,
NSet,
} from '@nostrify/nostrify';
import { Machina } from '@nostrify/nostrify/utils';
import Debug from '@soapbox/stickynotes/debug';
import { RelayPoolWorker } from 'nostr-relaypool';
import { getFilterLimit, matchFilters } from 'nostr-tools';
import { Conf } from '@/config.ts';
import { Storages } from '@/storages.ts';
import { purifyEvent } from '@/storages/hydrate.ts';
import { abortError } from '@/utils/abort.ts';
import { getRelays } from '@/utils/outbox.ts';
interface PoolStoreOpts {
pool: InstanceType<typeof RelayPoolWorker>;
relays: WebSocket['url'][];
}
class PoolStore implements NRelay {
private debug = Debug('ditto:client');
private pool: InstanceType<typeof RelayPoolWorker>;
private relays: WebSocket['url'][];
constructor(opts: PoolStoreOpts) {
this.pool = opts.pool;
this.relays = opts.relays;
}
async event(event: NostrEvent, opts: { signal?: AbortSignal } = {}): Promise<void> {
if (opts.signal?.aborted) return Promise.reject(abortError());
const relaySet = await getRelays(await Storages.db(), event.pubkey);
relaySet.delete(Conf.relay);
const relays = [...relaySet].slice(0, 4);
event = purifyEvent(event);
this.debug('EVENT', event, relays);
this.pool.publish(event, relays);
return Promise.resolve();
}
async *req(
filters: NostrFilter[],
opts: { signal?: AbortSignal; limit?: number } = {},
): AsyncIterable<NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED> {
this.debug('REQ', JSON.stringify(filters));
const uuid = crypto.randomUUID();
const machina = new Machina<NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED>(opts.signal);
const unsub = this.pool.subscribe(
filters,
this.relays,
(event: NostrEvent | null) => {
if (event && matchFilters(filters, event)) {
machina.push(['EVENT', uuid, purifyEvent(event)]);
}
},
undefined,
() => {
machina.push(['EOSE', uuid]);
},
);
try {
for await (const msg of machina) {
yield msg;
}
} finally {
unsub();
}
}
async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise<NostrEvent[]> {
const events = new NSet();
const limit = filters.reduce((result, filter) => result + getFilterLimit(filter), 0);
if (limit === 0) return [];
for await (const msg of this.req(filters, opts)) {
if (msg[0] === 'EOSE') break;
if (msg[0] === 'EVENT') events.add(msg[2]);
if (msg[0] === 'CLOSED') throw new Error('Subscription closed');
if (events.size >= limit) {
break;
}
}
return [...events];
}
}
export { PoolStore };

View file

@ -62,3 +62,7 @@ export async function getTestDB() {
[Symbol.asyncDispose]: () => kysely.destroy(), [Symbol.asyncDispose]: () => kysely.destroy(),
}; };
} }
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View file

@ -14,6 +14,7 @@ import { RelayError } from '@/RelayError.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';
import { purifyEvent } from '@/storages/hydrate.ts';
const debug = Debug('ditto:api'); const debug = Debug('ditto:api');
@ -152,7 +153,7 @@ async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEven
try { try {
await pipeline.handleEvent(event, c.req.raw.signal); await pipeline.handleEvent(event, c.req.raw.signal);
const client = await Storages.client(); const client = await Storages.client();
await client.event(event); await client.event(purifyEvent(event));
} catch (e) { } catch (e) {
if (e instanceof RelayError) { if (e instanceof RelayError) {
throw new HTTPException(422, { throw new HTTPException(422, {

29
src/utils/outbox.test.ts Normal file
View file

@ -0,0 +1,29 @@
import { MockRelay } from '@nostrify/nostrify/test';
import { eventFixture } from '@/test.ts';
import { getRelays } from '@/utils/outbox.ts';
import { assertEquals } from '@std/assert';
Deno.test('Get write relays - kind 10002', async () => {
const db = new MockRelay();
const relayListMetadata = await eventFixture('kind-10002-alex');
await db.event(relayListMetadata);
const relays = await getRelays(db, relayListMetadata.pubkey);
assertEquals(relays.size, 6);
});
Deno.test('Get write relays with invalid URL - kind 10002', async () => {
const db = new MockRelay();
const relayListMetadata = await eventFixture('kind-10002-alex');
relayListMetadata.tags[0] = ['r', 'yolo'];
await db.event(relayListMetadata);
const relays = await getRelays(db, relayListMetadata.pubkey);
assertEquals(relays.size, 5);
});

View file

@ -2,11 +2,11 @@ import { NStore } from '@nostrify/nostrify';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
export async function getRelays(store: NStore, _pubkey: string): Promise<Set<string>> { export async function getRelays(store: NStore, pubkey: string): Promise<Set<string>> {
const relays = new Set<`wss://${string}`>(); const relays = new Set<`wss://${string}`>();
const events = await store.query([ const events = await store.query([
{ kinds: [10002], authors: [/*pubkey, */ Conf.pubkey], limit: 2 }, { kinds: [10002], authors: [pubkey, Conf.pubkey], limit: 2 },
]); ]);
for (const event of events) { for (const event of events) {