diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 64f5d6d3..0e9be12e 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 with: egress-policy: "audit" @@ -135,7 +135,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 with: egress-policy: "audit" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07d64dcb..8c80cc7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 with: egress-policy: "audit" diff --git a/.oxlintrc.json b/.oxlintrc.json index 7760fc00..2a2f00b5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,7 +8,7 @@ "oxc/misrefactored-assign-op": "error", "oxc/no-accumulating-spread": "error", "oxc/no-barrel-file": "error", - "oxc/no-const-enum": "warn", + "oxc/no-const-enum": "off", "oxc/no-map-spread": "error", "oxc/no-this-in-exported-function": "error", "typescript/adjacent-overload-signatures": "error", @@ -20,8 +20,8 @@ "typescript/consistent-type-assertions": "error", "typescript/consistent-type-definitions": ["error", "type"], "typescript/dot-notation": "error", - "typescript/explicit-function-return-type": "error", - "typescript/explicit-module-boundary-types": "error", + "typescript/explicit-function-return-type": "off", + "typescript/explicit-module-boundary-types": "off", "typescript/no-confusing-non-null-assertion": "error", "typescript/no-confusing-void-expression": "error", "typescript/no-deprecated": "error", @@ -44,14 +44,14 @@ "typescript/no-unnecessary-type-assertion": "error", "typescript/no-unnecessary-type-constraint": "error", "typescript/no-unnecessary-type-parameters": "error", - "typescript/no-unsafe-argument": "error", - "typescript/no-unsafe-assignment": "error", + "typescript/no-unsafe-argument": "off", + "typescript/no-unsafe-assignment": "off", "typescript/no-unsafe-call": "off", - "typescript/no-unsafe-enum-comparison": "error", - "typescript/no-unsafe-function-type": "error", + "typescript/no-unsafe-enum-comparison": "off", + "typescript/no-unsafe-function-type": "off", "typescript/no-unsafe-member-access": "off", - "typescript/no-unsafe-return": "error", - "typescript/no-unsafe-type-assertion": "error", + "typescript/no-unsafe-return": "off", + "typescript/no-unsafe-type-assertion": "off", "typescript/no-useless-default-assignment": "error", "typescript/non-nullable-type-assertion-style": "error", "typescript/only-throw-error": "error", @@ -76,14 +76,14 @@ "typescript/require-await": "error", "typescript/restrict-plus-operands": "error", "typescript/return-await": "error", - "typescript/strict-boolean-expressions": "error", + "typescript/strict-boolean-expressions": "off", "typescript/strict-void-return": "error", - "typescript/switch-exhaustiveness-check": "error", + "typescript/switch-exhaustiveness-check": "warn", "typescript/unified-signatures": "error", "typescript/use-unknown-in-catch-callback-variable": "error" }, "options": { - // deno incompatibility + // manual check only "typeAware": false } } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b57c724f..19275ea7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ You can get a quick overview with: If using Scalar Client, disable the CORS proxy and follow these steps to import the instance `oas.json`..: -![](https://static.x.inetol.net/jspaste/backend/scalar-t1.webp) +![](https://static.inetol.net/jspaste/backend/scalar-t1.webp) ## Maintenance diff --git a/deno.json b/deno.json index 8a75cb5e..8f944389 100644 --- a/deno.json +++ b/deno.json @@ -3,6 +3,9 @@ "license": "EUPL-1.2", "lock": true, "nodeModulesDir": "auto", + "compilerOptions": { + "lib": ["deno.window", "deno.unstable", "esnext"] + }, "unstable": ["cron", "raw-imports", "bare-node-builtins"], "allowScripts": [], "imports": { @@ -17,25 +20,26 @@ "@hono/openapi": "npm:hono-openapi@^1.3.0", "@hono/standard-validator": "jsr:@hono/standard-validator@~0.2.2", "@std/assert": "jsr:@std/assert@^1.0.19", - "@std/async": "jsr:@std/async@^1.2.0", - "@std/cache": "jsr:@std/cache@~0.2.2", - "@std/collections": "jsr:@std/collections@^1.1.6", + "@std/async": "jsr:@std/async@^1.3.0", + "@std/cache": "jsr:@std/cache@~0.2.3", + "@std/collections": "jsr:@std/collections@^1.1.7", "@std/dotenv": "jsr:@std/dotenv@~0.225.6", "@std/encoding": "jsr:@std/encoding@^1.0.10", - "@std/fmt": "jsr:@std/fmt@^1.0.9", + "@std/fmt": "jsr:@std/fmt@^1.0.10", "@std/fs": "jsr:@std/fs@^1.0.23", "@std/path": "jsr:@std/path@^1.1.4", - "@std/streams": "jsr:@std/streams@^1.0.17", + "@std/streams": "jsr:@std/streams@^1.1.0", "@std/ulid": "jsr:@std/ulid@^1.0.0", - "@types/node": "npm:@types/node@^25.5.2", + "@types/node": "npm:@types/node@^25.6.0", "arkenv": "npm:arkenv@~0.11.0", "arktype": "npm:arktype@^2.2.0", "hash-wasm": "npm:hash-wasm@^4.12.0", - "hono": "jsr:@hono/hono@^4.12.10", - "nanoid": "jsr:@sitnik/nanoid@^5.1.7", - "oxfmt": "npm:oxfmt@^0.43.0", - "oxlint": "npm:oxlint@^1.58.0", - "rolldown": "npm:rolldown@1.0.0-rc.13" + "hono": "jsr:@hono/hono@^4.12.15", + "nanoid": "jsr:@sitnik/nanoid@^5.1.9", + "oxfmt": "npm:oxfmt@^0.46.0", + "oxlint": "npm:oxlint@^1.61.0", + "oxlint-tsgolint": "npm:oxlint-tsgolint@^0.21.1", + "rolldown": "npm:rolldown@1.0.0-rc.17" }, "fmt": { "exclude": ["**"] diff --git a/deno.lock b/deno.lock index 6f026837..5862514a 100644 --- a/deno.lock +++ b/deno.lock @@ -2,39 +2,41 @@ "version": "5", "specifiers": { "jsr:@deno/loader@0.5": "0.5.0", - "jsr:@hono/hono@^4.12.10": "4.12.10", - "jsr:@hono/hono@^4.8.3": "4.12.10", + "jsr:@hono/hono@^4.12.15": "4.12.15", + "jsr:@hono/hono@^4.8.3": "4.12.15", "jsr:@hono/standard-validator@~0.2.2": "0.2.2", - "jsr:@sitnik/nanoid@^5.1.7": "5.1.7", + "jsr:@sitnik/nanoid@^5.1.9": "5.1.9", "jsr:@standard-schema/spec@1": "1.1.0", "jsr:@std/assert@^1.0.19": "1.0.19", - "jsr:@std/async@^1.2.0": "1.2.0", + "jsr:@std/async@^1.3.0": "1.3.0", "jsr:@std/bytes@^1.0.6": "1.0.6", - "jsr:@std/cache@~0.2.2": "0.2.2", - "jsr:@std/collections@^1.1.6": "1.1.6", + "jsr:@std/cache@~0.2.3": "0.2.3", + "jsr:@std/collections@^1.1.7": "1.1.7", + "jsr:@std/data-structures@^1.0.11": "1.0.11", "jsr:@std/dotenv@~0.225.6": "0.225.6", "jsr:@std/encoding@^1.0.10": "1.0.10", - "jsr:@std/fmt@^1.0.9": "1.0.9", + "jsr:@std/fmt@^1.0.10": "1.0.10", "jsr:@std/fs@^1.0.23": "1.0.23", - "jsr:@std/internal@^1.0.12": "1.0.12", + "jsr:@std/internal@^1.0.12": "1.0.13", "jsr:@std/path@^1.1.4": "1.1.4", - "jsr:@std/streams@^1.0.17": "1.0.17", + "jsr:@std/streams@^1.1.0": "1.1.0", "jsr:@std/ulid@1": "1.0.0", - "npm:@types/node@^25.5.2": "25.5.2", + "npm:@types/node@^25.6.0": "25.6.0", "npm:arkenv@0.11": "0.11.0_arktype@2.2.0", "npm:arktype@^2.2.0": "2.2.0", "npm:hash-wasm@^4.12.0": "4.12.0", "npm:hono-openapi@^1.3.0": "1.3.0_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.2.0__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.2.0___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.2.0__openapi-types@12.1.3__@types+json-schema@7.0.15__quansync@0.2.11_@types+json-schema@7.0.15_openapi-types@12.1.3_@standard-schema+spec@1.1.0_arktype@2.2.0_quansync@0.2.11", - "npm:oxfmt@0.43": "0.43.0", - "npm:oxlint@^1.58.0": "1.58.0", - "npm:rolldown@1.0.0-rc.13": "1.0.0-rc.13" + "npm:oxfmt@0.46": "0.46.0", + "npm:oxlint-tsgolint@~0.21.1": "0.21.1", + "npm:oxlint@^1.61.0": "1.61.0_oxlint-tsgolint@0.21.1", + "npm:rolldown@1.0.0-rc.17": "1.0.0-rc.17" }, "jsr": { "@deno/loader@0.5.0": { "integrity": "a6d94408de5e6bacac404f8f6963c8b8cc278cfd1a878aa2f06b34a083d6bfee" }, - "@hono/hono@4.12.10": { - "integrity": "6e608e50bb13fd0933e86fda95d31c1ff57d1c8774ed585d6a8b8fc1bdd82c82" + "@hono/hono@4.12.15": { + "integrity": "06a0aef73d9072e73b8db570b81a5b56a94095d697578202a87fccdf04ce352d" }, "@hono/standard-validator@0.2.2": { "integrity": "bc94e1ab41d677a571cb6dd5012823f1162b9856ca24dfd60233734824bb0b0c", @@ -43,8 +45,8 @@ "jsr:@standard-schema/spec" ] }, - "@sitnik/nanoid@5.1.7": { - "integrity": "e25656f8197843deaac4230a24da5696234da3cf9dc487a626f74996d8d9fd3d" + "@sitnik/nanoid@5.1.9": { + "integrity": "dd15205aa2c8b2f75804a6b91d6b94ad5657dd28306da8eecf28f64e0d7f8583" }, "@standard-schema/spec@1.1.0": { "integrity": "2ccd54513cd9c960bd155ab569b1a901bc99c6f9ad29559d3f38a28c91c1822d" @@ -55,17 +57,23 @@ "jsr:@std/internal" ] }, - "@std/async@1.2.0": { - "integrity": "c059c6f6d95ca7cc012ae8e8d7164d1697113d54b0b679e4372b354b11c2dee5" + "@std/async@1.3.0": { + "integrity": "80485538a4f7baaa46bfe2246168069e02ed142b9f9079cd164f43bb060ad9e9", + "dependencies": [ + "jsr:@std/data-structures" + ] }, "@std/bytes@1.0.6": { "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" }, - "@std/cache@0.2.2": { - "integrity": "4c8fb31c7553837b537f55e26c9c91b2784f9a8c64f43772a7884a059b04d46e" + "@std/cache@0.2.3": { + "integrity": "4e0bcab2e61f7c5637937bfe2bb13ccdd15e4dc3092beb14b78726bea8c49916" }, - "@std/collections@1.1.6": { - "integrity": "b458160ce65ea5ad35da05d0a5cbee4b583677c8b443a10d7beb0c4ac63f2baa" + "@std/collections@1.1.7": { + "integrity": "56f659d011218a69740b12829cf5ea2c9b70bbed0af02597e27dc1eb5e69e208" + }, + "@std/data-structures@1.0.11": { + "integrity": "53b98ed7efa61f107dfc14244bd2ec5557f7f7ee0bbaef6d449d7937facacb89" }, "@std/dotenv@0.225.6": { "integrity": "1d6f9db72f565bd26790fa034c26e45ecb260b5245417be76c2279e5734c421b" @@ -73,8 +81,8 @@ "@std/encoding@1.0.10": { "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" }, - "@std/fmt@1.0.9": { - "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" + "@std/fmt@1.0.10": { + "integrity": "90dfba288802ac6de82fb31d0917eb9e4450b9925b954d5e51fc29ac07419db5" }, "@std/fs@1.0.23": { "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37", @@ -83,8 +91,8 @@ "jsr:@std/path" ] }, - "@std/internal@1.0.12": { - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + "@std/internal@1.0.13": { + "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0" }, "@std/path@1.1.4": { "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", @@ -92,8 +100,8 @@ "jsr:@std/internal" ] }, - "@std/streams@1.0.17": { - "integrity": "7859f3d9deed83cf4b41f19223d4a67661b3d3819e9fc117698f493bf5992140", + "@std/streams@1.1.0": { + "integrity": "2f7024d841f343fd478afe0c958a3f0f068ef2a0d2bcc954f550f97ac1fa22e3", "dependencies": [ "jsr:@std/bytes" ] @@ -112,288 +120,318 @@ "@ark/util@0.56.0": { "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==" }, - "@emnapi/core@1.9.1": { - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "@emnapi/core@1.10.0": { + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dependencies": [ "@emnapi/wasi-threads", "tslib" ] }, - "@emnapi/runtime@1.9.1": { - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "@emnapi/runtime@1.10.0": { + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dependencies": [ "tslib" ] }, - "@emnapi/wasi-threads@1.2.0": { - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "@emnapi/wasi-threads@1.2.1": { + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dependencies": [ "tslib" ] }, - "@napi-rs/wasm-runtime@1.1.2_@emnapi+core@1.9.1_@emnapi+runtime@1.9.1": { - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "@napi-rs/wasm-runtime@1.1.4_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0": { + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dependencies": [ "@emnapi/core", "@emnapi/runtime", "@tybys/wasm-util" ] }, - "@oxc-project/types@0.123.0": { - "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==" + "@oxc-project/types@0.127.0": { + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==" }, - "@oxfmt/binding-android-arm-eabi@0.43.0": { - "integrity": "sha512-CgU2s+/9hHZgo0IxVxrbMPrMj+tJ6VM3mD7Mr/4oiz4FNTISLoCvRmB5nk4wAAle045RtRjd86m673jwPyb1OQ==", + "@oxfmt/binding-android-arm-eabi@0.46.0": { + "integrity": "sha512-b1doV4WRcJU+BESSlCvCjV+5CEr/T6h0frArAdV26Nir+gGNFNaylvDiiMPfF1pxeV0txZEs38ojzJaxBYg+ng==", "os": ["android"], "cpu": ["arm"] }, - "@oxfmt/binding-android-arm64@0.43.0": { - "integrity": "sha512-T9OfRwjA/EdYxAqbvR7TtqLv5nIrwPXuCtTwOHtS7aR9uXyn74ZYgzgTo6/ZwvTq9DY4W+DsV09hB2EXgn9EbA==", + "@oxfmt/binding-android-arm64@0.46.0": { + "integrity": "sha512-v6+HhjsoV3GO0u2u9jLSAZrvWfTraDxKofUIQ7/ktS7tzS+epVsxdHmeM+XxuNcAY/nWxxU1Sg4JcGTNRXraBA==", "os": ["android"], "cpu": ["arm64"] }, - "@oxfmt/binding-darwin-arm64@0.43.0": { - "integrity": "sha512-o3i49ZUSJWANzXMAAVY1wnqb65hn4JVzwlRQ5qfcwhRzIA8lGVaud31Q3by5ALHPrksp5QEaKCQF9aAS3TXpZA==", + "@oxfmt/binding-darwin-arm64@0.46.0": { + "integrity": "sha512-3eeooJGrqGIlI5MyryDZsAcKXSmKIgAD4yYtfRrRJzXZ0UTFZtiSveIur56YPrGMYZwT4XyVhHsMqrNwr1XeFA==", "os": ["darwin"], "cpu": ["arm64"] }, - "@oxfmt/binding-darwin-x64@0.43.0": { - "integrity": "sha512-vWECzzCFkb0kK6jaHjbtC5sC3adiNWtqawFCxhpvsWlzVeKmv5bNvkB4nux+o4JKWTpHCM57NDK/MeXt44txmA==", + "@oxfmt/binding-darwin-x64@0.46.0": { + "integrity": "sha512-QG8BDM0CXWbu84k2SKmCqfEddPQPFiBicwtYnLqHRWZZl57HbtOLRMac/KTq2NO4AEc4ICCBpFxJIV9zcqYfkQ==", "os": ["darwin"], "cpu": ["x64"] }, - "@oxfmt/binding-freebsd-x64@0.43.0": { - "integrity": "sha512-rgz8JpkKiI/umOf7fl9gwKyQasC8bs5SYHy6g7e4SunfLBY3+8ATcD5caIg8KLGEtKFm5ujKaH8EfjcmnhzTLg==", + "@oxfmt/binding-freebsd-x64@0.46.0": { + "integrity": "sha512-9DdCqS/n2ncu/Chazvt3cpgAjAmIGQDz7hFKSrNItMApyV/Ja9mz3hD4JakIE3nS8PW9smEbPWnb389QLBY4nw==", "os": ["freebsd"], "cpu": ["x64"] }, - "@oxfmt/binding-linux-arm-gnueabihf@0.43.0": { - "integrity": "sha512-nWYnF3vIFzT4OM1qL/HSf1Yuj96aBuKWSaObXHSWliwAk2rcj7AWd6Lf7jowEBQMo4wCZVnueIGw/7C4u0KTBQ==", + "@oxfmt/binding-linux-arm-gnueabihf@0.46.0": { + "integrity": "sha512-Dgs7VeE2jT0LHMhw6tPEt0xQYe54kBqHEovmWsv4FVQlegCOvlIJNx0S8n4vj8WUtpT+Z6BD2HhKJPLglLxvZg==", "os": ["linux"], "cpu": ["arm"] }, - "@oxfmt/binding-linux-arm-musleabihf@0.43.0": { - "integrity": "sha512-sFg+NWJbLfupYTF4WELHAPSnLPOn1jiDZ33Z1jfDnTaA+cC3iB35x0FMMZTFdFOz3icRIArncwCcemJFGXu6TQ==", + "@oxfmt/binding-linux-arm-musleabihf@0.46.0": { + "integrity": "sha512-Zxn3adhTH13JKnU4xXJj8FeEfF680XjXh3gSShKl57HCMBRde2tUJTgogV/1MSHA80PJEVrDa7r66TLVq3Ia7Q==", "os": ["linux"], "cpu": ["arm"] }, - "@oxfmt/binding-linux-arm64-gnu@0.43.0": { - "integrity": "sha512-MelWqv68tX6wZEILDrTc9yewiGXe7im62+5x0bNXlCYFOZdA+VnYiJfAihbROsZ5fm90p9C3haFrqjj43XnlAA==", + "@oxfmt/binding-linux-arm64-gnu@0.46.0": { + "integrity": "sha512-+TWipjrgVM8D7aIdDD0tlr3teLTTvQTn7QTE5BpT10H1Fj82gfdn9X6nn2sDgx/MepuSCfSnzFNJq2paLL0OiA==", "os": ["linux"], "cpu": ["arm64"] }, - "@oxfmt/binding-linux-arm64-musl@0.43.0": { - "integrity": "sha512-ROaWfYh+6BSJ1Arwy5ujijTlwnZetxDxzBpDc1oBR4d7rfrPBqzeyjd5WOudowzQUgyavl2wEpzn1hw3jWcqLA==", + "@oxfmt/binding-linux-arm64-musl@0.46.0": { + "integrity": "sha512-aAUPBWJ1lGwwnxZUEDLJ94+Iy6MuwJwPxUgO4sCA5mEEyDk7b+cDQ+JpX1VR150Zoyd+D49gsrUzpUK5h587Eg==", "os": ["linux"], "cpu": ["arm64"] }, - "@oxfmt/binding-linux-ppc64-gnu@0.43.0": { - "integrity": "sha512-PJRs/uNxmFipJJ8+SyKHh7Y7VZIKQicqrrBzvfyM5CtKi8D7yZKTwUOZV3ffxmiC2e7l1SDJpkBEOyue5NAFsg==", + "@oxfmt/binding-linux-ppc64-gnu@0.46.0": { + "integrity": "sha512-ufBCJukyFX/UDrokP/r6BGDoTInnsDs7bxyzKAgMiZlt2Qu8GPJSJ6Zm6whIiJzKk0naxA8ilwmbO1LMw6Htxw==", "os": ["linux"], "cpu": ["ppc64"] }, - "@oxfmt/binding-linux-riscv64-gnu@0.43.0": { - "integrity": "sha512-j6biGAgzIhj+EtHXlbNumvwG7XqOIdiU4KgIWRXAEj/iUbHKukKW8eXa4MIwpQwW1YkxovduKtzEAPnjlnAhVQ==", + "@oxfmt/binding-linux-riscv64-gnu@0.46.0": { + "integrity": "sha512-eqtlC2YmPqjun76R1gVfGLuKWx7NuEnLEAudZ7n6ipSKbCZTqIKSs1b5Y8K/JHZsRpLkeSmAAjig5HOIg8fQzQ==", "os": ["linux"], "cpu": ["riscv64"] }, - "@oxfmt/binding-linux-riscv64-musl@0.43.0": { - "integrity": "sha512-RYWxAcslKxvy7yri24Xm9cmD0RiANaiEPs007EFG6l9h1ChM69Q5SOzACaCoz4Z9dEplnhhneeBaTWMEdpgIbA==", + "@oxfmt/binding-linux-riscv64-musl@0.46.0": { + "integrity": "sha512-yccVOO2nMXkQLGgy0He3EQEwKD7NF0zEk+/OWmroznkqXyJdN6bfK0LtNnr6/14Bh3FjpYq7bP33l/VloCnxpA==", "os": ["linux"], "cpu": ["riscv64"] }, - "@oxfmt/binding-linux-s390x-gnu@0.43.0": { - "integrity": "sha512-DT6Q8zfQQy3jxpezAsBACEHNUUixKSYTwdXeXojNHe4DQOoxjPdjr3Szu6BRNjxLykZM/xMNmp9ElOIyDppwtw==", + "@oxfmt/binding-linux-s390x-gnu@0.46.0": { + "integrity": "sha512-aAf7fG23OQCey6VRPj9IeCraoYtpgtx0ZyJ1CXkPyT1wjzBE7c3xtuxHe/AdHaJfVVb/SXpSk8Gl1LzyQupSqw==", "os": ["linux"], "cpu": ["s390x"] }, - "@oxfmt/binding-linux-x64-gnu@0.43.0": { - "integrity": "sha512-R8Yk7iYcuZORXmCfFZClqbDxRZgZ9/HEidUuBNdoX8Ptx07cMePnMVJ/woB84lFIDjh2ROHVaOP40Ds3rBXFqg==", + "@oxfmt/binding-linux-x64-gnu@0.46.0": { + "integrity": "sha512-q0JPsTMyJNjYrBvYFDz4WbVsafNZaPCZv4RnFypRotLqpKROtBZcEaXQW4eb9YmvLU3NckVemLJnzkSZSdmOxw==", "os": ["linux"], "cpu": ["x64"] }, - "@oxfmt/binding-linux-x64-musl@0.43.0": { - "integrity": "sha512-F2YYqyvnQNvi320RWZNAvsaWEHwmW3k4OwNJ1hZxRKXupY63expbBaNp6jAgvYs7y/g546vuQnGHQuCBhslhLQ==", + "@oxfmt/binding-linux-x64-musl@0.46.0": { + "integrity": "sha512-7LsLY9Cw57GPkhSR+duI3mt9baRczK/DtHYSldQ4BEU92da9igBQNl4z7Vq5U9NNPsh1FmpKvv1q9WDtiUQR1A==", "os": ["linux"], "cpu": ["x64"] }, - "@oxfmt/binding-openharmony-arm64@0.43.0": { - "integrity": "sha512-OE6TdietLXV3F6c7pNIhx/9YC1/2YFwjU9DPc/fbjxIX19hNIaP1rS0cFjCGJlGX+cVJwIKWe8Mos+LdQ1yAJw==", + "@oxfmt/binding-openharmony-arm64@0.46.0": { + "integrity": "sha512-lHiBOz8Duaku7JtRNLlps3j++eOaICPZSd8FCVmTDM4DFOPT71Bjn7g6iar1z7StXlKRweUKxWUs4sA+zWGDXg==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@oxfmt/binding-win32-arm64-msvc@0.43.0": { - "integrity": "sha512-0nWK6a7pGkbdoypfVicmV9k/N1FwjPZENoqhlTU+5HhZnAhpIO3za30nEE33u6l6tuy9OVfpdXUqxUgZ+4lbZw==", + "@oxfmt/binding-win32-arm64-msvc@0.46.0": { + "integrity": "sha512-/5ktYUliP89RhgC37DBH1x20U5zPSZMy3cMEcO0j3793rbHP9MWsknBwQB6eozRzWmYrh0IFM/p20EbPvDlYlg==", "os": ["win32"], "cpu": ["arm64"] }, - "@oxfmt/binding-win32-ia32-msvc@0.43.0": { - "integrity": "sha512-9aokTR4Ft+tRdvgN/pKzSkVy2ksc4/dCpDm9L/xFrbIw0yhLtASLbvoG/5WOTUh/BRPPnfGTsWznEqv0dlOmhA==", + "@oxfmt/binding-win32-ia32-msvc@0.46.0": { + "integrity": "sha512-3WTnoiuIr8XvV0DIY7SN+1uJSwKf4sPpcbHfobcRT9JutGcLaef/miyBB87jxd3aqH+mS0+G5lsgHuXLUwjjpQ==", "os": ["win32"], "cpu": ["ia32"] }, - "@oxfmt/binding-win32-x64-msvc@0.43.0": { - "integrity": "sha512-4bPgdQux2ZLWn3bf2TTXXMHcJB4lenmuxrLqygPmvCJ104Yqzj1UctxSRzR31TiJ4MLaG22RK8dUsVpJtrCz5g==", + "@oxfmt/binding-win32-x64-msvc@0.46.0": { + "integrity": "sha512-IXxiQpkYnOwNfP23vzwSfhdpxJzyiPTY7eTn6dn3DsriKddESzM8i6kfq9R7CD/PUJwCvQT22NgtygBeug3KoA==", "os": ["win32"], "cpu": ["x64"] }, - "@oxlint/binding-android-arm-eabi@1.58.0": { - "integrity": "sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==", + "@oxlint-tsgolint/darwin-arm64@0.21.1": { + "integrity": "sha512-7TLjyWe4wG9saJc992VWmaHq2hwKfOEEVTjheReXJXaDhavMZI4X9a6nKhbEng4IVkYtzjD2jw16vw2WFXLYLw==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@oxlint-tsgolint/darwin-x64@0.21.1": { + "integrity": "sha512-7wf9Wf75nTzA7zpL9myhFe2RKvfuqGUOADNvUooCjEWvh7hmPz3lSEqTMh5Z/VQhzsG04mM9ACyghxhRzq7zFw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@oxlint-tsgolint/linux-arm64@0.21.1": { + "integrity": "sha512-IPuQN/Vd0Rjklg/cCGBbQyUuRBp2f6LQXpZYwk5ivOR6V/+CgiYsv8pn/PVY7gjeyoNvPQrXB7xMjHUO2YZbdw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@oxlint-tsgolint/linux-x64@0.21.1": { + "integrity": "sha512-d1niGuTbh2qiv7dR7tqkbOcM5cIR63of0lMBFdEQavL1KrJV8zuRdwdi68K7MNGdgoR+J5A9ajpGGvsHwp1bPg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@oxlint-tsgolint/win32-arm64@0.21.1": { + "integrity": "sha512-ICu9y2JLnFPvFqstnWPPNqBM8LK8BWw2OTeaR0UgEMm4hOSbrZAKv1/hwZYyiLqnCNjBL87AGSQIgTHCYlsipw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@oxlint-tsgolint/win32-x64@0.21.1": { + "integrity": "sha512-cTEFCFjCj6iXfrSHcvajSPNqhEA4TxSzU3gFxbdGSAUTNXGToU99IbdhWAPSbhcucoym0XE4Zl7E41NiSkNTug==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@oxlint/binding-android-arm-eabi@1.61.0": { + "integrity": "sha512-6eZBPgiigK5txqoVgRqxbaxiom4lM8AP8CyKPPvpzKnQ3iFRFOIDc+0AapF+qsUSwjOzr5SGk4SxQDpQhkSJMQ==", "os": ["android"], "cpu": ["arm"] }, - "@oxlint/binding-android-arm64@1.58.0": { - "integrity": "sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==", + "@oxlint/binding-android-arm64@1.61.0": { + "integrity": "sha512-CkwLR69MUnyv5wjzebvbbtTSUwqLxM35CXE79bHqDIK+NtKmPEUpStTcLQRZMCo4MP0qRT6TXIQVpK0ZVScnMA==", "os": ["android"], "cpu": ["arm64"] }, - "@oxlint/binding-darwin-arm64@1.58.0": { - "integrity": "sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==", + "@oxlint/binding-darwin-arm64@1.61.0": { + "integrity": "sha512-8JbefTkbmvqkqWjmQrHke+MdpgT2UghhD/ktM4FOQSpGeCgbMToJEKdl9zwhr/YWTl92i4QI1KiTwVExpcUN8A==", "os": ["darwin"], "cpu": ["arm64"] }, - "@oxlint/binding-darwin-x64@1.58.0": { - "integrity": "sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==", + "@oxlint/binding-darwin-x64@1.61.0": { + "integrity": "sha512-uWpoxDT47hTnDLcdEh5jVbso8rlTTu5o0zuqa9J8E0JAKmIWn7kGFEIB03Pycn2hd2vKxybPGLhjURy/9We5FQ==", "os": ["darwin"], "cpu": ["x64"] }, - "@oxlint/binding-freebsd-x64@1.58.0": { - "integrity": "sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==", + "@oxlint/binding-freebsd-x64@1.61.0": { + "integrity": "sha512-K/o4hEyW7flfMel0iBVznmMBt7VIMHGdjADocHKpK1DUF9erpWnJ+BSSWd2W0c8K3mPtpph+CuHzRU6CI3l9jQ==", "os": ["freebsd"], "cpu": ["x64"] }, - "@oxlint/binding-linux-arm-gnueabihf@1.58.0": { - "integrity": "sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==", + "@oxlint/binding-linux-arm-gnueabihf@1.61.0": { + "integrity": "sha512-P6040ZkcyweJ0Po9yEFqJCdvZnf3VNCGs1SIHgXDf8AAQNC6ID/heXQs9iSgo2FH7gKaKq32VWc59XZwL34C5Q==", "os": ["linux"], "cpu": ["arm"] }, - "@oxlint/binding-linux-arm-musleabihf@1.58.0": { - "integrity": "sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==", + "@oxlint/binding-linux-arm-musleabihf@1.61.0": { + "integrity": "sha512-bwxrGCzTZkuB+THv2TQ1aTkVEfv5oz8sl+0XZZCpoYzErJD8OhPQOTA0ENPd1zJz8QsVdSzSrS2umKtPq4/JXg==", "os": ["linux"], "cpu": ["arm"] }, - "@oxlint/binding-linux-arm64-gnu@1.58.0": { - "integrity": "sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==", + "@oxlint/binding-linux-arm64-gnu@1.61.0": { + "integrity": "sha512-vkhb9/wKguMkLlrm3FoJW/Xmdv31GgYAE+x8lxxQ+7HeOxXUySI0q36a3NTVIuQUdLzxCI1zzMGsk1o37FOe3w==", "os": ["linux"], "cpu": ["arm64"] }, - "@oxlint/binding-linux-arm64-musl@1.58.0": { - "integrity": "sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==", + "@oxlint/binding-linux-arm64-musl@1.61.0": { + "integrity": "sha512-bl1dQh8LnVqsj6oOQAcxwbuOmNJkwc4p6o//HTBZhNTzJy21TLDwAviMqUFNUxDHkPGpmdKTSN4tWTjLryP8xg==", "os": ["linux"], "cpu": ["arm64"] }, - "@oxlint/binding-linux-ppc64-gnu@1.58.0": { - "integrity": "sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==", + "@oxlint/binding-linux-ppc64-gnu@1.61.0": { + "integrity": "sha512-QoOX6KB2IiEpyOj/HKqaxi+NQHPnOgNgnr22n9N4ANJCzXkUlj1UmeAbFb4PpqdlHIzvGDM5xZ0OKtcLq9RhiQ==", "os": ["linux"], "cpu": ["ppc64"] }, - "@oxlint/binding-linux-riscv64-gnu@1.58.0": { - "integrity": "sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==", + "@oxlint/binding-linux-riscv64-gnu@1.61.0": { + "integrity": "sha512-1TGcTerjY6p152wCof3oKElccq3xHljS/Mucp04gV/4ATpP6nO7YNnp7opEg6SHkv2a57/b4b8Ndm9znJ1/qAw==", "os": ["linux"], "cpu": ["riscv64"] }, - "@oxlint/binding-linux-riscv64-musl@1.58.0": { - "integrity": "sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==", + "@oxlint/binding-linux-riscv64-musl@1.61.0": { + "integrity": "sha512-65wXEmZIrX2ADwC8i/qFL4EWLSbeuBpAm3suuX1vu4IQkKd+wLT/HU/BOl84kp91u2SxPkPDyQgu4yrqp8vwVA==", "os": ["linux"], "cpu": ["riscv64"] }, - "@oxlint/binding-linux-s390x-gnu@1.58.0": { - "integrity": "sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==", + "@oxlint/binding-linux-s390x-gnu@1.61.0": { + "integrity": "sha512-TVvhgMvor7Qa6COeXxCJ7ENOM+lcAOGsQ0iUdPSCv2hxb9qSHLQ4XF1h50S6RE1gBOJ0WV3rNukg4JJJP1LWRA==", "os": ["linux"], "cpu": ["s390x"] }, - "@oxlint/binding-linux-x64-gnu@1.58.0": { - "integrity": "sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==", + "@oxlint/binding-linux-x64-gnu@1.61.0": { + "integrity": "sha512-SjpS5uYuFoDnDdZPwZE59ndF95AsY47R5MliuneTWR1pDm2CxGJaYXbKULI71t5TVfLQUWmrHEGRL9xvuq6dnA==", "os": ["linux"], "cpu": ["x64"] }, - "@oxlint/binding-linux-x64-musl@1.58.0": { - "integrity": "sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==", + "@oxlint/binding-linux-x64-musl@1.61.0": { + "integrity": "sha512-gGfAeGD4sNJGILZbc/yKcIimO9wQnPMoYp9swAaKeEtwsSQAbU+rsdQze5SBtIP6j0QDzeYd4XSSUCRCF+LIeQ==", "os": ["linux"], "cpu": ["x64"] }, - "@oxlint/binding-openharmony-arm64@1.58.0": { - "integrity": "sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==", + "@oxlint/binding-openharmony-arm64@1.61.0": { + "integrity": "sha512-OlVT0LrG/ct33EVtWRyR+B/othwmDWeRxfi13wUdPeb3lAT5TgTcFDcfLfarZtzB4W1nWF/zICMgYdkggX2WmQ==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@oxlint/binding-win32-arm64-msvc@1.58.0": { - "integrity": "sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==", + "@oxlint/binding-win32-arm64-msvc@1.61.0": { + "integrity": "sha512-vI//NZPJk6DToiovPtaiwD4iQ7kO1r5ReWQD0sOOyKRtP3E2f6jxin4uvwi3OvDzHA2EFfd7DcZl5dtkQh7g1w==", "os": ["win32"], "cpu": ["arm64"] }, - "@oxlint/binding-win32-ia32-msvc@1.58.0": { - "integrity": "sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==", + "@oxlint/binding-win32-ia32-msvc@1.61.0": { + "integrity": "sha512-0ySj4/4zd2XjePs3XAQq7IigIstN4LPQZgCyigX5/ERMLjdWAJfnxcTsrtxZxuij8guJW8foXuHmhGxW0H4dDA==", "os": ["win32"], "cpu": ["ia32"] }, - "@oxlint/binding-win32-x64-msvc@1.58.0": { - "integrity": "sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==", + "@oxlint/binding-win32-x64-msvc@1.61.0": { + "integrity": "sha512-0xgSiyeqDLDZxXoe9CVJrOx3TUVsfyoOY7cNi03JbItNcC9WCZqrSNdrAbHONxhSPaVh/lzfnDcON1RqSUMhHw==", "os": ["win32"], "cpu": ["x64"] }, - "@rolldown/binding-android-arm64@1.0.0-rc.13": { - "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==", + "@rolldown/binding-android-arm64@1.0.0-rc.17": { + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "os": ["android"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-arm64@1.0.0-rc.13": { - "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==", + "@rolldown/binding-darwin-arm64@1.0.0-rc.17": { + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-x64@1.0.0-rc.13": { - "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==", + "@rolldown/binding-darwin-x64@1.0.0-rc.17": { + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "os": ["darwin"], "cpu": ["x64"] }, - "@rolldown/binding-freebsd-x64@1.0.0-rc.13": { - "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==", + "@rolldown/binding-freebsd-x64@1.0.0-rc.17": { + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.13": { - "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==", + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17": { + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "os": ["linux"], "cpu": ["arm"] }, - "@rolldown/binding-linux-arm64-gnu@1.0.0-rc.13": { - "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==", + "@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17": { + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-arm64-musl@1.0.0-rc.13": { - "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==", + "@rolldown/binding-linux-arm64-musl@1.0.0-rc.17": { + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13": { - "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==", + "@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17": { + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13": { - "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==", + "@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17": { + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "os": ["linux"], "cpu": ["s390x"] }, - "@rolldown/binding-linux-x64-gnu@1.0.0-rc.13": { - "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==", + "@rolldown/binding-linux-x64-gnu@1.0.0-rc.17": { + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-linux-x64-musl@1.0.0-rc.13": { - "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==", + "@rolldown/binding-linux-x64-musl@1.0.0-rc.17": { + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-openharmony-arm64@1.0.0-rc.13": { - "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==", + "@rolldown/binding-openharmony-arm64@1.0.0-rc.17": { + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rolldown/binding-wasm32-wasi@1.0.0-rc.13": { - "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==", + "@rolldown/binding-wasm32-wasi@1.0.0-rc.17": { + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", "dependencies": [ "@emnapi/core", "@emnapi/runtime", @@ -401,18 +439,18 @@ ], "cpu": ["wasm32"] }, - "@rolldown/binding-win32-arm64-msvc@1.0.0-rc.13": { - "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==", + "@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17": { + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "os": ["win32"], "cpu": ["arm64"] }, - "@rolldown/binding-win32-x64-msvc@1.0.0-rc.13": { - "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==", + "@rolldown/binding-win32-x64-msvc@1.0.0-rc.17": { + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "os": ["win32"], "cpu": ["x64"] }, - "@rolldown/pluginutils@1.0.0-rc.13": { - "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==" + "@rolldown/pluginutils@1.0.0-rc.17": { + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==" }, "@standard-community/standard-json@0.3.5_@standard-schema+spec@1.1.0_@types+json-schema@7.0.15_arktype@2.2.0_quansync@0.2.11": { "integrity": "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==", @@ -450,8 +488,8 @@ "@types/json-schema@7.0.15": { "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, - "@types/node@25.5.2": { - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "@types/node@25.6.0": { + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dependencies": [ "undici-types" ] @@ -494,8 +532,8 @@ "openapi-types@12.1.3": { "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" }, - "oxfmt@0.43.0": { - "integrity": "sha512-KTYNG5ISfHSdmeZ25Xzb3qgz9EmQvkaGAxgBY/p38+ZiAet3uZeu7FnMwcSQJg152Qwl0wnYAxDc+Z/H6cvrwA==", + "oxfmt@0.46.0": { + "integrity": "sha512-CopwJOwPAjZ9p76fCvz+mSOJTw9/NY3cSksZK3VO/bUQ8UoEcketNgUuYS0UB3p+R9XnXe7wGGXUmyFxc7QxJA==", "dependencies": [ "tinypool" ], @@ -522,8 +560,23 @@ ], "bin": true }, - "oxlint@1.58.0": { - "integrity": "sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==", + "oxlint-tsgolint@0.21.1": { + "integrity": "sha512-O2hxiT14C2HJkwzBU6CQBFPoagSd/IcV+Tt3e3UUaXFwbW4BO5DSDPSSboc3UM5MIDY+MLyepvtQwBQafNxWdw==", + "optionalDependencies": [ + "@oxlint-tsgolint/darwin-arm64", + "@oxlint-tsgolint/darwin-x64", + "@oxlint-tsgolint/linux-arm64", + "@oxlint-tsgolint/linux-x64", + "@oxlint-tsgolint/win32-arm64", + "@oxlint-tsgolint/win32-x64" + ], + "bin": true + }, + "oxlint@1.61.0_oxlint-tsgolint@0.21.1": { + "integrity": "sha512-ZC0ALuhDZ6ivOFG+sy0D0pEDN49EvsId98zVlmYdkcXHsEM14m/qTNUEsUpiFiCVbpIxYtVBmmLE87nsbUHohQ==", + "dependencies": [ + "oxlint-tsgolint" + ], "optionalDependencies": [ "@oxlint/binding-android-arm-eabi", "@oxlint/binding-android-arm64", @@ -545,13 +598,16 @@ "@oxlint/binding-win32-ia32-msvc", "@oxlint/binding-win32-x64-msvc" ], + "optionalPeers": [ + "oxlint-tsgolint" + ], "bin": true }, "quansync@0.2.11": { "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==" }, - "rolldown@1.0.0-rc.13": { - "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==", + "rolldown@1.0.0-rc.17": { + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dependencies": [ "@oxc-project/types", "@rolldown/pluginutils" @@ -581,35 +637,36 @@ "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "undici-types@7.18.2": { - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==" + "undici-types@7.19.2": { + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==" } }, "workspace": { "dependencies": [ "jsr:@deno/loader@0.5", - "jsr:@hono/hono@^4.12.10", + "jsr:@hono/hono@^4.12.15", "jsr:@hono/standard-validator@~0.2.2", - "jsr:@sitnik/nanoid@^5.1.7", + "jsr:@sitnik/nanoid@^5.1.9", "jsr:@std/assert@^1.0.19", - "jsr:@std/async@^1.2.0", - "jsr:@std/cache@~0.2.2", - "jsr:@std/collections@^1.1.6", + "jsr:@std/async@^1.3.0", + "jsr:@std/cache@~0.2.3", + "jsr:@std/collections@^1.1.7", "jsr:@std/dotenv@~0.225.6", "jsr:@std/encoding@^1.0.10", - "jsr:@std/fmt@^1.0.9", + "jsr:@std/fmt@^1.0.10", "jsr:@std/fs@^1.0.23", "jsr:@std/path@^1.1.4", - "jsr:@std/streams@^1.0.17", + "jsr:@std/streams@^1.1.0", "jsr:@std/ulid@1", - "npm:@types/node@^25.5.2", + "npm:@types/node@^25.6.0", "npm:arkenv@0.11", "npm:arktype@^2.2.0", "npm:hash-wasm@^4.12.0", "npm:hono-openapi@^1.3.0", - "npm:oxfmt@0.43", - "npm:oxlint@^1.58.0", - "npm:rolldown@1.0.0-rc.13" + "npm:oxfmt@0.46", + "npm:oxlint-tsgolint@~0.21.1", + "npm:oxlint@^1.61.0", + "npm:rolldown@1.0.0-rc.17" ] } } diff --git a/mise.toml b/mise.toml index ec1e3183..fd9bf908 100644 --- a/mise.toml +++ b/mise.toml @@ -89,11 +89,11 @@ run = [{ task = "install" }, { task = "fix:oxfmt" }, { task = "fix:oxlint" }] [tasks."fix:oxfmt"] description = "Run oxfmt formatter" -run = ["mise exec -- deno x -A oxfmt **"] +run = ["mise exec -- deno x -A oxfmt"] [tasks."fix:oxlint"] description = "Run oxlint formatter" -run = ["mise exec -- deno x -A oxlint --fix **"] +run = ["mise exec -- deno x -A oxlint --fix"] [tasks."lint"] description = "Run all linters" @@ -105,7 +105,7 @@ run = ["mise exec -- deno check --quiet"] [tasks."lint:oxlint"] description = "Run oxlint linter" -run = ["mise exec -- deno x -A oxlint **"] +run = ["mise exec -- deno x -A oxlint"] [tasks."tidy"] description = "Tidy all" diff --git a/src/database/index.ts b/src/database/index.ts index 8208f8db..f44b5a8e 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -4,14 +4,13 @@ import { LruCache } from "@std/cache"; import { monotonicUlid, ulid } from "@std/ulid"; import { constantPathDatabaseFile } from "#/global.ts"; +import { migrations } from "#db/migration.ts"; +import { DocumentQuery, UserQuery } from "#db/query.ts"; import { Logger } from "#util/console.ts"; import { generateHash } from "#util/crypto.ts"; import { env } from "#util/env.ts"; import { generateToken } from "#util/user.ts"; -import { migrations } from "./migration.ts"; -import { DocumentQuery, UserQuery } from "./query.ts"; - const log: Logger = new Logger("database"); type Options = { diff --git a/src/database/migration.ts b/src/database/migration.ts index f04eca92..84826bd3 100644 --- a/src/database/migration.ts +++ b/src/database/migration.ts @@ -1,12 +1,11 @@ import { mapNotNullish } from "@std/collections"; import { ulid } from "@std/ulid"; +import { mutable } from "#/global.ts"; import type { Database } from "#db/index.ts"; import { Logger } from "#util/console.ts"; import { generateHash } from "#util/crypto.ts"; -import { mutable } from "../global.ts"; - const log: Logger = new Logger("database::migration"); type Migration = { @@ -68,25 +67,6 @@ export const migrations: Migration[] = [ database.user.update("id", userRootId, "token", userRootToken); } } - - const userTokens = database.user.getAll(["token"]); - - let userTokenUnhashed = false; - for (const entry of userTokens) { - // combo separator - if (!entry.token.includes(" ")) { - userTokenUnhashed = true; - break; - } - } - - if (userTokenUnhashed) { - log.warn( - "Users with plain tokens found!", - "New users in the instance will have their token hashed,", - "In the future we will enforce that every user token is hashed." - ); - } }, sql: (await import("./migrations/0002.sql", { with: { type: "text" } })).default } diff --git a/src/database/query.ts b/src/database/query.ts index a979e779..76e90c1f 100644 --- a/src/database/query.ts +++ b/src/database/query.ts @@ -3,13 +3,12 @@ import type { SQLInputValue } from "node:sqlite"; import { chunk } from "@std/collections"; import { monotonicUlid } from "@std/ulid"; +import { constantDatabaseMaxElements } from "#/global.ts"; import type { Database } from "#db/index.ts"; import { generateHash } from "#util/crypto.ts"; import type { DocumentVersionType } from "#util/document.ts"; import { generateToken } from "#util/user.ts"; -import { constantDatabaseMaxElements } from "../global.ts"; - export type Document = { id: string; user_id: string | null; diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/drop.ts similarity index 83% rename from src/endpoints/document/v1/delete.ts rename to src/endpoints/document/v1/drop.ts index 28e5a8fe..ee892efe 100644 --- a/src/endpoints/document/v1/delete.ts +++ b/src/endpoints/document/v1/drop.ts @@ -6,7 +6,7 @@ import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { isOwner } from "#util/document.ts"; -import { errorCodeDocumentNotFound, errorCodeUserInvalidToken, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsDelete } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -19,8 +19,8 @@ export default new Hono().delete( "/:name", describeRoute({ tags: ["DOCUMENT (v1)"], - summary: "Delete document", - description: "Deletes a published document in the instance", + summary: "Drop document", + description: "Deletes a document in the instance", security: [{}, { bearer: [] }], responses: { 200: { @@ -43,13 +43,13 @@ export default new Hono().delete( const document = mutable.database.document.get("name", name); if (!document?.id) { - return errorThrow(errorCodeDocumentNotFound); + return errorThrow(ErrorCode.DocumentNotFound); } const userId = ctx.get("userId"); const owner = isOwner(userId, document.user_id); if (!owner) { - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } mutable.database.document.delete("name", name); diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index a19b6255..bb2744e3 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -7,13 +7,7 @@ import { Hono } from "hono/tiny"; import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; -import { - errorCodeDocumentInvalidPassword, - errorCodeDocumentNotFound, - errorCodeDocumentPasswordNeeded, - errorThrow, - genericErrorResponse -} from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsRead } from "#util/fs.ts"; import { validatorDocumentName, @@ -48,7 +42,7 @@ export default new Hono().get( describeRoute({ tags: ["DOCUMENT (v1)"], summary: "Get document", - description: `Get the content/metadata of a published document in the instance + description: `Fetch the content/metadata of a document in the instance Note: If you only need to query the document metadata, you should use HEAD method instead`, responses: { @@ -68,7 +62,7 @@ Note: If you only need to query the document metadata, you should use HEAD metho validator("param", schemaParam, validatorHandler), validator("header", schemaHeader, validatorHandler), validator("query", schemaQuery, validatorHandler), - async (ctx) => { + (ctx) => { const { name // @ts-expect-error upstream @@ -84,15 +78,15 @@ Note: If you only need to query the document metadata, you should use HEAD metho const document = mutable.database.document.get("name", name); if (!document?.id) { - return errorThrow(errorCodeDocumentNotFound); + return errorThrow(ErrorCode.DocumentNotFound); } if (document.password) { if (!password) { - return errorThrow(errorCodeDocumentPasswordNeeded); + return errorThrow(ErrorCode.DocumentPasswordNeeded); } if (!verifyHash(password, document.password)) { - return errorThrow(errorCodeDocumentInvalidPassword); + return errorThrow(ErrorCode.DocumentInvalidPassword); } } diff --git a/src/endpoints/document/v1/index.ts b/src/endpoints/document/v1/index.ts index 768aee16..370d41b4 100644 --- a/src/endpoints/document/v1/index.ts +++ b/src/endpoints/document/v1/index.ts @@ -2,7 +2,7 @@ import { Hono } from "hono/tiny"; import type { Env } from "#http/handler.ts"; -import delete_ from "./delete.ts"; +import drop from "./drop.ts"; import get from "./get.ts"; import list from "./list.ts"; import patch from "./patch.ts"; @@ -10,7 +10,7 @@ import post from "./post.ts"; export const v1DocumentHandler = new Hono(); -v1DocumentHandler.route("/", delete_); +v1DocumentHandler.route("/", drop); v1DocumentHandler.route("/", get); v1DocumentHandler.route("/", list); v1DocumentHandler.route("/", patch); diff --git a/src/endpoints/document/v1/list.ts b/src/endpoints/document/v1/list.ts index e28f0c25..f6b8e4c1 100644 --- a/src/endpoints/document/v1/list.ts +++ b/src/endpoints/document/v1/list.ts @@ -5,7 +5,7 @@ import { Hono } from "hono/tiny"; import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; -import { errorCodeUserInvalidToken, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { validatorDocumentListObject } from "#util/validator/document.ts"; const schemaBodyResponse = resolver(validatorDocumentListObject.array()); @@ -34,10 +34,10 @@ export default new Hono().get( } }), authMiddleware, - async (ctx) => { + (ctx) => { const userId = ctx.get("userId"); if (!userId) { - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } // https://github.com/honojs/hono/issues/1130 diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 67d5b1f9..5e7a5544 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -9,13 +9,7 @@ import { bodyStream } from "#http/middleware/bodyStream.ts"; import { generateHash } from "#util/crypto.ts"; import { isOwner } from "#util/document.ts"; import { env } from "#util/env.ts"; -import { - errorCodeDocumentNameAlreadyExists, - errorCodeDocumentNotFound, - errorCodeUserInvalidToken, - errorThrow, - genericErrorResponse -} from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, @@ -45,7 +39,7 @@ export default new Hono().patch( describeRoute({ tags: ["DOCUMENT (v1)"], summary: "Alter document", - description: `Edit the content/metadata of a published document in the instance + description: `Edit the content/metadata of a document in the instance Note: You can't move the ownership of a document, duplicate the document instead @@ -91,13 +85,13 @@ Note: To remove (nullify) a value, send the header with an empty value`, const document = mutable.database.document.get("name", actualName); if (!document?.id) { - return errorThrow(errorCodeDocumentNotFound); + return errorThrow(ErrorCode.DocumentNotFound); } const userId = ctx.get("userId"); const owner = isOwner(userId, document.user_id); if (!owner) { - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } if (newPassword !== undefined) { @@ -113,7 +107,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, // keep newName last thing to alter in case of race conditions if (newName) { if (mutable.database.document.get("name", newName)?.name) { - return errorThrow(errorCodeDocumentNameAlreadyExists); + return errorThrow(ErrorCode.DocumentNameAlreadyExists); } mutable.database.document.update("name", actualName, "name", newName); diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 718ba3f7..439ee979 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -10,7 +10,7 @@ import { bodyStream } from "#http/middleware/bodyStream.ts"; import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; import { env } from "#util/env.ts"; -import { errorCodeDocumentNameAlreadyExists, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, @@ -88,7 +88,7 @@ export default new Hono().post( let setName: string; if (name) { if (mutable.database.document.get("name", name)?.name) { - return errorThrow(errorCodeDocumentNameAlreadyExists); + return errorThrow(ErrorCode.DocumentNameAlreadyExists); } setName = name; @@ -112,7 +112,14 @@ export default new Hono().post( name: setName, password: hashCombo }); - await fsWrite(ctx, { id: setId }); + + try { + await fsWrite(ctx, { id: setId }); + } catch (why) { + mutable.database.document.delete("id", setId); + + throw why; + } return ctx.json({ name: setName diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index c8420b6b..2f299473 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -6,13 +6,7 @@ import { Hono } from "hono/tiny"; import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; -import { - errorCodeDocumentInvalidPassword, - errorCodeDocumentNotFound, - errorCodeDocumentPasswordNeeded, - errorThrow, - genericErrorResponse -} from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsRead } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -82,15 +76,15 @@ export default new Hono().get( const document = mutable.database.document.get("name", param.name); if (!document?.id) { - return errorThrow(errorCodeDocumentNotFound); + return errorThrow(ErrorCode.DocumentNotFound); } if (document.password) { if (!header.password) { - return errorThrow(errorCodeDocumentPasswordNeeded); + return errorThrow(ErrorCode.DocumentPasswordNeeded); } if (!verifyHash(header.password, document.password)) { - return errorThrow(errorCodeDocumentInvalidPassword); + return errorThrow(ErrorCode.DocumentInvalidPassword); } } diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index 26aa0d45..f0ac71c0 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -6,13 +6,7 @@ import { Hono } from "hono/tiny"; import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; -import { - errorCodeDocumentInvalidPassword, - errorCodeDocumentNotFound, - errorCodeDocumentPasswordNeeded, - errorThrow, - genericErrorResponse -} from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsRead } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -56,7 +50,7 @@ export default new Hono().get( validator("param", schemaParam, validatorHandler), validator("header", schemaHeader, validatorHandler), validator("query", schemaQuery, validatorHandler), - async (ctx) => { + (ctx) => { // https://github.com/honojs/hono/issues/1130 if (ctx.req.method === "HEAD") { return ctx.body(null); @@ -74,15 +68,15 @@ export default new Hono().get( const document = mutable.database.document.get("name", param.name); if (!document?.id) { - return errorThrow(errorCodeDocumentNotFound); + return errorThrow(ErrorCode.DocumentNotFound); } if (document.password) { if (!options.password) { - return errorThrow(errorCodeDocumentPasswordNeeded); + return errorThrow(ErrorCode.DocumentPasswordNeeded); } if (!verifyHash(options.password, document.password)) { - return errorThrow(errorCodeDocumentInvalidPassword); + return errorThrow(ErrorCode.DocumentInvalidPassword); } } diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts index b1ac44e8..9eb2c493 100644 --- a/src/endpoints/legacy/v2/documents/edit.route.ts +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -6,7 +6,7 @@ import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; import { env } from "#util/env.ts"; -import { errorCodeDocumentNotFound, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -65,7 +65,7 @@ export default new Hono().patch( const document = mutable.database.document.get("name", param.name); if (!document?.id || document.user_id) { - return errorThrow(errorCodeDocumentNotFound); + return errorThrow(ErrorCode.DocumentNotFound); } mutable.database.document.update("name", param.name, "version", env.JSPB_DOCUMENT_COMPRESSION); diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index 7cc4b514..a1d2fbce 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -14,7 +14,7 @@ import { bodyStream } from "#http/middleware/bodyStream.ts"; import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; import { env } from "#util/env.ts"; -import { errorCodeDocumentNameAlreadyExists, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -82,7 +82,7 @@ export default new Hono().post( let setName: string; if (name) { if (mutable.database.document.get("name", name)?.name) { - return errorThrow(errorCodeDocumentNameAlreadyExists); + return errorThrow(ErrorCode.DocumentNameAlreadyExists); } setName = name; diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index e55a5a5d..bcba2dbb 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -4,7 +4,7 @@ import { Hono } from "hono/tiny"; import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; -import { errorCodeDocumentNotFound, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsDelete } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -48,7 +48,7 @@ export default new Hono().delete( const document = mutable.database.document.get("name", param.name); if (!document?.id || document.user_id) { - return errorThrow(errorCodeDocumentNotFound); + return errorThrow(ErrorCode.DocumentNotFound); } mutable.database.document.delete("name", param.name); diff --git a/src/endpoints/user/v1/create.ts b/src/endpoints/user/v1/create.ts index f52cb382..66b5360e 100644 --- a/src/endpoints/user/v1/create.ts +++ b/src/endpoints/user/v1/create.ts @@ -6,7 +6,7 @@ import { constantHttpStatusCodes, mutable } from "#/global.ts"; import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { env } from "#util/env.ts"; -import { errorCodeUserInvalidToken, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; import { validatorUserToken } from "#util/validator/user.ts"; const schemaBodyResponse = resolver( @@ -41,7 +41,7 @@ export default new Hono().post( authMiddleware, (ctx) => { if (!env.JSPB_USER_REGISTER && ctx.get("userId") !== mutable.database.user.getRoot()?.id) { - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } return ctx.json({ diff --git a/src/endpoints/user/v1/drop.ts b/src/endpoints/user/v1/drop.ts new file mode 100644 index 00000000..6f33b848 --- /dev/null +++ b/src/endpoints/user/v1/drop.ts @@ -0,0 +1,40 @@ +import { describeRoute } from "@hono/openapi"; +import { Hono } from "hono/tiny"; + +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; + +export default new Hono().delete( + "/", + describeRoute({ + tags: ["USER (v1)"], + summary: "Drop user", + description: `Deletes a user in the instance + +Note: All documents owned by the user will also be deleted`, + security: [{ bearer: [] }], + responses: { + 200: { + description: constantHttpStatusCodes[200] + }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] } + } + }), + authMiddleware, + (ctx) => { + const userId = ctx.get("userId"); + if (!userId) { + return errorThrow(ErrorCode.UserInvalidToken); + } + + mutable.database.user.delete("id", userId); + + return ctx.body(null); + } +); diff --git a/src/endpoints/user/v1/index.ts b/src/endpoints/user/v1/index.ts index 8e29ad69..26a03ab4 100644 --- a/src/endpoints/user/v1/index.ts +++ b/src/endpoints/user/v1/index.ts @@ -3,7 +3,11 @@ import { Hono } from "hono/tiny"; import type { Env } from "#http/handler.ts"; import create from "./create.ts"; +import drop from "./drop.ts"; +import rotateToken from "./rotateToken.ts"; export const v1UserHandler = new Hono(); v1UserHandler.route("/", create); +v1UserHandler.route("/", drop); +v1UserHandler.route("/", rotateToken); diff --git a/src/endpoints/user/v1/rotateToken.ts b/src/endpoints/user/v1/rotateToken.ts new file mode 100644 index 00000000..bc35753a --- /dev/null +++ b/src/endpoints/user/v1/rotateToken.ts @@ -0,0 +1,58 @@ +import { describeRoute, resolver } from "@hono/openapi"; +import { type } from "arktype"; +import { Hono } from "hono/tiny"; + +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import { generateHash } from "#util/crypto.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { generateToken } from "#util/user.ts"; +import { validatorUserToken } from "#util/validator/user.ts"; + +const schemaBodyResponse = resolver( + type({ + token: validatorUserToken + }) +); + +export default new Hono().post( + "/token", + describeRoute({ + tags: ["USER (v1)"], + summary: "Rotate user token", + description: "Rotates a user token in the instance", + security: [{ bearer: [] }], + responses: { + 200: { + content: { + "application/json": { + schema: schemaBodyResponse + } + }, + description: constantHttpStatusCodes[200] + }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] } + } + }), + authMiddleware, + (ctx) => { + const userId = ctx.get("userId"); + if (!userId) { + return errorThrow(ErrorCode.UserInvalidToken); + } + + const token = generateToken(userId); + const hash = generateHash(token); + + mutable.database.user.update("id", userId, "token", hash.combo); + + return ctx.json({ + token: token + }); + } +); diff --git a/src/global.ts b/src/global.ts index e21ccab3..8ccab5c1 100644 --- a/src/global.ts +++ b/src/global.ts @@ -7,7 +7,7 @@ import type { Database } from "#db/index.ts"; export const mutable = { database: undefined as unknown as Database, - http: undefined as Deno.HttpServer | undefined + http: undefined as unknown as Deno.HttpServer }; export const constantDatabaseMaxElements = 10_000; @@ -21,7 +21,7 @@ export const constantNanoid = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWX export const constantPathStructStorage = "./storage/"; export const constantPathStructStorageData = "./storage/data/"; export const constantPathDatabaseFile = "./storage/database.db"; -export const constantStoreDispose = new Map Promise]>(); +export const constantStoreDispose = new Map Promise | void }>(); export const constantTemporalUTC = (): Temporal.ZonedDateTime => Temporal.Now.zonedDateTimeISO("Etc/UTC"); export const constantTemporalToUTC = (temporal: Temporal.Instant): Temporal.ZonedDateTime => temporal.toZonedDateTimeISO("Etc/UTC"); diff --git a/src/http/handler.ts b/src/http/handler.ts index aa1ba07d..37d37559 100644 --- a/src/http/handler.ts +++ b/src/http/handler.ts @@ -8,14 +8,14 @@ import { v2LegacyDocumentHandler } from "#endpoint/legacy/v2/documents/index.ts" import { v1UserHandler } from "#endpoint/user/v1/index.ts"; import { Logger } from "#util/console.ts"; import { env } from "#util/env.ts"; -import { errorCodeCrash, errorCodeDocumentCorrupted, errorGet } from "#util/error.ts"; +import { ErrorCode, errorGet } from "#util/error.ts"; const log: Logger = new Logger("http"); export type Env = { Variables: { - userId: string | undefined; - hasBody: boolean | undefined; + userId?: string; + hasBody?: boolean; }; }; @@ -42,12 +42,12 @@ export const handler = (): Hono => { ) { log.debug(instance); - return ctx.json(errorGet(errorCodeDocumentCorrupted)); + return ctx.json(errorGet(ErrorCode.DocumentCorrupted)); } log.error(instance); - return ctx.json(errorGet(errorCodeCrash)); + return ctx.json(errorGet(ErrorCode.Crash)); }); handler.use("*", cors()); @@ -80,9 +80,9 @@ Each instance can impose restrictions to the API usage. These restrictions may i (the following values might change without notice) - Instance registration policy: ${env.JSPB_USER_REGISTER ? "OPEN" : "CLOSED"} -- Document size limit: ${env.JSPB_DOCUMENT_SIZE === 0 ? "unlimited" : (env.JSPB_DOCUMENT_SIZE ?? "unknown")} -- Document lifetime: ${env.JSPB_DOCUMENT_AGE.total("minutes") === 0 ? "unlimited" : (env.JSPB_DOCUMENT_AGE.total("minutes") ?? "unknown")} -- Document anonymous lifetime: ${env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") === 0 ? "unlimited" : (env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") ?? "unknown")} +- Document size limit: ${env.JSPB_DOCUMENT_SIZE === 0 ? "unlimited" : env.JSPB_DOCUMENT_SIZE} +- Document lifetime: ${env.JSPB_DOCUMENT_AGE.total("minutes") === 0 ? "unlimited" : env.JSPB_DOCUMENT_AGE.total("minutes")} +- Document anonymous lifetime: ${env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") === 0 ? "unlimited" : env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes")} `, license: { name: "EUPL-1.2", diff --git a/src/http/index.ts b/src/http/index.ts index b5e4d5aa..80dff32a 100644 --- a/src/http/index.ts +++ b/src/http/index.ts @@ -1,13 +1,13 @@ import { Logger } from "#util/console.ts"; import { env } from "#util/env.ts"; -import { errorCodeUnknown, errorGet } from "#util/error.ts"; +import { ErrorCode, errorGet } from "#util/error.ts"; const log: Logger = new Logger("http"); const dummyHandler = (): Response => { return Response.json( { - ...errorGet(errorCodeUnknown) + ...errorGet(ErrorCode.Unknown) }, { status: 503, diff --git a/src/http/middleware/authorization.ts b/src/http/middleware/authorization.ts index 7f20b300..3d1b454b 100644 --- a/src/http/middleware/authorization.ts +++ b/src/http/middleware/authorization.ts @@ -2,12 +2,11 @@ import { type } from "arktype"; import { createMiddleware } from "hono/factory"; import { mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; -import { errorCodeUserInvalidToken, errorCodeValidation, errorThrow } from "#util/error.ts"; +import { ErrorCode, errorThrow } from "#util/error.ts"; import { validatorUserHeader } from "#util/validator/user.ts"; -import type { Env } from "../handler.ts"; - export const authMiddleware = createMiddleware(async (ctx, next) => { const authorization = ctx.req.header("authorization"); if (!authorization) { @@ -16,7 +15,7 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { const token = validatorUserHeader(authorization); if (token instanceof type.errors) { - return errorThrow(errorCodeValidation, token.summary); + return errorThrow(ErrorCode.Validation, token.summary); } if (!token.includes(".")) { @@ -25,7 +24,7 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { // @ts-expect-error unindexed select const id = mutable.database.user.get("token", token)?.id; if (!id) { - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } ctx.set("userId", id); @@ -33,18 +32,18 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { return next(); } - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } const [id] = token.split("."); if (!id) { - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } // trying to minimize timing attacks by always calling verifyHash const combo = mutable.database.user.get("id", id)?.token ?? "0 0"; if (!verifyHash(token, combo)) { - return errorThrow(errorCodeUserInvalidToken); + return errorThrow(ErrorCode.UserInvalidToken); } ctx.set("userId", id); diff --git a/src/http/middleware/bodyStream.ts b/src/http/middleware/bodyStream.ts index 94500dba..a2eec136 100644 --- a/src/http/middleware/bodyStream.ts +++ b/src/http/middleware/bodyStream.ts @@ -1,9 +1,8 @@ import { createMiddleware } from "hono/factory"; +import type { Env } from "#http/handler.ts"; import { env } from "#util/env.ts"; -import { errorCodeDocumentInvalidSize, errorThrow } from "#util/error.ts"; - -import type { Env } from "../handler.ts"; +import { ErrorCode, errorThrow } from "#util/error.ts"; export const bodyStream = createMiddleware(async (ctx, next) => { if (!ctx.req.raw.body) { @@ -16,7 +15,7 @@ export const bodyStream = createMiddleware(async (ctx, next) => { if (contentLengthHeader !== null && !ctx.req.raw.headers.has("transfer-encoding")) { const size = Number.parseInt(contentLengthHeader, 10); if (size > env.JSPB_DOCUMENT_SIZE) { - return errorThrow(errorCodeDocumentInvalidSize); + return errorThrow(ErrorCode.DocumentInvalidSize); } ctx.set("hasBody", size > 0); diff --git a/src/index.ts b/src/index.ts index 9a67fa8c..8b64543c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,9 @@ -import { init } from "./init.ts"; +import { abortable } from "@std/async"; + +import { constantStoreDispose } from "#/global.ts"; +import { initDatabase, initDirStruct, initHTTPServer, initUnhashedTokenCheck, initTask } from "#/init.ts"; +import { handler } from "#http/handler.ts"; +import { Logger } from "#util/console.ts"; import "@std/dotenv/load"; @@ -11,4 +16,55 @@ declare global { } } -void init(); +const log: Logger = new Logger(); + +let shutdown = false; + +for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1", "SIGUSR2"] satisfies Deno.Signal[]) { + Deno.addSignalListener(signal, async () => { + if (shutdown) return; + shutdown = true; + + log.debug(`Received ${signal}.`); + + const storeDispose = constantStoreDispose + .entries() + .toArray() + .sort(([, { priority: a }], [, { priority: b }]) => b - a); + + try { + for (const [key, { run }] of storeDispose) { + log.debug(`Closing "${key}"...`); + + try { + const value = run(); + if (value instanceof Promise) { + await abortable(value, AbortSignal.timeout(3000)); + } + } catch { + throw new Error(`Couldn't close "${key}" on time.`); + } + } + } catch (error) { + log.error("Failed to gracefully shutdown (bad state)..:", error); + Deno.exit(1); + } + + if (Deno.exitCode === 0) { + log.info("Bye."); + } + }); +} + +try { + await Promise.all([initDirStruct(), initHTTPServer()]); + await initDatabase(); + initUnhashedTokenCheck(); + initTask(); + await initHTTPServer(handler().fetch); +} catch (error) { + log.error(error); + + Deno.exitCode = 1; + Deno.kill(Deno.pid, "SIGTERM"); +} diff --git a/src/init.ts b/src/init.ts index e59ec02c..4b9cf160 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,104 +1,80 @@ -import { abortable } from "@std/async"; import { ensureDir } from "@std/fs"; import { Database } from "#db/index.ts"; -import { handler } from "#http/handler.ts"; import { http } from "#http/index.ts"; -import { sweeper } from "#task/sweeper.ts"; +import { taskRegister } from "#task/index.ts"; +import { sweeper } from "#task/list/sweeper.ts"; import { Logger } from "#util/console.ts"; import { env } from "#util/env.ts"; import { constantPathStructStorage, constantPathStructStorageData, constantStoreDispose, mutable } from "./global.ts"; -import { taskRegister } from "./task.ts"; const log: Logger = new Logger(); -let shutdown = false; - -const initDirStruct = async (): Promise => { +export const initDirStruct = async (): Promise => { await Promise.all([ensureDir(constantPathStructStorage), ensureDir(constantPathStructStorageData)]); }; -const initHTTPServer = async (handler?: Deno.ServeHandler): Promise => { +export const initHTTPServer = async (handler?: Deno.ServeHandler): Promise => { const id = "__httpServer"; - await constantStoreDispose.get(id)?.[1](); + await constantStoreDispose.get(id)?.run(); mutable.http = http({ handler: handler }); - constantStoreDispose.set(id, [ - 10, - async (): Promise => { - mutable.http?.unref(); + constantStoreDispose.set(id, { + priority: 10, + run: async (): Promise => { + mutable.http.unref(); // Deno.serve will deadlock on shutdown under pressure - await mutable.http?.shutdown(); + await mutable.http.shutdown(); } - ]); + }); }; -const initDatabase = async (): Promise => { +export const initDatabase = async (): Promise => { const id = "__databaseServer"; - await constantStoreDispose.get(id)?.[1](); + await constantStoreDispose.get(id)?.run(); mutable.database = new Database(); - constantStoreDispose.set(id, [0, async (): Promise => mutable.database[Symbol.dispose]()]); + constantStoreDispose.set(id, { + priority: 0, + run: (): void => { + mutable.database[Symbol.dispose](); + } + }); await mutable.database.migration(); }; -const initTask = async (): Promise => { +export const initTask = (): void => { taskRegister(env.JSPB_TASK_SWEEPER, sweeper, { name: "sweeper" }); }; -export const init = async (): Promise => { - for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1", "SIGUSR2"] satisfies Deno.Signal[]) { - Deno.addSignalListener(signal, async () => { - if (shutdown) return; - shutdown = true; - - log.debug(`Received ${signal}.`); - - const storeDispose = constantStoreDispose - .entries() - .toArray() - .sort(([, [pa]], [, [pb]]) => pb - pa); - - try { - for (const [key, [, dispose]] of storeDispose) { - log.debug(`Closing "${key}"...`); - - try { - await abortable(dispose(), AbortSignal.timeout(3000)); - } catch { - log.warn(`Couldn't close "${key}" on time.`); - } - } - } catch (error) { - log.error("Failed to gracefully shutdown (bad state)..:", error); - Deno.exit(1); - } - - if (Deno.exitCode === 0) { - log.info("Bye."); - } - }); - } +export const initUnhashedTokenCheck = (): void => { + const userTokens = mutable.database.user.getAll(["token"]); - try { - await Promise.all([initDirStruct(), initHTTPServer()]); - await initDatabase(); - await Promise.all([initTask(), initHTTPServer(handler().fetch)]); - } catch (error) { - log.error(error); + let userUnhashedToken = false; + for (const entry of userTokens) { + // combo separator + if (!entry.token.includes(" ")) { + userUnhashedToken = true; + break; + } + } - Deno.exitCode = 1; - Deno.kill(Deno.pid, "SIGTERM"); + if (userUnhashedToken) { + log.warn( + "Users with unhashed tokens found!", + "Those users may lose access in future versions of JSPaste!", + "See: https://github.com/jspaste/backend/issues/318" + ); } }; diff --git a/src/task.ts b/src/tasks/index.ts similarity index 83% rename from src/task.ts rename to src/tasks/index.ts index 871a9f12..d716fd6c 100644 --- a/src/task.ts +++ b/src/tasks/index.ts @@ -1,7 +1,6 @@ +import { constantStoreDispose } from "#/global.ts"; import { Logger } from "#util/console.ts"; -import { constantStoreDispose } from "./global.ts"; - const log: Logger = new Logger("task"); type TaskRegisterOptions = { @@ -29,7 +28,7 @@ export const taskRegister = ( const id = `__task-${options.name}`; - constantStoreDispose.get(id)?.[1](); + constantStoreDispose.get(id)?.run(); try { Deno.cron(options.name, expression, { signal: abort.signal }, () => trigger(callback, options)); @@ -37,7 +36,10 @@ export const taskRegister = ( log.error(`Failed to register "${options.name}"..:`, error); } - constantStoreDispose.set(id, [100, async (): Promise => abort.abort()]); + constantStoreDispose.set(id, { + priority: 100, + run: () => abort.abort() + }); log.debug(`Registered "${options.name}".`); }; diff --git a/src/tasks/sweeper.ts b/src/tasks/list/sweeper.ts similarity index 100% rename from src/tasks/sweeper.ts rename to src/tasks/list/sweeper.ts diff --git a/src/utils/console.ts b/src/utils/console.ts index 0d5dff68..1aae7876 100644 --- a/src/utils/console.ts +++ b/src/utils/console.ts @@ -1,77 +1,64 @@ import { mapNotNullish } from "@std/collections"; import { blue, gray, red, yellow } from "@std/fmt/colors"; +import { constantTextEncoder } from "#/global.ts"; + import { env } from "./env.ts"; export class Logger { public static readonly level = { - none: [0, null], - error: [1, red], - warn: [2, yellow], - info: [3, blue], - debug: [4, gray] + error: [1, red("ERROR")], + warn: [2, yellow("WARN") + " "], + info: [3, blue("INFO") + " "], + debug: [4, gray("DEBUG")] } as const; public readonly source: string; - public constructor(source = "common") { - this.source = source; + public constructor(source?: string) { + this.source = source ? gray(`[${source}]`) : ""; } public error(...message: unknown[]): void { - this.flush("error", message); + this.flush(Logger.level.error, message); } public warn(...message: unknown[]): void { - this.flush("warn", message); + this.flush(Logger.level.warn, message); } public info(...message: unknown[]): void { - this.flush("info", message); + this.flush(Logger.level.info, message); } public debug(...message: unknown[]): void { - this.flush("debug", message); + this.flush(Logger.level.debug, message); } - private flush(level: Exclude, message: unknown[]): void { - const [levelNumber, color] = Logger.level[level]; - - if (levelNumber > env.JSPB_LOG_VERBOSITY) return; + private flush([level, name]: (typeof Logger.level)[keyof typeof Logger.level], message: unknown[]): void { + if (level > env.JSPB_LOG_VERBOSITY) return; - const prefix: string[] = []; + let prefix = ""; if (env.JSPB_LOG_TIME) { - const temporalLocal = Temporal.Now.zonedDateTimeISO(); - const temporalYear = temporalLocal.year; - const temporalMonth = temporalLocal.month.toString().padStart(2, "0"); - const temporalDay = temporalLocal.day.toString().padStart(2, "0"); - const temporalHour = temporalLocal.hour.toString().padStart(2, "0"); - const temporalMinute = temporalLocal.minute.toString().padStart(2, "0"); - const temporalSecond = temporalLocal.second.toString().padStart(2, "0"); - const temporalMillisecond = temporalLocal.millisecond.toString().padStart(3, "0"); - const temporalOffset = temporalLocal.offset; - - prefix.push( - gray( - `${temporalYear}-${temporalMonth}-${temporalDay}T${temporalHour}:${temporalMinute}:${temporalSecond}.${temporalMillisecond + temporalOffset}` - ) - ); + prefix += + gray(Temporal.Now.zonedDateTimeISO().toString({ timeZoneName: "never", fractionalSecondDigits: 3 })) + " "; } - prefix.push(color(level.toUpperCase().padEnd(5))); - prefix.push(gray(`[${this.source}]`)); + prefix += name; - const prefixString = prefix.join(" "); + if (this.source) { + prefix += " " + this.source; + } const render = mapNotNullish(message, (item) => { if (item == null) return; if (typeof item === "string") { - return `${prefixString} ${item}`; + return `${prefix} ${item}`; } - return `${prefixString} ${Deno.inspect(item, { + return `${prefix} ${Deno.inspect(item, { colors: true, strAbbreviateSize: 60, iterableLimit: 10 @@ -79,7 +66,13 @@ export class Logger { }); for (const line of render) { - console[level](line); + const data = constantTextEncoder.encode(line + "\n"); + + if (level > 2) { + Deno.stdout.writeSync(data); + } else { + Deno.stderr.writeSync(data); + } } } } diff --git a/src/utils/error.ts b/src/utils/error.ts index ea0ee53e..3ed4b9c9 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -3,122 +3,99 @@ import { type } from "arktype"; import { HTTPException } from "hono/http-exception"; import type { ContentfulStatusCode } from "hono/utils/http-status"; -// allow const enum in the future -// https://github.com/rolldown/rolldown/issues/7676 +export enum ErrorCode { + Crash = 1000, + Unknown = 1001, + Validation = 1002, + // Parse = 1003, // moved to 1002 + NotFound = 1004, + Dummy = 1005, -export const errorCodeCrash = 1000; -export const errorCodeUnknown = 1001; -export const errorCodeValidation = 1002; -// export const errorCodeParse = 1003; // moved to 1002 -export const errorCodeNotFound = 1004; -export const errorCodeDummy = 1005; - -// document -export const errorCodeDocumentNotFound = 1200; -export const errorCodeDocumentNameAlreadyExists = 1201; -export const errorCodeDocumentPasswordNeeded = 1202; -export const errorCodeDocumentInvalidSize = 1203; -// export const errorCodeDocumentInvalidNameLength = 1204; // moved to 1002 -export const errorCodeDocumentInvalidPassword = 1205; -// export const errorCodeDocumentInvalidPasswordLength = 1206; // moved to 1002 -// export const errorCodeDocumentInvalidSecret = 1207; // deprecated -// export const errorCodeDocumentInvalidSecretLength = 1208; // deprecated -// export const errorCodeDocumentInvalidName = 1209; // moved to 1002 -export const errorCodeDocumentCorrupted = 1210; - -// user -export const errorCodeUserInvalidToken = 1300; - -export type ErrorCodeType = - | typeof errorCodeCrash - | typeof errorCodeUnknown - | typeof errorCodeValidation - // | typeof errorCodeParse - | typeof errorCodeNotFound - | typeof errorCodeDummy // document - | typeof errorCodeDocumentNotFound - | typeof errorCodeDocumentNameAlreadyExists - | typeof errorCodeDocumentPasswordNeeded - | typeof errorCodeDocumentInvalidSize - // | typeof errorCodeDocumentInvalidNameLength - | typeof errorCodeDocumentInvalidPassword - // | typeof errorCodeDocumentInvalidPasswordLength - // | typeof errorCodeDocumentInvalidSecret - // | typeof errorCodeDocumentInvalidSecretLength - // | typeof errorCodeDocumentInvalidName - | typeof errorCodeDocumentCorrupted + DocumentNotFound = 1200, + DocumentNameAlreadyExists = 1201, + DocumentPasswordNeeded = 1202, + DocumentInvalidSize = 1203, + // DocumentInvalidNameLength = 1204, // moved to 1002 + DocumentInvalidPassword = 1205, + // DocumentInvalidPasswordLength = 1206, // moved to 1002 + // DocumentInvalidSecret = 1207, // deprecated + // DocumentInvalidSecretLength = 1208, // deprecated + // DocumentInvalidName = 1209, // moved to 1002 + DocumentCorrupted = 1210, + // user - | typeof errorCodeUserInvalidToken; + UserInvalidToken = 1300 +} export type Schema = { httpCode: ContentfulStatusCode; message: string; }; -const errorDefinition: Record = { - [errorCodeCrash]: { +const errorDefinition: Record = { + [ErrorCode.Crash]: { httpCode: 500, message: "An unexpected server error occurred. If this persists, open an issue at: https://github.com/jspaste/backend/issues" }, - [errorCodeUnknown]: { + [ErrorCode.Unknown]: { httpCode: 503, message: "Server handler has not loaded yet. Wait..." }, - [errorCodeValidation]: { + [ErrorCode.Validation]: { httpCode: 400, message: "The request contains invalid or malformed data." }, - [errorCodeNotFound]: { + [ErrorCode.NotFound]: { httpCode: 404, message: "The requested resource could not be found." }, - [errorCodeDummy]: { + [ErrorCode.Dummy]: { httpCode: 200, message: "Placeholder response for documentation purposes." }, // document - [errorCodeDocumentNotFound]: { + [ErrorCode.DocumentNotFound]: { httpCode: 404, message: "No document exists with the specified name." }, - [errorCodeDocumentNameAlreadyExists]: { + [ErrorCode.DocumentNameAlreadyExists]: { httpCode: 409, message: "A document with this name already exists. Choose a different name." }, - [errorCodeDocumentPasswordNeeded]: { + [ErrorCode.DocumentPasswordNeeded]: { httpCode: 401, message: "This document is password protected. Include the password in your request." }, - [errorCodeDocumentInvalidSize]: { + [ErrorCode.DocumentInvalidSize]: { httpCode: 413, message: "The document content exceeds the maximum allowed size." }, - [errorCodeDocumentInvalidPassword]: { + [ErrorCode.DocumentInvalidPassword]: { httpCode: 403, message: "The provided password is incorrect." }, - [errorCodeDocumentCorrupted]: { + [ErrorCode.DocumentCorrupted]: { httpCode: 500, message: "The document content is corrupted and cannot be retrieved." }, // user - [errorCodeUserInvalidToken]: { + [ErrorCode.UserInvalidToken]: { httpCode: 401, message: "The provided authorization token is invalid or missing privileges." } } as const; -export const errorGet = (code: ErrorCodeType, overrideMessage?: string): { code: ErrorCodeType; message: string } => { +export const errorGet = (code: ErrorCode, overrideMessage?: string): { code: ErrorCode; message: string } => { const { message } = errorDefinition[code]; return { code: code, message: overrideMessage ?? message }; }; -export const errorThrow = (code: ErrorCodeType, overrideMessage?: string): never => { +export const errorThrow = (code: ErrorCode, overrideMessage?: string): never => { const { httpCode, message } = errorDefinition[code]; throw new HTTPException(httpCode, { @@ -133,7 +110,7 @@ export const genericErrorResponse = { type({ code: type.number.configure({ description: "The error code", - examples: [errorCodeDummy] + examples: [ErrorCode.Dummy] }), message: type.string.configure({ description: "The error description" diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 22b82882..75f5b0d5 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -1,12 +1,12 @@ import type { Context } from "hono"; +import { constantPathStructStorageData, constantTemporalToUTC, constantTemporalUTC } from "#/global.ts"; import type { Document } from "#db/query.ts"; import type { Env } from "#http/handler.ts"; -import { constantPathStructStorageData, constantTemporalToUTC, constantTemporalUTC } from "../global.ts"; import { documentVersionV1, documentVersionV2 } from "./document.ts"; import { env } from "./env.ts"; -import { errorCodeDocumentCorrupted, errorCodeDocumentInvalidSize, errorThrow } from "./error.ts"; +import { ErrorCode, errorThrow } from "./error.ts"; export const fsWrite = async (ctx: Context, { id }: Pick): Promise => { await using handle = await Deno.open(constantPathStructStorageData + id, { @@ -18,19 +18,19 @@ export const fsWrite = async (ctx: Context, { id }: Pick): let stream: ReadableStream; switch (env.JSPB_DOCUMENT_COMPRESSION) { case documentVersionV1: { - // ctx.req.raw.body is only null on GET/HEAD - stream = (ctx.req.raw.body as NonNullable).pipeThrough(new CompressionStream("deflate")); + // oxlint-disable-next-line typescript-eslint/no-non-null-assertion: ctx.req.raw.body is only null on GET/HEAD + stream = ctx.req.raw.body!.pipeThrough(new CompressionStream("deflate")); break; } case documentVersionV2: { - // ctx.req.raw.body is only null on GET/HEAD - stream = ctx.req.raw.body as NonNullable; + // oxlint-disable-next-line typescript-eslint/no-non-null-assertion: ctx.req.raw.body is only null on GET/HEAD + stream = ctx.req.raw.body!; break; } default: { - return errorThrow(errorCodeDocumentCorrupted); + return errorThrow(ErrorCode.DocumentCorrupted); } } @@ -40,7 +40,7 @@ export const fsWrite = async (ctx: Context, { id }: Pick): void fsDelete({ id: id }); if (why instanceof Deno.errors.BrokenPipe) { - return errorThrow(errorCodeDocumentInvalidSize); + return errorThrow(ErrorCode.DocumentInvalidSize); } throw why; @@ -62,7 +62,7 @@ export const fsRead = async ( ctx: Context, { id, version }: Pick, clientIgnoreCapabilities = false -): Promise>> => { +): Promise> => { const handle = await Deno.open(constantPathStructStorageData + id); const hasClientDeflate = clientIgnoreCapabilities ? false : ctx.req.header("accept-encoding")?.includes("deflate"); @@ -85,7 +85,7 @@ export const fsRead = async ( break; } default: { - return errorThrow(errorCodeDocumentCorrupted); + return errorThrow(ErrorCode.DocumentCorrupted); } } diff --git a/src/utils/humanize.ts b/src/utils/humanize.ts index 244fa976..1e9771ac 100644 --- a/src/utils/humanize.ts +++ b/src/utils/humanize.ts @@ -45,5 +45,5 @@ export const humanizeSize = (input: string): number => { throw new Error(`Invalid size "${input}"`); } - return Number.parseFloat(value) * (sizeUnits[unit.toLowerCase()] as number); + return Number.parseFloat(value) * (sizeUnits[unit.toLowerCase()] ?? 0); }; diff --git a/src/utils/user.ts b/src/utils/user.ts index d0f63093..63d07b93 100644 --- a/src/utils/user.ts +++ b/src/utils/user.ts @@ -1,4 +1,4 @@ -import { constantNanoid } from "../global.ts"; +import { constantNanoid } from "#/global.ts"; export const generateToken = (id: string): string => { const noise = constantNanoid(32); diff --git a/src/utils/validator/document.ts b/src/utils/validator/document.ts index 0153a6ab..4926444f 100644 --- a/src/utils/validator/document.ts +++ b/src/utils/validator/document.ts @@ -18,6 +18,7 @@ export const validatorDocumentName = type(regexBase64URL) description: "The document name", examples: ["myDocumentNameHere"], expected: (ctx) => { + // oxlint-disable-next-line typescript-eslint/switch-exhaustiveness-check switch (ctx.code) { case "pattern": { return "a valid Base64URL"; @@ -41,6 +42,7 @@ export const validatorDocumentNameLength = type.keywords.string.integer.parse ref: "DocumentNameLength", description: "The name length for the document", expected: (ctx) => { + // oxlint-disable-next-line typescript-eslint/switch-exhaustiveness-check switch (ctx.code) { case "domain": { return "a valid integer"; diff --git a/src/utils/validator/handler.ts b/src/utils/validator/handler.ts index f55a7c3e..b8b2b380 100644 --- a/src/utils/validator/handler.ts +++ b/src/utils/validator/handler.ts @@ -1,9 +1,9 @@ import type { sValidator } from "@hono/standard-validator"; -import { errorCodeValidation, errorThrow } from "../error.ts"; +import { ErrorCode, errorThrow } from "../error.ts"; export const validatorHandler: Parameters[2] = (res) => { if (res.success) return; - return errorThrow(errorCodeValidation, res.error[0]?.message); + return errorThrow(ErrorCode.Validation, res.error[0]?.message); }; diff --git a/src/utils/validator/user.ts b/src/utils/validator/user.ts index 8288d22f..3ad1d67b 100644 --- a/src/utils/validator/user.ts +++ b/src/utils/validator/user.ts @@ -10,6 +10,7 @@ export const validatorUserToken = type.string.exactlyLength(constantUserTokenLen description: "A user token", examples: ["myUserTokenHere"], expected: (ctx) => { + // oxlint-disable-next-line typescript-eslint/switch-exhaustiveness-check switch (ctx.code) { case "domain": { return "a string"; @@ -31,6 +32,7 @@ export const validatorUserTokenLegacy = type(regexBase64URL) description: "An unhashed user token", examples: ["myUserTokenHere"], expected: (ctx) => { + // oxlint-disable-next-line typescript-eslint/switch-exhaustiveness-check switch (ctx.code) { case "pattern": { return "a valid Base64URL"; diff --git a/tsconfig.json b/tsconfig.json index 4f59ed92..29f2a788 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "$schema": "https://www.schemastore.org/tsconfig.json", "compilerOptions": { - "lib": ["ESNext", "deno.window", "deno.unstable"], + "lib": ["esnext"], "types": ["node"], "module": "esnext", "moduleResolution": "bundler", @@ -27,7 +27,6 @@ "useUnknownInCatchVariables": true, "verbatimModuleSyntax": true, - "baseUrl": ".", "paths": { "#/*": ["./src/*"], "#db/*": ["./src/database/*"],