diff --git a/deno.lock b/deno.lock
new file mode 100644
index 0000000..d908cee
--- /dev/null
+++ b/deno.lock
@@ -0,0 +1,250 @@
+{
+ "version": "3",
+ "packages": {
+ "specifiers": {
+ "npm:drizzle-orm@0.33.0": "npm:drizzle-orm@0.33.0_pg@8.12.0",
+ "npm:pg@8.12.0": "npm:pg@8.12.0"
+ },
+ "npm": {
+ "drizzle-orm@0.33.0_pg@8.12.0": {
+ "integrity": "sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==",
+ "dependencies": {
+ "pg": "pg@8.12.0"
+ }
+ },
+ "pg-cloudflare@1.1.1": {
+ "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
+ "dependencies": {}
+ },
+ "pg-connection-string@2.6.4": {
+ "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==",
+ "dependencies": {}
+ },
+ "pg-int8@1.0.1": {
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "dependencies": {}
+ },
+ "pg-pool@3.6.2_pg@8.12.0": {
+ "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
+ "dependencies": {
+ "pg": "pg@8.12.0"
+ }
+ },
+ "pg-protocol@1.6.1": {
+ "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==",
+ "dependencies": {}
+ },
+ "pg-types@2.2.0": {
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "dependencies": {
+ "pg-int8": "pg-int8@1.0.1",
+ "postgres-array": "postgres-array@2.0.0",
+ "postgres-bytea": "postgres-bytea@1.0.0",
+ "postgres-date": "postgres-date@1.0.7",
+ "postgres-interval": "postgres-interval@1.2.0"
+ }
+ },
+ "pg@8.12.0": {
+ "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
+ "dependencies": {
+ "pg-cloudflare": "pg-cloudflare@1.1.1",
+ "pg-connection-string": "pg-connection-string@2.6.4",
+ "pg-pool": "pg-pool@3.6.2_pg@8.12.0",
+ "pg-protocol": "pg-protocol@1.6.1",
+ "pg-types": "pg-types@2.2.0",
+ "pgpass": "pgpass@1.0.5"
+ }
+ },
+ "pgpass@1.0.5": {
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "dependencies": {
+ "split2": "split2@4.2.0"
+ }
+ },
+ "postgres-array@2.0.0": {
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "dependencies": {}
+ },
+ "postgres-bytea@1.0.0": {
+ "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+ "dependencies": {}
+ },
+ "postgres-date@1.0.7": {
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "dependencies": {}
+ },
+ "postgres-interval@1.2.0": {
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "dependencies": {
+ "xtend": "xtend@4.0.2"
+ }
+ },
+ "split2@4.2.0": {
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "dependencies": {}
+ },
+ "xtend@4.0.2": {
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dependencies": {}
+ }
+ }
+ },
+ "redirects": {
+ "https://esm.sh/@aws-sdk/client-s3@^3.592.0": "https://esm.sh/@aws-sdk/client-s3@3.645.0",
+ "https://esm.sh/@aws-sdk/s3-request-presigner@^3.592.0": "https://esm.sh/@aws-sdk/s3-request-presigner@3.645.0"
+ },
+ "remote": {
+ "https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9",
+ "https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48",
+ "https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
+ "https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee",
+ "https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c",
+ "https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9",
+ "https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227",
+ "https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7",
+ "https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6",
+ "https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63",
+ "https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c",
+ "https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c",
+ "https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b",
+ "https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4",
+ "https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848",
+ "https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b",
+ "https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754",
+ "https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22",
+ "https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0",
+ "https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99",
+ "https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54",
+ "https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057",
+ "https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265",
+ "https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c",
+ "https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd",
+ "https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56",
+ "https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece",
+ "https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278",
+ "https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085",
+ "https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a",
+ "https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536",
+ "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2",
+ "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
+ "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
+ "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
+ "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
+ "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
+ "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
+ "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
+ "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
+ "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
+ "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
+ "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
+ "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
+ "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
+ "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
+ "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
+ "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
+ "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
+ "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
+ "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
+ "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
+ "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
+ "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
+ "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
+ "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
+ "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
+ "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
+ "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
+ "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
+ "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
+ "https://deno.land/std@0.224.0/collections/_utils.ts": "b2ec8ada31b5a72ebb1d99774b849b4c09fe4b3a38d07794bd010bd218a16e0b",
+ "https://deno.land/std@0.224.0/collections/deep_merge.ts": "04f8d2a6cfa15c7580e788689bcb5e162512b9ccb18bab1241824b432a78551e",
+ "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
+ "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
+ "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
+ "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
+ "https://deno.land/x/b64@1.1.28/src/base64.js": "c81768c67f6f461b01d10ec24c6c4da71e2f12b3c96e32c62146c98c69685101",
+ "https://esm.sh/@aws-sdk/client-s3@3.645.0": "2e295b9079e0a711af7e5db24b02d36c8a3670796893253e46292dfaff59b8f5",
+ "https://esm.sh/@aws-sdk/s3-request-presigner@3.645.0": "03cf57cb951aece8cb946fb31f910b5d96fcb54aadc15973cee8fa079a9783a1",
+ "https://esm.sh/nanoevents@9.0.0": "29ccd1d8839f2d7dd8d28ad9ec5d18723a7dbf966bf29179063442b1e88b3a4f",
+ "https://esm.sh/v135/@aws-crypto/crc32@5.2.0/denonext/crc32.mjs": "6a9bc8418c01e2539665b528ccea843f1319a3b32d759fcbb1d4468156c25100",
+ "https://esm.sh/v135/@aws-crypto/crc32c@5.2.0/denonext/crc32c.mjs": "1e8985997bd2c0807d349acaf192a54147d779e5349faf6507f51aa8becb85ca",
+ "https://esm.sh/v135/@aws-crypto/sha1-browser@5.2.0/denonext/sha1-browser.mjs": "d80868d5524769e0334b50124d547ce9875fb05f9924acca4c42ed877b41ce7f",
+ "https://esm.sh/v135/@aws-crypto/sha256-browser@5.2.0/denonext/sha256-browser.mjs": "84e59b20eb4974a23fafdcf5fcd6513757ad195ca809b80d19a389602cff335a",
+ "https://esm.sh/v135/@aws-crypto/sha256-js@5.2.0/denonext/sha256-js.mjs": "2e1014e03baf7b5eb5d773c8409af836dacbec2c0a522b789774f76d3eb2e5ad",
+ "https://esm.sh/v135/@aws-crypto/supports-web-crypto@5.2.0/denonext/supports-web-crypto.mjs": "2ae3bd2aa25db0761277ad0feda7aea68cd829c89b714e8e03e07aac06345d81",
+ "https://esm.sh/v135/@aws-crypto/util@5.2.0/denonext/util.mjs": "376903ba54e09eed466b45e243cef1133f20bf015c0505e70fc794896d1412d5",
+ "https://esm.sh/v135/@aws-sdk/client-s3@3.645.0/denonext/client-s3.mjs": "9913ffe288034103e23f7b33a39620b53e5964a6954013df6c362e65ed2e8f5f",
+ "https://esm.sh/v135/@aws-sdk/core@3.635.0/denonext/client.js": "8a39588a5d58924ebc0fbe17dcb8b9a72d16372ea5a19a8ad57087f9bff48683",
+ "https://esm.sh/v135/@aws-sdk/core@3.635.0/denonext/core.mjs": "7cf3ea701618416bd56c43488b12adf12d564bfe317850baaf54cb6d123e4246",
+ "https://esm.sh/v135/@aws-sdk/core@3.635.0/denonext/httpAuthSchemes.js": "0618ba15447abfa957d5ec60b3deb280c8c4e806b96e4a66110226e2fbc4eb0d",
+ "https://esm.sh/v135/@aws-sdk/core@3.635.0/denonext/protocols.js": "c2ea1228ca889c7dfcdbc36a4924a14507eb6d8c40d44f0e060d8e600d94af73",
+ "https://esm.sh/v135/@aws-sdk/middleware-expect-continue@3.620.0/denonext/middleware-expect-continue.mjs": "7257dc7aa9fd7a34fc44b5f8b2460cadfdd72b2e8d7a54d2027a69d1e94c902e",
+ "https://esm.sh/v135/@aws-sdk/middleware-flexible-checksums@3.620.0/denonext/middleware-flexible-checksums.mjs": "13e3af9f03eae1deb232c6201bac2eabbf986c2bb6f5cfbd80c06988172e5cd6",
+ "https://esm.sh/v135/@aws-sdk/middleware-host-header@3.620.0/denonext/middleware-host-header.mjs": "1e2c8804ebfb981b393e843ada215a2f2a5faf82f92ebe8906794bb0d1f09338",
+ "https://esm.sh/v135/@aws-sdk/middleware-location-constraint@3.609.0/denonext/middleware-location-constraint.mjs": "ba8c934c030e5168ad09260026bae3b5f538eca8c50b528fb3b6e945967b7f36",
+ "https://esm.sh/v135/@aws-sdk/middleware-logger@3.609.0/denonext/middleware-logger.mjs": "2105c33b2e62ed2567b20a71438f8f1409220f7bd0426910b0bccf5b84316b84",
+ "https://esm.sh/v135/@aws-sdk/middleware-recursion-detection@3.620.0/denonext/middleware-recursion-detection.mjs": "e4b76653eb33598813018b3d924a4d7ff86243a7bd4d818ac7a194d147e7a267",
+ "https://esm.sh/v135/@aws-sdk/middleware-sdk-s3@3.635.0/denonext/middleware-sdk-s3.mjs": "19d026384d6c2223ef650a5f6791da38f2cf93612a2f3f2474bca2c78c002a19",
+ "https://esm.sh/v135/@aws-sdk/middleware-ssec@3.609.0/denonext/middleware-ssec.mjs": "55d27e9c5fcdd0f4bf2cf7b8f0c6b834d4b3cba6c044de9a57cc0419c58d64bf",
+ "https://esm.sh/v135/@aws-sdk/middleware-user-agent@3.645.0/denonext/middleware-user-agent.mjs": "a506307c80af68bf9618d5eb8810603ec03810aa1ea9086ed57653745517f89c",
+ "https://esm.sh/v135/@aws-sdk/region-config-resolver@3.614.0/denonext/region-config-resolver.mjs": "580b2f14c0d72423f166859afd2441fdf3883f7a3ab86c36d746a159029d40fd",
+ "https://esm.sh/v135/@aws-sdk/s3-request-presigner@3.645.0/denonext/s3-request-presigner.mjs": "57125a72c13a69f88078aa6505ef6088efa4c773604463a08b9be275996c38ae",
+ "https://esm.sh/v135/@aws-sdk/signature-v4-multi-region@3.635.0/denonext/signature-v4-multi-region.mjs": "de9c08397d25f620680522d022422ebb30cc534d44cc91592f31922ec3f9bc88",
+ "https://esm.sh/v135/@aws-sdk/util-arn-parser@3.568.0/denonext/util-arn-parser.mjs": "e80995eaf790640e591f09d89d9099b022efa6d7954d6e23a1a7f5691b9b5110",
+ "https://esm.sh/v135/@aws-sdk/util-endpoints@3.645.0/denonext/util-endpoints.mjs": "c72e746a164f107dbe5d43f4e175635cd0bde6f25bf41852134d4622a5e0cd58",
+ "https://esm.sh/v135/@aws-sdk/util-format-url@3.609.0/denonext/util-format-url.mjs": "097aa6da9b813dfd68e0bdcd25391d7e77ae808911463309604f8022ac38ab0b",
+ "https://esm.sh/v135/@aws-sdk/util-locate-window@3.568.0/denonext/util-locate-window.mjs": "44c4acffec7669f2d0e0307ebfca7cac1f85260a6f8238dcbeb5e79f769e6f00",
+ "https://esm.sh/v135/@aws-sdk/util-user-agent-browser@3.609.0/denonext/util-user-agent-browser.mjs": "47329052476de081fa1bd227be1f83dd1ed360162aecae204218295bf9dc5ab5",
+ "https://esm.sh/v135/@aws-sdk/xml-builder@3.609.0/denonext/xml-builder.mjs": "1822a0c319298642be9cdac624fadf1c77392d02f6b33fb9e36b27738de5fcc6",
+ "https://esm.sh/v135/@smithy/chunked-blob-reader@3.0.0/denonext/chunked-blob-reader.mjs": "bfd33430ff0d1b7c3dc6e42401a2adfcdeaf2dbb9ac56ca6578782c99e2cb359",
+ "https://esm.sh/v135/@smithy/config-resolver@3.0.5/denonext/config-resolver.mjs": "0ccf80d6a6427058db95154498485b6a5ae77d12c4fdae48406c9a60b41afe2b",
+ "https://esm.sh/v135/@smithy/core@2.4.0/denonext/core.mjs": "3ad714d4c1fdb7dcffd91936255289197d6bf0523f13d36bb94e9ce1fd1756d5",
+ "https://esm.sh/v135/@smithy/eventstream-codec@3.1.2/denonext/eventstream-codec.mjs": "8ea933c44dc8baa334f47b1c2b70a9bf2a14836f9fab720b1125664fb26c4527",
+ "https://esm.sh/v135/@smithy/eventstream-serde-browser@3.0.6/denonext/eventstream-serde-browser.mjs": "c66e37b7b31c63ff96977e1627d8016b543fccdf95a4f9e388da378d61ce7d0f",
+ "https://esm.sh/v135/@smithy/eventstream-serde-config-resolver@3.0.3/denonext/eventstream-serde-config-resolver.mjs": "0960eeb9f45540bca3281e9d539b75ed114891b8453c91c2c87dee294387d81d",
+ "https://esm.sh/v135/@smithy/eventstream-serde-universal@3.0.5/denonext/eventstream-serde-universal.mjs": "ff38bc5052d81372e0cb60749c44354488002d3b0dba292e3626f195bc560ac8",
+ "https://esm.sh/v135/@smithy/fetch-http-handler@3.2.4/denonext/fetch-http-handler.mjs": "7890ad9cef41a0b0a1a5440153108391d5e5f995a39a028637b3cef271c76075",
+ "https://esm.sh/v135/@smithy/hash-blob-browser@3.1.2/denonext/hash-blob-browser.mjs": "39b8b23e12aafc146af0af6954ff957752343c9c040d09bda1a0cf4aa5de52fa",
+ "https://esm.sh/v135/@smithy/invalid-dependency@3.0.3/denonext/invalid-dependency.mjs": "99f4bdd11680348113a0acd593a7f402a33d10654cf4218b5b0f967dbcdae19e",
+ "https://esm.sh/v135/@smithy/is-array-buffer@3.0.0/denonext/is-array-buffer.mjs": "f8bb7f850b646a10880d4e52c60151913b7d81911b2b1cd1355c9adef56ab3e2",
+ "https://esm.sh/v135/@smithy/md5-js@3.0.3/denonext/md5-js.mjs": "6f4d21d0d4e09cce9245a4e3bddb899b40da3a1c0ce9a8fd12b8f8ac09375857",
+ "https://esm.sh/v135/@smithy/middleware-content-length@3.0.5/denonext/middleware-content-length.mjs": "bce550610386d8945899345a97f9aabb00976d7db378a51c463c043008e0f6df",
+ "https://esm.sh/v135/@smithy/middleware-endpoint@3.1.0/denonext/middleware-endpoint.mjs": "becfe2cb560079a86b0102a3a817c3a6b6f61d7ed1b7f65b6b28ae772871e638",
+ "https://esm.sh/v135/@smithy/middleware-retry@3.0.15/denonext/middleware-retry.mjs": "2d6b23bdb5ce62336afc02d045d8bb1bf0832fa8eafb022d500372c08b8ea6cb",
+ "https://esm.sh/v135/@smithy/middleware-serde@3.0.3/denonext/middleware-serde.mjs": "2513b3aaa3f35cf0c33841550aa23b4f4ab4d645d60f86c7a173a11b2b0c9b7a",
+ "https://esm.sh/v135/@smithy/middleware-stack@3.0.3/denonext/middleware-stack.mjs": "a84a0dda6e1d402ba69cba6747643d6d3f0f3532ac263beb0920f0f5f34ed53c",
+ "https://esm.sh/v135/@smithy/property-provider@3.1.3/denonext/property-provider.mjs": "8fbecd9b01ba1486726b9f43559926332389b292f276a10708239b1bb666c819",
+ "https://esm.sh/v135/@smithy/protocol-http@4.1.0/denonext/protocol-http.mjs": "8dc60c296a28eea35bb0c394d3cfdb22bb81385424e0c1099bbda21d38ff132c",
+ "https://esm.sh/v135/@smithy/querystring-builder@3.0.3/denonext/querystring-builder.mjs": "26803f47afc07fdcfb0506cb95235db97250abfb6e5e31311d4d3e34356ffd45",
+ "https://esm.sh/v135/@smithy/querystring-parser@3.0.3/denonext/querystring-parser.mjs": "1186ec8e490e5eb9a911945652400304ab9a2128e13734f80717e59f455d0b3b",
+ "https://esm.sh/v135/@smithy/service-error-classification@3.0.3/denonext/service-error-classification.mjs": "46b409a7d492acacb936ecae2c05e8e11e4910146f6eb2f290067b3cdae8410b",
+ "https://esm.sh/v135/@smithy/signature-v4@4.1.0/denonext/signature-v4.mjs": "d4adec6b85e442a4dbce5bc391d3856ef202f00f2bedc37f12d5f40fec050e69",
+ "https://esm.sh/v135/@smithy/smithy-client@3.2.0/denonext/smithy-client.mjs": "2f051fcd8addfba2786c6d712cb8ce443c25b32f2ba258d1d0a46f550eb31451",
+ "https://esm.sh/v135/@smithy/types@3.3.0/denonext/types.mjs": "0b82ba4c0d421c6476ac68730acdd7a0c9bd014d34c9c556b627fd1c06673eb3",
+ "https://esm.sh/v135/@smithy/url-parser@3.0.3/denonext/url-parser.mjs": "69067083fcbb733d78ff55e0a1b39852dba3c893e436868fc062829fec623cd8",
+ "https://esm.sh/v135/@smithy/util-base64@3.0.0/denonext/util-base64.mjs": "d6a01faaa94fdbeb4b92b02e91801dfbe241439e37a0edf7d817c59daf66c0e3",
+ "https://esm.sh/v135/@smithy/util-body-length-browser@3.0.0/denonext/util-body-length-browser.mjs": "d67382004d61919b97a756a454f9b312cfb0011a9727d3d1ca69ebddf1c7843a",
+ "https://esm.sh/v135/@smithy/util-config-provider@3.0.0/denonext/util-config-provider.mjs": "832c0ab1d3b06a51351ea23b33628bd36a37ef570e02e469f6ab39f71d88d7b1",
+ "https://esm.sh/v135/@smithy/util-defaults-mode-browser@3.0.15/denonext/util-defaults-mode-browser.mjs": "9c1088619d3fe879e13cd81f97e26a605c1fe6d28aa0e47022441a6229965a1d",
+ "https://esm.sh/v135/@smithy/util-endpoints@2.0.5/denonext/util-endpoints.mjs": "3876bd3404b820a5fab88bbe3f8ba2a8e373bb0099c9838617ec88f898dd78d0",
+ "https://esm.sh/v135/@smithy/util-hex-encoding@3.0.0/denonext/util-hex-encoding.mjs": "cbdd7aabeb3903596980e2903efec3e5501f7e1259fb7b97e327a3b4e635f23c",
+ "https://esm.sh/v135/@smithy/util-middleware@3.0.3/denonext/util-middleware.mjs": "a885e613b933ce02c7c73507e80ef5b81374a55a647cc4bc397bb1f19284a95b",
+ "https://esm.sh/v135/@smithy/util-retry@3.0.3/denonext/util-retry.mjs": "2bc452ea87cbe471e2bee783776d528fec4afcd083367c1dafd8936e229c64f3",
+ "https://esm.sh/v135/@smithy/util-stream@3.1.3/denonext/util-stream.mjs": "13b6b4e3c10e0a0586e6fca8a7e3d2d8fea840aecb413337c2d75c0fceb75f37",
+ "https://esm.sh/v135/@smithy/util-uri-escape@3.0.0/denonext/util-uri-escape.mjs": "df2c80781ede692323dee6e2da3711e7ccc4f7a1cee949b09aba8d1ce15bbe03",
+ "https://esm.sh/v135/@smithy/util-utf8@2.0.2/denonext/util-utf8.mjs": "d1869dca8a21b3e6c297cb55f90e1b78bf8f365afd1f173c16d719f28245604b",
+ "https://esm.sh/v135/@smithy/util-utf8@2.3.0/denonext/util-utf8.mjs": "10a9f2014b2b5b2e387e04c1c7974e8219332fa30a6904923f54a46c974c6c84",
+ "https://esm.sh/v135/@smithy/util-utf8@3.0.0/denonext/util-utf8.mjs": "abe704ed8c4266b29906116ef723b98e8729078537b252c9a213ad373559488a",
+ "https://esm.sh/v135/@smithy/util-waiter@3.1.2/denonext/util-waiter.mjs": "8bff673e4c8b620b34f59cbfa0e6c92de95b3c00190861b5b2cb113923bf8288",
+ "https://esm.sh/v135/bowser@2.11.0/denonext/bowser.mjs": "3fd0c5d68c4bb8b3243c1b0ac76442fa90f5e20ee12773ce2b2f476c2e7a3615",
+ "https://esm.sh/v135/fast-xml-parser@4.4.1/denonext/fast-xml-parser.mjs": "506f0ae0ce83e4664b4e2a3bf3cde30b3d44c019012938ab12b76fa38353e864",
+ "https://esm.sh/v135/nanoevents@9.0.0/denonext/nanoevents.mjs": "666c9d584019a64758bd3071e561051747454da64299ac06b79ede7210fe5e85",
+ "https://esm.sh/v135/strnum@1.0.5/denonext/strnum.mjs": "1ffef4adec2f74139e36a2bfed8381880541396fe1c315779fb22e081b17468b",
+ "https://esm.sh/v135/tslib@2.6.2/denonext/tslib.mjs": "29782bcd3139f77ec063dc5a9385c0fff4a8d0a23b6765c73d9edeb169a04bf1",
+ "https://esm.sh/v135/tslib@2.6.3/denonext/tslib.mjs": "0834c22e9fbf95f6a5659cc2017543f7d41aa880f24ab84cb11d24e6bee99303",
+ "https://esm.sh/v135/uuid@9.0.1/denonext/uuid.mjs": "7d7d3aa57fa136e2540886654c416d9da10d8cfebe408bae47fd47070f0bfb2a",
+ "https://esm.sh/v135/zod-validation-error@3.3.0/denonext/zod-validation-error.mjs": "4efabd593e1430c31a044f79d299a62120946a3e701159b29922b50f3223c186",
+ "https://esm.sh/v135/zod@3.23.8/denonext/zod.mjs": "b3707b03ddc01aab11b740436ab23c0fcc8d15fed072be20085c1fd611016b61",
+ "https://esm.sh/zod-validation-error@3.3.0": "d8825ca67952b6adff6b35026dc465f9638d4923dbd54fe9e8e81fbfddca9630",
+ "https://esm.sh/zod@3.23.8": "728819c1f651800179a5a80daf24b3e54b2ddea87828bd10e63875a604bcb94e"
+ }
+}
diff --git a/modules/captcha/actors/throttle.ts b/modules/captcha/actors/throttle.ts
new file mode 100644
index 0000000..0acf905
--- /dev/null
+++ b/modules/captcha/actors/throttle.ts
@@ -0,0 +1,44 @@
+import { ActorBase, ActorContext, Empty } from "../module.gen.ts";
+import { ThrottleRequest, ThrottleResponse } from "../utils/types.ts";
+
+type Input = undefined;
+
+interface State {
+ start: number;
+ count: number;
+}
+
+export class Actor extends ActorBase {
+ public initialize(_ctx: ActorContext): State {
+ // Will refill on first call of `throttle`
+ return {
+ start: 0,
+ count: 0,
+ };
+ }
+
+ throttle(_ctx: ActorContext, req: ThrottleRequest): ThrottleResponse {
+ const now = Date.now();
+
+ if (now - this.state.start > req.period) {
+ this.state.start = now;
+ this.state.count = 1;
+ return { success: true };
+ }
+
+ if (this.state.count >= req.requests) {
+ return { success: false };
+ }
+
+ this.state.count += 1;
+
+ return { success: true };
+ }
+
+ reset(_ctx: ActorContext, req: Empty): Empty {
+ this.state.start = 0;
+ this.state.count = 0;
+
+ return {};
+ }
+}
diff --git a/modules/captcha/module.json b/modules/captcha/module.json
new file mode 100644
index 0000000..5cb0b9c
--- /dev/null
+++ b/modules/captcha/module.json
@@ -0,0 +1,34 @@
+{
+ "status": "stable",
+ "name": "Captcha",
+ "description": "",
+ "icon": "",
+ "tags": [],
+ "authors": [
+ "rivet-gg",
+ "ABCxFF"
+ ],
+ "scripts": {
+ "verify_captcha_token": {
+ "name": "Verify Captcha Response",
+ "public": false
+ },
+ "guard": {
+ "name": "Ratelimit Guarded with Captcha Challenge",
+ "public": false
+ }
+ },
+ "errors": {
+ "captcha_failed": {
+ "name": "Captcha Challenge Failed",
+ "internal": false
+ },
+ "captcha_needed": {
+ "name": "Captcha Required (Rate Limit Exceeded)",
+ "internal": false
+ }
+ },
+ "actors": {
+ "throttle": {}
+ }
+}
\ No newline at end of file
diff --git a/modules/captcha/scripts/guard.ts b/modules/captcha/scripts/guard.ts
new file mode 100644
index 0000000..5f28047
--- /dev/null
+++ b/modules/captcha/scripts/guard.ts
@@ -0,0 +1,54 @@
+import { RuntimeError, ScriptContext } from "../module.gen.ts";
+import { getPublicConfig } from "../utils/get_sitekey.ts";
+// import { getPublicConfig } from "../utils/get_sitekey.ts";
+import type { CaptchaProvider, ThrottleRequest, ThrottleResponse } from "../utils/types.ts";
+
+export interface Request {
+ type: string;
+ key: string;
+ requests: number;
+ period: number;
+ captchaToken?: string | null,
+ captchaProvider: CaptchaProvider
+}
+
+export type Response = Record;
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ const key = `${JSON.stringify(req.type)}.${JSON.stringify(req.key)}`;
+
+ if (req.captchaToken) {
+ try {
+ await ctx.modules.captcha.verifyCaptchaToken({
+ token: req.captchaToken,
+ provider: req.captchaProvider
+ });
+
+ await ctx.actors.throttle.getOrCreateAndCall(key, undefined, "reset", {});
+
+ return {};
+ } catch {
+ // If we error, it means the captcha failed, we can continue with our normal ratelimitting
+ }
+ }
+
+ const res = await ctx.actors.throttle.getOrCreateAndCall<
+ undefined,
+ ThrottleRequest,
+ ThrottleResponse
+ >(key, undefined, "throttle", {
+ requests: req.requests,
+ period: req.period,
+ });
+
+ if (!res.success) {
+ throw new RuntimeError("captcha_needed", {
+ meta: getPublicConfig(req.captchaProvider)
+ });
+ }
+
+ return {};
+}
\ No newline at end of file
diff --git a/modules/captcha/scripts/verify_captcha_token.ts b/modules/captcha/scripts/verify_captcha_token.ts
new file mode 100644
index 0000000..47832e2
--- /dev/null
+++ b/modules/captcha/scripts/verify_captcha_token.ts
@@ -0,0 +1,36 @@
+import { RuntimeError, ScriptContext } from "../module.gen.ts";
+import { validateHCaptchaResponse } from "../utils/providers/hcaptcha.ts";
+import { validateCFTurnstileResponse } from "../utils/providers/turnstile.ts";
+// import { validateHCaptchaResponse } from "../providers/hcaptcha.ts";
+// import { validateCFTurnstileResponse } from "../providers/turnstile.ts";
+import { CaptchaProvider } from "../utils/types.ts";
+
+export interface Request {
+ token: string,
+ provider: CaptchaProvider
+}
+
+export type Response = Record;
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ const captchaToken = req.token;
+ const captchaProvider = req.provider;
+
+ let success: boolean = false;
+ if ("hcaptcha" in captchaProvider) {
+ success = await validateHCaptchaResponse(captchaProvider.hcaptcha.secret, captchaToken);
+ } else if ("turnstile" in captchaProvider) {
+ success = await validateCFTurnstileResponse(captchaProvider.turnstile.secret, captchaToken);
+ } else {
+ success = true;
+ }
+
+ if (!success) {
+ throw new RuntimeError("captcha_failed");
+ }
+
+ return {};
+}
\ No newline at end of file
diff --git a/modules/captcha/tests/e2e_guard.ts b/modules/captcha/tests/e2e_guard.ts
new file mode 100644
index 0000000..824fc67
--- /dev/null
+++ b/modules/captcha/tests/e2e_guard.ts
@@ -0,0 +1,45 @@
+import { test, TestContext } from "../module.gen.ts";
+import { assertEquals } from "https://deno.land/std@0.217.0/assert/mod.ts";
+
+const didFail = async (x: () => Promise) => {
+ try {
+ await x();
+ return false
+ } catch {
+ return true;
+ }
+}
+
+test("e2e success and failure", async (ctx: TestContext) => {
+ const PERIOD = 5000;
+ const REQUESTS = 5;
+
+ const captchaProvider = {
+ turnstile: {
+ secret: "0x0000000000000000000000000000000000000000",
+ sitekey: "" // doesn't really matter here
+ }
+ }
+
+ assertEquals(false, await didFail(async () => {
+ for (let i = 0; i < REQUESTS; ++i) {
+ await ctx.modules.captcha.guard({
+ type: "ip",
+ key: "aaaa",
+ requests: REQUESTS,
+ period: PERIOD,
+ captchaProvider
+ });
+ }
+ }));
+
+ assertEquals(true, await didFail(async () => {
+ await ctx.modules.captcha.guard({
+ type: "ip",
+ key: "aaaa",
+ requests: REQUESTS,
+ period: PERIOD,
+ captchaProvider
+ });
+ }));
+});
\ No newline at end of file
diff --git a/modules/captcha/tests/e2e_verify_token.ts b/modules/captcha/tests/e2e_verify_token.ts
new file mode 100644
index 0000000..2e014cf
--- /dev/null
+++ b/modules/captcha/tests/e2e_verify_token.ts
@@ -0,0 +1,75 @@
+import { test, TestContext } from "../module.gen.ts";
+import { assertEquals } from "https://deno.land/std@0.217.0/assert/mod.ts";
+
+const didFail = async (x: () => Promise) => {
+ try {
+ await x();
+ return false
+ } catch {
+ return true;
+ }
+}
+
+test(
+ "hcaptcha success and failure",
+ async (ctx: TestContext) => {
+ const shouldBeFalse = await didFail(async () => {
+ await ctx.modules.captcha.verifyCaptchaToken({
+ provider: {
+ hcaptcha: {
+ secret: "0x0000000000000000000000000000000000000000",
+ sitekey: "" // doesn't really matter here
+ }
+ },
+ token: "10000000-aaaa-bbbb-cccc-000000000001"
+ });
+ });
+ assertEquals(shouldBeFalse, false);
+
+ const shouldBeTrue = await didFail(async () => {
+ await ctx.modules.captcha.verifyCaptchaToken({
+ provider: {
+ hcaptcha: {
+ secret: "0x0000000000000000000000000000000000000000",
+ sitekey: "" // doesn't really matter here
+ }
+ },
+ token: "lorem"
+ });
+ });
+ assertEquals(shouldBeTrue, true);
+ },
+);
+
+test(
+ "turnstile success and failure",
+ async (ctx: TestContext) => {
+ // Always passes
+ const shouldBeTrue = await didFail(async () => {
+ await ctx.modules.captcha.verifyCaptchaToken({
+ provider: {
+ turnstile: {
+ secret: "2x0000000000000000000000000000000AA",
+ sitekey: "" // doesn't really matter here
+ }
+ },
+ token: "lorem"
+ });
+ });
+ assertEquals(shouldBeTrue, true);
+
+ // Always fails
+ const shouldBeFalse = await didFail(async () => {
+ await ctx.modules.captcha.verifyCaptchaToken({
+ provider: {
+ turnstile: {
+ secret: "1x0000000000000000000000000000000AA",
+ sitekey: "" // doesn't really matter here
+ }
+ },
+ token: "ipsum"
+ });
+ });
+ assertEquals(shouldBeFalse, false);
+ },
+);
diff --git a/modules/captcha/utils/get_sitekey.ts b/modules/captcha/utils/get_sitekey.ts
new file mode 100644
index 0000000..8930e45
--- /dev/null
+++ b/modules/captcha/utils/get_sitekey.ts
@@ -0,0 +1,19 @@
+import { CaptchaProvider, PublicCaptchaProviderConfig } from "./types.ts";
+
+export const getPublicConfig = (provider: CaptchaProvider): PublicCaptchaProviderConfig => {
+ if ("hcaptcha" in provider) {
+ return {
+ hcaptcha: { sitekey: provider.hcaptcha.sitekey }
+ };
+ } else if ("turnstile" in provider) {
+ return {
+ turnstile: {
+ sitekey: provider.turnstile.sitekey
+ }
+ }
+ } else {
+ return {
+ test: {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/captcha/utils/providers/hcaptcha.ts b/modules/captcha/utils/providers/hcaptcha.ts
new file mode 100644
index 0000000..9de6903
--- /dev/null
+++ b/modules/captcha/utils/providers/hcaptcha.ts
@@ -0,0 +1,21 @@
+const API = "https://api.hcaptcha.com/siteverify";
+export const validateHCaptchaResponse = async (
+ secret: string,
+ response: string
+): Promise => {
+ try {
+ const body = new FormData();
+ body.append("secret", secret);
+ body.append("response", response);
+ const result = await fetch(API, {
+ body,
+ method: "POST",
+ });
+
+ const { success } = await result.json();
+
+ return success;
+ } catch {}
+
+ return false;
+}
\ No newline at end of file
diff --git a/modules/captcha/utils/providers/turnstile.ts b/modules/captcha/utils/providers/turnstile.ts
new file mode 100644
index 0000000..542a4e1
--- /dev/null
+++ b/modules/captcha/utils/providers/turnstile.ts
@@ -0,0 +1,21 @@
+const API = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
+export const validateCFTurnstileResponse = async (
+ secret: string,
+ response: string
+): Promise => {
+ try {
+ const result = await fetch(API, {
+ body: JSON.stringify({ secret, response }),
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ }
+ });
+
+ const { success } = await result.json();
+
+ return success;
+ } catch {}
+
+ return false;
+}
\ No newline at end of file
diff --git a/modules/captcha/utils/types.ts b/modules/captcha/utils/types.ts
new file mode 100644
index 0000000..1e17b81
--- /dev/null
+++ b/modules/captcha/utils/types.ts
@@ -0,0 +1,31 @@
+
+
+interface ProviderCFTurnstile {
+ sitekey: string;
+ secret: string;
+}
+
+interface ProviderHCaptcha {
+ // TODO: Score threshold
+ sitekey: string;
+ secret: string;
+}
+type PublicCFTurnstileConfig = { sitekey: string; }
+type PublicHCaptchaConfig = { sitekey: string; }
+
+export type CaptchaProvider = { test: Record }
+ | { turnstile: ProviderCFTurnstile }
+ | { hcaptcha: ProviderHCaptcha };
+
+export type PublicCaptchaProviderConfig = { test: Record }
+ | { turnstile: PublicCFTurnstileConfig }
+ | { hcaptcha: PublicHCaptchaConfig };
+
+export interface ThrottleRequest {
+ requests: number;
+ period: number;
+}
+
+export interface ThrottleResponse {
+ success: boolean;
+}
\ No newline at end of file
diff --git a/tests/basic/backend.json b/tests/basic/backend.json
index 6a6547b..8f40519 100644
--- a/tests/basic/backend.json
+++ b/tests/basic/backend.json
@@ -10,6 +10,9 @@
"achievements": {
"registry": "local"
},
+ "captcha": {
+ "registry": "local"
+ },
"analytics": {
"registry": "local"
},