diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6447334..fdbffa1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,11 +14,25 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 cache: "pnpm" - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - run: pnpm install --frozen-lockfile - run: pnpm test + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v4 + with: + version: 10.4.1 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + + - run: pnpm install --frozen-lockfile + - run: pnpm lint diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa621..8d5f7b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..3b0a2f0 --- /dev/null +++ b/biome.json @@ -0,0 +1,48 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "files": { + "includes": ["src/**", "test/**", "package.json", "biome.json"] + }, + "assist": { + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "groups": [ + [":BUN:", ":NODE:"], + ":BLANK_LINE:", + [":PACKAGE:", "!@test/**", "cloudflare:test"], + ":BLANK_LINE:", + [":ALIAS:", "@test/**", "!@/**"], + ":BLANK_LINE:", + [":PATH:", "@/**"] + ] + } + } + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true + }, + "javascript": { + "formatter": { + "enabled": true, + "semicolons": "always", + "quoteStyle": "double", + "indentWidth": 2, + "indentStyle": "space", + "trailingCommas": "all" + }, + "linter": { + "enabled": true + } + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 511cab0..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; -import stylistic from "@stylistic/eslint-plugin"; -import { defineConfig } from "eslint/config"; - -/** @type {import('eslint').Linter.Config[]} */ -export default defineConfig([ - { ignores: ["node_modules/", "dist/", ".wrangler/", "worker-configuration.d.ts"] }, - { languageOptions: { globals: globals.browser } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - stylistic.configs.customize({ - quotes: "double", - semi: true, - indent: 2, - }), -]); diff --git a/package.json b/package.json index 976e9c6..62dcbe5 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,33 @@ { - "name": "ens-avatar-worker-v2", - "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy --minify", - "generate-types": "wrangler types", - "type-check": "pnpm generate-types && tsc", - "lint": "eslint -c eslint.config.mjs", - "test": "vitest" - }, - "dependencies": { - "@ensdomains/ensjs": "^4.0.2", - "@hono/valibot-validator": "^0.5.2", - "hono": "^4.7.4", - "valibot": "1.0.0-rc.4", - "viem": "^2.23.10" - }, - "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.8.0", - "@eslint/js": "^9.22.0", - "@stylistic/eslint-plugin": "^4.2.0", - "eslint": "^9.22.0", - "globals": "^15.15.0", - "typescript": "^5.8.2", - "typescript-eslint": "^8.26.1", - "vitest": "3.0.8", - "wrangler": "^4.0.0" - }, - "packageManager": "pnpm@10.4.1", - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild" - ] - } + "name": "ens-avatar-worker-v2", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy --minify", + "generate-types": "wrangler types", + "type-check": "pnpm generate-types && tsc", + "lint": "biome check", + "test": "vitest" + }, + "dependencies": { + "@ensdomains/ensjs": "^4.0.2", + "@hono/valibot-validator": "^0.5.2", + "hono": "^4.7.4", + "valibot": "1.0.0-rc.4", + "viem": "^2.23.10" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.5", + "@cloudflare/vitest-pool-workers": "^0.8.0", + "@eslint/js": "^9.22.0", + "globals": "^15.15.0", + "typescript": "^5.8.2", + "vitest": "3.0.8", + "wrangler": "^4.0.0" + }, + "packageManager": "pnpm@10.4.1", + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03cf8b1..843c7c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,27 +24,21 @@ importers: specifier: ^2.23.10 version: 2.23.10(typescript@5.8.2)(zod@3.24.2) devDependencies: + '@biomejs/biome': + specifier: ^2.3.5 + version: 2.3.5 '@cloudflare/vitest-pool-workers': specifier: ^0.8.0 version: 0.8.0(@cloudflare/workers-types@4.20250312.0)(@vitest/runner@3.0.8)(@vitest/snapshot@3.0.8)(vitest@3.0.8) '@eslint/js': specifier: ^9.22.0 version: 9.22.0 - '@stylistic/eslint-plugin': - specifier: ^4.2.0 - version: 4.2.0(eslint@9.22.0)(typescript@5.8.2) - eslint: - specifier: ^9.22.0 - version: 9.22.0 globals: specifier: ^15.15.0 version: 15.15.0 typescript: specifier: ^5.8.2 version: 5.8.2 - typescript-eslint: - specifier: ^8.26.1 - version: 8.26.1(eslint@9.22.0)(typescript@5.8.2) vitest: specifier: 3.0.8 version: 3.0.8 @@ -60,6 +54,59 @@ packages: '@adraffy/ens-normalize@1.11.0': resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} + '@biomejs/biome@2.3.5': + resolution: {integrity: sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.5': + resolution: {integrity: sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.5': + resolution: {integrity: sha512-qpT8XDqeUlzrOW8zb4k3tjhT7rmvVRumhi2657I2aGcY4B+Ft5fNwDdZGACzn8zj7/K1fdWjgwYE3i2mSZ+vOA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.5': + resolution: {integrity: sha512-eGUG7+hcLgGnMNl1KHVZUYxahYAhC462jF/wQolqu4qso2MSk32Q+QrpN7eN4jAHAg7FUMIo897muIhK4hXhqg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.3.5': + resolution: {integrity: sha512-u/pybjTBPGBHB66ku4pK1gj+Dxgx7/+Z0jAriZISPX1ocTO8aHh8x8e7Kb1rB4Ms0nA/SzjtNOVJ4exVavQBCw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.3.5': + resolution: {integrity: sha512-awVuycTPpVTH/+WDVnEEYSf6nbCBHf/4wB3lquwT7puhNg8R4XvonWNZzUsfHZrCkjkLhFH/vCZK5jHatD9FEg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.3.5': + resolution: {integrity: sha512-XrIVi9YAW6ye0CGQ+yax0gLfx+BFOtKaNX74n+xHWla6Cl6huUmcKNO7HPx7BiKnJUzrxXY1qYlm7xMvi08X4g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.3.5': + resolution: {integrity: sha512-DlBiMlBZZ9eIq4H7RimDSGsYcOtfOIfZOaI5CqsWiSlbTfqbPVfWtCf92wNzx8GNMbu1s7/g3ZZESr6+GwM/SA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.5': + resolution: {integrity: sha512-nUmR8gb6yvrKhtRgzwo/gDimPwnO5a4sCydf8ZS2kHIJhEmSmk+STsusr1LHTuM//wXppBawvSQi2xFXJCdgKQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -438,44 +485,10 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.5.0': - resolution: {integrity: sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.19.2': - resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.1.0': - resolution: {integrity: sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.12.0': - resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.0': - resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.22.0': resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.2.7': - resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -491,26 +504,6 @@ packages: hono: '>=3.9.0' valibot: ^1.0.0 || ^1.0.0-beta.4 || ^1.0.0-rc - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - - '@humanwhocodes/retry@0.4.2': - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} - engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -637,18 +630,6 @@ packages: resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} engines: {node: ^14.21.3 || >=16} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@rollup/rollup-android-arm-eabi@4.35.0': resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} cpu: [arm] @@ -753,65 +734,9 @@ packages: '@scure/bip39@1.5.4': resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} - '@stylistic/eslint-plugin@4.2.0': - resolution: {integrity: sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: '>=9.0.0' - '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@typescript-eslint/eslint-plugin@8.26.1': - resolution: {integrity: sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/parser@8.26.1': - resolution: {integrity: sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/scope-manager@8.26.1': - resolution: {integrity: sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/type-utils@8.26.1': - resolution: {integrity: sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/types@8.26.1': - resolution: {integrity: sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.26.1': - resolution: {integrity: sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/utils@8.26.1': - resolution: {integrity: sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/visitor-keys@8.26.1': - resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/expect@3.0.8': resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} @@ -852,11 +777,6 @@ packages: zod: optional: true - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} @@ -866,21 +786,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} @@ -888,41 +793,20 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - birpc@0.2.14: resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -944,9 +828,6 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -954,10 +835,6 @@ packages: cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} @@ -974,9 +851,6 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -1007,55 +881,9 @@ packages: engines: {node: '>=18'} hasBin: true - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.22.0: - resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1070,41 +898,6 @@ packages: exsolve@1.0.4: resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1113,28 +906,13 @@ packages: get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql-request@6.1.0: resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} peerDependencies: @@ -1144,90 +922,24 @@ packages: resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - hono@4.7.4: resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} engines: {node: '>=16.9.0'} - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isows@1.0.6: resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==} peerDependencies: ws: '*' - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -1238,13 +950,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1257,9 +962,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -1272,10 +974,6 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - ox@0.6.9: resolution: {integrity: sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==} peerDependencies: @@ -1284,29 +982,9 @@ packages: typescript: optional: true - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -1320,48 +998,18 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - postcss@8.5.3: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.35.0: resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -1371,14 +1019,6 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1425,14 +1065,6 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1451,36 +1083,15 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - ts-api-utils@2.0.1: - resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-pattern@5.6.2: resolution: {integrity: sha512-d4IxJUXROL5NCa3amvMg6VQW2HVtZYmUTPfvVtO7zJWGYLJ+mry9v2OmYm+z67aniQoQ8/yFNadiEwtNS9qQiw==} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript-eslint@8.26.1: - resolution: {integrity: sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - typescript-logging@1.0.1: resolution: {integrity: sha512-zp28ABme0m5q/nXabBaY9Hv/35N8lMH4FsvhpUO0zVi4vFs3uKlb5br2it61HAZF5k+U0aP6E67j0VD0IzXGpQ==} @@ -1499,9 +1110,6 @@ packages: unenv@2.0.0-rc.14: resolution: {integrity: sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - valibot@1.0.0-rc.4: resolution: {integrity: sha512-VRaChgFv7Ab0P54AMLu7+GqoexdTPQ54Plj59X9qV0AFozI3j9CGH43skg+TqgMpXnrW8jxlJ2TTHAtAD3t4qA==} peerDependencies: @@ -1597,20 +1205,11 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - workerd@1.20250310.0: resolution: {integrity: sha512-bAaZ9Bmts3mArbIrXYAtr+ZRsAJAAUEsCtvwfBavIYXaZ5sgdEOJBEiBbvsHp6CsVObegOM85tIWpYLpbTxQrQ==} engines: {node: '>=16'} @@ -1650,10 +1249,6 @@ packages: utf-8-validate: optional: true - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - youch@3.2.3: resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==} @@ -1669,6 +1264,41 @@ snapshots: '@adraffy/ens-normalize@1.11.0': {} + '@biomejs/biome@2.3.5': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.5 + '@biomejs/cli-darwin-x64': 2.3.5 + '@biomejs/cli-linux-arm64': 2.3.5 + '@biomejs/cli-linux-arm64-musl': 2.3.5 + '@biomejs/cli-linux-x64': 2.3.5 + '@biomejs/cli-linux-x64-musl': 2.3.5 + '@biomejs/cli-win32-arm64': 2.3.5 + '@biomejs/cli-win32-x64': 2.3.5 + + '@biomejs/cli-darwin-arm64@2.3.5': + optional: true + + '@biomejs/cli-darwin-x64@2.3.5': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.5': + optional: true + + '@biomejs/cli-linux-arm64@2.3.5': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.5': + optional: true + + '@biomejs/cli-linux-x64@2.3.5': + optional: true + + '@biomejs/cli-win32-arm64@2.3.5': + optional: true + + '@biomejs/cli-win32-x64@2.3.5': + optional: true + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -1916,50 +1546,8 @@ snapshots: '@esbuild/win32-x64@0.25.1': optional: true - '@eslint-community/eslint-utils@4.5.0(eslint@9.22.0)': - dependencies: - eslint: 9.22.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/config-array@0.19.2': - dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.0 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.1.0': {} - - '@eslint/core@0.12.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.0': - dependencies: - ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - '@eslint/js@9.22.0': {} - '@eslint/object-schema@2.1.6': {} - - '@eslint/plugin-kit@0.2.7': - dependencies: - '@eslint/core': 0.12.0 - levn: 0.4.1 - '@fastify/busboy@2.1.1': {} '@graphql-typed-document-node/core@3.2.0(graphql@16.10.0)': @@ -1971,19 +1559,6 @@ snapshots: hono: 4.7.4 valibot: 1.0.0-rc.4(typescript@5.8.2) - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.6': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.3.1': {} - - '@humanwhocodes/retry@0.4.2': {} - '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 @@ -2076,18 +1651,6 @@ snapshots: '@noble/hashes@1.7.1': {} - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - '@rollup/rollup-android-arm-eabi@4.35.0': optional: true @@ -2158,99 +1721,8 @@ snapshots: '@noble/hashes': 1.7.1 '@scure/base': 1.2.4 - '@stylistic/eslint-plugin@4.2.0(eslint@9.22.0)(typescript@5.8.2)': - dependencies: - '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) - eslint: 9.22.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 - estraverse: 5.3.0 - picomatch: 4.0.2 - transitivePeerDependencies: - - supports-color - - typescript - '@types/estree@1.0.6': {} - '@types/json-schema@7.0.15': {} - - '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) - '@typescript-eslint/scope-manager': 8.26.1 - '@typescript-eslint/type-utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.26.1 - eslint: 9.22.0 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.26.1 - '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.26.1 - debug: 4.4.0 - eslint: 9.22.0 - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.26.1': - dependencies: - '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/visitor-keys': 8.26.1 - - '@typescript-eslint/type-utils@8.26.1(eslint@9.22.0)(typescript@5.8.2)': - dependencies: - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) - debug: 4.4.0 - eslint: 9.22.0 - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.26.1': {} - - '@typescript-eslint/typescript-estree@8.26.1(typescript@5.8.2)': - dependencies: - '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/visitor-keys': 8.26.1 - debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.26.1(eslint@9.22.0)(typescript@5.8.2)': - dependencies: - '@eslint-community/eslint-utils': 4.5.0(eslint@9.22.0) - '@typescript-eslint/scope-manager': 8.26.1 - '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) - eslint: 9.22.0 - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.26.1': - dependencies: - '@typescript-eslint/types': 8.26.1 - eslint-visitor-keys: 4.2.0 - '@vitest/expect@3.0.8': dependencies: '@vitest/spy': 3.0.8 @@ -2296,58 +1768,22 @@ snapshots: typescript: 5.8.2 zod: 3.24.2 - acorn-jsx@5.3.2(acorn@8.14.1): - dependencies: - acorn: 8.14.1 - acorn-walk@8.3.2: {} acorn@8.14.0: {} - acorn@8.14.1: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - as-table@1.0.55: dependencies: printable-characters: 1.0.42 assertion-error@2.0.1: {} - balanced-match@1.0.2: {} - birpc@0.2.14: {} blake3-wasm@2.1.5: {} - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - cac@6.7.14: {} - callsites@3.1.0: {} - chai@5.2.0: dependencies: assertion-error: 2.0.1 @@ -2356,11 +1792,6 @@ snapshots: loupe: 3.1.3 pathval: 2.0.0 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - check-error@2.1.1: {} cjs-module-lexer@1.4.3: {} @@ -2368,8 +1799,10 @@ snapshots: color-convert@2.0.1: dependencies: color-name: 1.1.4 + optional: true - color-name@1.1.4: {} + color-name@1.1.4: + optional: true color-string@1.9.1: dependencies: @@ -2383,8 +1816,6 @@ snapshots: color-string: 1.9.1 optional: true - concat-map@0.0.1: {} - cookie@0.5.0: {} cross-fetch@3.2.0: @@ -2393,12 +1824,6 @@ snapshots: transitivePeerDependencies: - encoding - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - data-uri-to-buffer@2.0.2: {} debug@4.4.0: @@ -2407,8 +1832,6 @@ snapshots: deep-eql@5.0.2: {} - deep-is@0.1.4: {} - defu@6.1.4: {} detect-libc@2.0.3: @@ -2482,79 +1905,10 @@ snapshots: '@esbuild/win32-ia32': 0.25.1 '@esbuild/win32-x64': 0.25.1 - escape-string-regexp@4.0.0: {} - - eslint-scope@8.3.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.0: {} - - eslint@9.22.0: - dependencies: - '@eslint-community/eslint-utils': 4.5.0(eslint@9.22.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.19.2 - '@eslint/config-helpers': 0.1.0 - '@eslint/core': 0.12.0 - '@eslint/eslintrc': 3.3.0 - '@eslint/js': 9.22.0 - '@eslint/plugin-kit': 0.2.7 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.6 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.0 - escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.3.0: - dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.0 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 - esutils@2.0.3: {} - eventemitter3@5.0.1: {} exit-hook@2.2.1: {} @@ -2563,44 +1917,6 @@ snapshots: exsolve@1.0.4: {} - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - fsevents@2.3.3: optional: true @@ -2609,22 +1925,10 @@ snapshots: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} - globals@14.0.0: {} - globals@15.15.0: {} - graphemer@1.4.0: {} - graphql-request@6.1.0(graphql@16.10.0): dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) @@ -2635,74 +1939,21 @@ snapshots: graphql@16.10.0: {} - has-flag@4.0.0: {} - hono@4.7.4: {} - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - is-arrayish@0.3.2: optional: true - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - isexe@2.0.0: {} - isows@1.0.6(ws@8.18.1): dependencies: ws: 8.18.1 - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - loupe@3.1.3: {} magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - mime@3.0.0: {} miniflare@4.20250310.0: @@ -2722,37 +1973,18 @@ snapshots: - bufferutil - utf-8-validate - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - ms@2.1.3: {} mustache@4.2.0: {} nanoid@3.3.9: {} - natural-compare@1.4.0: {} - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 ohash@2.0.11: {} - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - ox@0.6.9(typescript@5.8.2)(zod@3.24.2): dependencies: '@adraffy/ens-normalize': 1.11.0 @@ -2767,24 +1999,8 @@ snapshots: transitivePeerDependencies: - zod - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - pako@2.1.0: {} - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - path-to-regexp@6.3.0: {} pathe@2.0.3: {} @@ -2793,28 +2009,14 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} - - picomatch@4.0.2: {} - postcss@8.5.3: dependencies: nanoid: 3.3.9 picocolors: 1.1.1 source-map-js: 1.2.1 - prelude-ls@1.2.1: {} - printable-characters@1.0.42: {} - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - resolve-from@4.0.0: {} - - reusify@1.1.0: {} - rollup@4.35.0: dependencies: '@types/estree': 1.0.6 @@ -2840,10 +2042,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.35.0 fsevents: 2.3.3 - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - semver@7.7.1: {} sharp@0.33.5: @@ -2873,12 +2071,6 @@ snapshots: '@img/sharp-win32-x64': 0.33.5 optional: true - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - siginfo@2.0.0: {} simple-swizzle@0.2.2: @@ -2922,12 +2114,6 @@ snapshots: stoppable@1.1.0: {} - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -2938,35 +2124,13 @@ snapshots: tinyspy@3.0.2: {} - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - tr46@0.0.3: {} - ts-api-utils@2.0.1(typescript@5.8.2): - dependencies: - typescript: 5.8.2 - ts-pattern@5.6.2: {} tslib@2.8.1: optional: true - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript-eslint@8.26.1(eslint@9.22.0)(typescript@5.8.2): - dependencies: - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2) - '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) - eslint: 9.22.0 - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - typescript-logging@1.0.1: dependencies: stacktrace-js: 1.3.1 @@ -2987,10 +2151,6 @@ snapshots: pathe: 2.0.3 ufo: 1.5.4 - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - valibot@1.0.0-rc.4(typescript@5.8.2): optionalDependencies: typescript: 5.8.2 @@ -3084,17 +2244,11 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - which@2.0.2: - dependencies: - isexe: 2.0.0 - why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - word-wrap@1.2.5: {} - workerd@1.20250310.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20250310.0 @@ -3125,8 +2279,6 @@ snapshots: ws@8.18.1: {} - yocto-queue@0.1.0: {} - youch@3.2.3: dependencies: cookie: 0.5.0 diff --git a/src/index.ts b/src/index.ts index 67876e3..4e4d772 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ -import { createApp } from "./utils/hono"; -import { NetworkMiddlewareEnv, networkMiddleware } from "./utils/chains"; +import { cors } from "hono/cors"; + import avatarRouter from "./routes/avatar"; import headerRouter from "./routes/header"; -import { cors } from "hono/cors"; +import { type NetworkMiddlewareEnv, networkMiddleware } from "./utils/chains"; +import { createApp } from "./utils/hono"; const app = createApp(); app.use( @@ -21,8 +22,7 @@ app.use( if (hostname.endsWith(".ens.domains")) { return requestOrigin; // reflect subdomain } - } - catch { + } catch { // If it's not a valid URL, deny } return ""; // empty => disallowed diff --git a/src/routes/avatar.ts b/src/routes/avatar.ts index bab5ca1..ccfcfb9 100644 --- a/src/routes/avatar.ts +++ b/src/routes/avatar.ts @@ -1,17 +1,17 @@ import { vValidator } from "@hono/valibot-validator"; import * as v from "valibot"; -import { createApp } from "@/utils/hono"; -import { clientMiddleware, NetworkMiddlewareEnv } from "@/utils/chains"; -import { Address, isAddress, sha256 } from "viem"; -import { Hex } from "viem"; +import { type Address, type Hex, isAddress, sha256 } from "viem"; import { normalize } from "viem/ens"; -import { getVerifiedAddress } from "@/utils/eth"; -import { getOwnerAndAvailable } from "@/utils/owner"; + +import { clientMiddleware, type NetworkMiddlewareEnv } from "@/utils/chains"; import { dataURLToBytes, R2GetOrHead } from "@/utils/data"; +import { getVerifiedAddress } from "@/utils/eth"; +import { createApp } from "@/utils/hono"; import { findAndPromoteUnregisteredMedia, MEDIA_BUCKET_KEY, } from "@/utils/media"; +import { getOwnerAndAvailable } from "@/utils/owner"; import { isParentOwner, isSubname } from "@/utils/subname"; const router = createApp(); @@ -50,8 +50,8 @@ router.get("/:name", clientMiddleware, async (c) => { ); if ( - existingAvatarFile - && existingAvatarFile.httpMetadata?.contentType === "image/jpeg" + existingAvatarFile && + existingAvatarFile.httpMetadata?.contentType === "image/jpeg" ) { c.header("Content-Type", "image/jpeg"); c.header("Content-Length", existingAvatarFile.size.toString()); @@ -121,8 +121,7 @@ router.put( if (!available) { if (!owner) { return c.text("Name not found", 404); - } - else if (verifiedAddress !== owner) { + } else if (verifiedAddress !== owner) { return c.text( `Address ${verifiedAddress} is not the owner of ${name}`, 403, @@ -130,7 +129,10 @@ router.put( } } // Check that user is the parent owner of the name if it is a subname and not available - else if (isSubname(name) && !(await isParentOwner({ name, client, verifiedAddress }))) { + else if ( + isSubname(name) && + !(await isParentOwner({ name, client, verifiedAddress })) + ) { return c.text( `Address ${verifiedAddress} is not the parent owner of ${name}`, 403, @@ -152,8 +154,7 @@ router.put( if (uploaded.key === key) { return c.json({ message: "uploaded" }, 200); - } - else { + } else { return c.text(`${name} not uploaded`, 500); } }, diff --git a/src/routes/header.ts b/src/routes/header.ts index d8673fe..128c7f4 100644 --- a/src/routes/header.ts +++ b/src/routes/header.ts @@ -1,15 +1,18 @@ import { vValidator } from "@hono/valibot-validator"; import * as v from "valibot"; -import { createApp } from "@/utils/hono"; -import { clientMiddleware, NetworkMiddlewareEnv } from "@/utils/chains"; -import { Address, isAddress, sha256 } from "viem"; -import { Hex } from "viem"; +import { type Address, type Hex, isAddress, sha256 } from "viem"; import { normalize } from "viem/ens"; + +import { clientMiddleware, type NetworkMiddlewareEnv } from "@/utils/chains"; +import { dataURLToBytes, R2GetOrHead } from "@/utils/data"; import { getVerifiedAddress } from "@/utils/eth"; +import { createApp } from "@/utils/hono"; +import { + findAndPromoteUnregisteredMedia, + MEDIA_BUCKET_KEY, +} from "@/utils/media"; import { getOwnerAndAvailable } from "@/utils/owner"; -import { dataURLToBytes, R2GetOrHead } from "@/utils/data"; -import { findAndPromoteUnregisteredMedia, MEDIA_BUCKET_KEY } from "@/utils/media"; -import { isSubname, isParentOwner } from "@/utils/subname"; +import { isParentOwner, isSubname } from "@/utils/subname"; const router = createApp(); @@ -28,10 +31,7 @@ const uploadSchema = v.object({ unverifiedAddress: v.pipe( v.string("unverifiedAddress value is missing"), v.hexadecimal("unverifiedAddress value is not hex"), - v.check( - isAddress, - "unverifiedAddress value is not address", - ), + v.check(isAddress, "unverifiedAddress value is not address"), ), }); @@ -47,7 +47,10 @@ router.get("/:name/h", clientMiddleware, async (c) => { isHead, ); - if (existingHeaderFile && existingHeaderFile.httpMetadata?.contentType === "image/jpeg") { + if ( + existingHeaderFile && + existingHeaderFile.httpMetadata?.contentType === "image/jpeg" + ) { c.header("Content-Type", "image/jpeg"); c.header("Content-Length", existingHeaderFile.size.toString()); @@ -72,78 +75,87 @@ router.get("/:name/h", clientMiddleware, async (c) => { return c.text(`${name} not found on ${network}`, 404); }); -router.put("/:name/h", clientMiddleware, vValidator("json", uploadSchema), async (c) => { - const name = c.req.param("name"); - const { expiry, dataURL, sig, unverifiedAddress } = c.req.valid("json"); - const { network, client } = c.var; +router.put( + "/:name/h", + clientMiddleware, + vValidator("json", uploadSchema), + async (c) => { + const name = c.req.param("name"); + const { expiry, dataURL, sig, unverifiedAddress } = c.req.valid("json"); + const { network, client } = c.var; - const { mime, bytes } = dataURLToBytes(dataURL); - const hash = sha256(bytes); + const { mime, bytes } = dataURLToBytes(dataURL); + const hash = sha256(bytes); - if (!hash) { - return c.text("Failed to hash image", 500); - } + if (!hash) { + return c.text("Failed to hash image", 500); + } - if (mime !== "image/jpeg") - return c.text("File must be of type image/jpeg", 415); + if (mime !== "image/jpeg") + return c.text("File must be of type image/jpeg", 415); - if (name !== normalize(name)) - return c.text("Name must be in normalized form", 400); + if (name !== normalize(name)) + return c.text("Name must be in normalized form", 400); - const verifiedAddress = await getVerifiedAddress({ - client, - sig: sig as Hex, - expiry, - name, - hash, - unverifiedAddress: unverifiedAddress as Address, - uploadType: "header", - }); + const verifiedAddress = await getVerifiedAddress({ + client, + sig: sig as Hex, + expiry, + name, + hash, + unverifiedAddress: unverifiedAddress as Address, + uploadType: "header", + }); - if (!verifiedAddress) { - return c.text("Invalid signature", 400); - } + if (!verifiedAddress) { + return c.text("Invalid signature", 400); + } - if (bytes.byteLength > MAX_IMAGE_SIZE) { - return c.text("Image is too large", 413); - } + if (bytes.byteLength > MAX_IMAGE_SIZE) { + return c.text("Image is too large", 413); + } - const { available, owner } = await getOwnerAndAvailable({ client, name }); - if (!available) { - if (!owner) { - return c.text("Name not found", 404); + const { available, owner } = await getOwnerAndAvailable({ client, name }); + if (!available) { + if (!owner) { + return c.text("Name not found", 404); + } else if (verifiedAddress !== owner) { + return c.text( + `Address ${verifiedAddress} is not the owner of ${name}`, + 403, + ); + } } - else if (verifiedAddress !== owner) { - return c.text(`Address ${verifiedAddress} is not the owner of ${name}`, 403); + // Check that user is the parent owner of the name if it is a subname and not available + else if ( + isSubname(name) && + !(await isParentOwner({ name, client, verifiedAddress })) + ) { + return c.text( + `Address ${verifiedAddress} is not the parent owner of ${name}`, + 403, + ); } - } - // Check that user is the parent owner of the name if it is a subname and not available - else if (isSubname(name) && !(await isParentOwner({ name, client, verifiedAddress }))) { - return c.text( - `Address ${verifiedAddress} is not the parent owner of ${name}`, - 403, - ); - } - if (parseInt(expiry) < Date.now()) { - return c.text("Signature expired", 403); - } + if (parseInt(expiry) < Date.now()) { + return c.text("Signature expired", 403); + } - const bucket = c.env.HEADER_BUCKET; - const key = available - ? MEDIA_BUCKET_KEY.unregistered(network, name, verifiedAddress) - : MEDIA_BUCKET_KEY.registered(network, name); + const bucket = c.env.HEADER_BUCKET; + const key = available + ? MEDIA_BUCKET_KEY.unregistered(network, name, verifiedAddress) + : MEDIA_BUCKET_KEY.registered(network, name); - const uploaded = await bucket.put(key, bytes, { - httpMetadata: { contentType: "image/jpeg" }, - }); + const uploaded = await bucket.put(key, bytes, { + httpMetadata: { contentType: "image/jpeg" }, + }); - if (uploaded.key === key) { - return c.json({ message: "uploaded" }, 200); - } - else { - return c.text(`${name} not uploaded`, 500); - } -}); + if (uploaded.key === key) { + return c.json({ message: "uploaded" }, 200); + } else { + return c.text(`${name} not uploaded`, 500); + } + }, +); export default router; diff --git a/src/utils/chains.test.ts b/src/utils/chains.test.ts index 6825158..7d35ff5 100644 --- a/src/utils/chains.test.ts +++ b/src/utils/chains.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "vitest"; + import { getChainFromNetwork } from "./chains"; describe("getChainFromNetwork", () => { diff --git a/src/utils/chains.ts b/src/utils/chains.ts index 09f1a0e..ebe4800 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -1,13 +1,17 @@ -import { addEnsContracts, createEnsPublicClient } from "@ensdomains/ensjs"; -import { mainnet, goerli, sepolia, holesky } from "viem/chains"; -import { BaseEnv } from "./hono"; +import { + addEnsContracts, + type createEnsPublicClient, + ensPublicActions, + ensSubgraphActions, +} from "@ensdomains/ensjs"; +import type { Context } from "hono"; import { createMiddleware } from "hono/factory"; -import { http } from "viem"; -import { addLocalhostEnsContracts } from "./localhost-chain"; -import { ensPublicActions, ensSubgraphActions } from "@ensdomains/ensjs"; -import { createClient } from "viem"; -import { Context } from "hono"; +import { createClient, http } from "viem"; +import { goerli, holesky, mainnet, sepolia } from "viem/chains"; + import { getErrorMessage } from "./error"; +import type { BaseEnv } from "./hono"; +import { addLocalhostEnsContracts } from "./localhost-chain"; const baseChains = [ addEnsContracts(mainnet), @@ -22,10 +26,16 @@ export const chains = baseChains; export type Chain = | (typeof baseChains)[number] | ReturnType; -export type Network = "mainnet" | "goerli" | "sepolia" | "holesky" | "localhost"; +export type Network = + | "mainnet" + | "goerli" + | "sepolia" + | "holesky" + | "localhost"; export type EnsPublicClient = ReturnType; -const isDev = (c: Context) => c.env.ENVIRONMENT === "dev"; +const isDev = (c: Context) => + c.env.ENVIRONMENT === "dev"; export const getChainFromNetwork = ( _network: string, @@ -39,7 +49,7 @@ export const getChainFromNetwork = ( } const network = lowercased === "mainnet" ? "ethereum" : lowercased; - return chains.find(chain => chain.name.toLowerCase() === network); + return chains.find((chain) => chain.name.toLowerCase() === network); }; export type NetworkMiddlewareEnv = { @@ -56,7 +66,8 @@ export const networkMiddleware = createMiddleware< const network = c.req.param("network")?.toLowerCase() ?? "mainnet"; // Check if localhost is being accessed in non-dev mode - if (network === "localhost" && !isDev(c)) throw new Error("localhost is only available in development mode"); + if (network === "localhost" && !isDev(c)) + throw new Error("localhost is only available in development mode"); const chain = getChainFromNetwork(network, c); @@ -68,8 +79,7 @@ export const networkMiddleware = createMiddleware< c.set("network", network as Network); await next(); - } - catch (e) { + } catch (e) { return c.text(getErrorMessage(e, "Network middleware error: "), 400); } }); diff --git a/src/utils/data.test.ts b/src/utils/data.test.ts index a8d662f..585115b 100644 --- a/src/utils/data.test.ts +++ b/src/utils/data.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test } from "vitest"; + import { dataURLToBytes } from "@/utils/data"; describe("dataURLToBytes", () => { test("should correctly extract MIME type and bytes from a PNG data URL", () => { - const dataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + const dataURL = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; const { mime, bytes } = dataURLToBytes(dataURL); expect(mime).toBe("image/png"); @@ -12,7 +14,8 @@ describe("dataURLToBytes", () => { }); test("should correctly extract MIME type and bytes from a JPEG data URL", () => { - const dataURL = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8R6KKKAP/2Q=="; + const dataURL = + "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8R6KKKAP/2Q=="; const { mime, bytes } = dataURLToBytes(dataURL); expect(mime).toBe("image/jpeg"); @@ -21,7 +24,8 @@ describe("dataURLToBytes", () => { }); test("should correctly extract MIME type and bytes from an SVG data URL", () => { - const dataURL = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIzIiBmaWxsPSJyZWQiIC8+PC9zdmc+"; + const dataURL = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIzIiBmaWxsPSJyZWQiIC8+PC9zdmc+"; const { mime, bytes } = dataURLToBytes(dataURL); expect(mime).toBe("image/svg+xml"); @@ -43,7 +47,8 @@ describe("dataURLToBytes", () => { }); test("should handle data URLs without a MIME type", () => { - const dataURL = "data:;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + const dataURL = + "data:;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; const { mime, bytes } = dataURLToBytes(dataURL); expect(mime).toBe(""); diff --git a/src/utils/data.ts b/src/utils/data.ts index 698de83..c2bfcb4 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -3,7 +3,7 @@ import { sha256 } from "viem/utils"; export const dataURLToBytes = (dataURL: string) => { const base64 = dataURL.split(",")[1]; const mime = dataURL.split(",")[0].split(":")[1].split(";")[0]; - const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); + const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); return { mime, bytes }; }; @@ -12,7 +12,11 @@ export const makeHashFromDataUrl = (dataUrl: string) => { return sha256(bytes); }; -export const R2GetOrHead = async (bucket: R2Bucket, key: string, isHead: boolean): Promise => { +export const R2GetOrHead = async ( + bucket: R2Bucket, + key: string, + isHead: boolean, +): Promise => { if (!isHead) { return await bucket.get(key); } diff --git a/src/utils/eth.test.ts b/src/utils/eth.test.ts index e44a426..b753213 100644 --- a/src/utils/eth.test.ts +++ b/src/utils/eth.test.ts @@ -1,8 +1,14 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { typedDataParameters, getVerifiedAddress } from "@/utils/eth"; -import { createTestUploadData, TEST_ACCOUNT, TEST_PUBLIC_CLIENT } from "@test/setup/helpers"; -import { Address, Hex } from "viem"; +import type { Address, Hex } from "viem"; import { verifyTypedData } from "viem/actions"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import { + createTestUploadData, + TEST_ACCOUNT, + TEST_PUBLIC_CLIENT, +} from "@test/setup/helpers"; + +import { getVerifiedAddress, typedDataParameters } from "@/utils/eth"; vi.mock("viem/actions", { spy: true, @@ -11,7 +17,8 @@ vi.mock("viem/actions", { describe("getVerifiedAddress", () => { // Sample ENS name and hash for testing const TEST_NAME = "test.eth"; - const TEST_HASH = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" as Hex; + const TEST_HASH = + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" as Hex; beforeEach(() => { vi.resetAllMocks(); @@ -38,20 +45,17 @@ describe("getVerifiedAddress", () => { // Assert expect(result).toEqual(address); - expect(vi.mocked(verifyTypedData)).toHaveBeenCalledWith( - expect.anything(), - { - ...typedDataParameters, - address, - signature: sig, - message: { - upload: "avatar", - expiry, - name: TEST_NAME, - hash: TEST_HASH, - }, + expect(vi.mocked(verifyTypedData)).toHaveBeenCalledWith(expect.anything(), { + ...typedDataParameters, + address, + signature: sig, + message: { + upload: "avatar", + expiry, + name: TEST_NAME, + hash: TEST_HASH, }, - ); + }); }); test("should return null when signature is invalid", async () => { @@ -75,20 +79,17 @@ describe("getVerifiedAddress", () => { // Assert expect(result).toBeNull(); - expect(vi.mocked(verifyTypedData)).toHaveBeenCalledWith( - expect.anything(), - { - ...typedDataParameters, - address, - signature: "0x0", - message: { - upload: "avatar", - expiry, - name: TEST_NAME, - hash: TEST_HASH, - }, + expect(vi.mocked(verifyTypedData)).toHaveBeenCalledWith(expect.anything(), { + ...typedDataParameters, + address, + signature: "0x0", + message: { + upload: "avatar", + expiry, + name: TEST_NAME, + hash: TEST_HASH, }, - ); + }); }); test("should return null when verification throws an error", async () => { @@ -100,7 +101,9 @@ describe("getVerifiedAddress", () => { ); // Mock the verifyTypedData function to throw an error - vi.mocked(verifyTypedData).mockRejectedValue(new Error("Verification error")); + vi.mocked(verifyTypedData).mockRejectedValue( + new Error("Verification error"), + ); // Act const result = await getVerifiedAddress({ @@ -171,20 +174,17 @@ describe("getVerifiedAddress", () => { // Assert expect(result).toEqual(address); - expect(verifyTypedData).toHaveBeenCalledWith( - expect.anything(), - { - ...typedDataParameters, - address, - signature: sig, - message: { - upload: "header", - expiry, - name: TEST_NAME, - hash: TEST_HASH, - }, + expect(verifyTypedData).toHaveBeenCalledWith(expect.anything(), { + ...typedDataParameters, + address, + signature: sig, + message: { + upload: "header", + expiry, + name: TEST_NAME, + hash: TEST_HASH, }, - ); + }); }); test("integration: signature verification should work for real signatures", async () => { diff --git a/src/utils/eth.ts b/src/utils/eth.ts index 7a0e197..d85ba3e 100644 --- a/src/utils/eth.ts +++ b/src/utils/eth.ts @@ -1,7 +1,8 @@ -import { Address, Client, getAddress, Hex } from "viem"; -import { EnsPublicClient } from "./chains"; +import { type Address, type Client, getAddress, type Hex } from "viem"; import { verifyTypedData } from "viem/actions"; +import type { EnsPublicClient } from "./chains"; + export const typedDataParameters = { domain: { name: "Ethereum Name Service", diff --git a/src/utils/localhost-chain.ts b/src/utils/localhost-chain.ts index a18b0b5..2dba2fe 100644 --- a/src/utils/localhost-chain.ts +++ b/src/utils/localhost-chain.ts @@ -1,5 +1,5 @@ +import type { Address, Chain } from "viem"; import { localhost } from "viem/chains"; -import type { Chain, Address } from "viem"; // Add ENS contracts to localhost chain for development export const addLocalhostEnsContracts = (env: Env): Chain => { @@ -9,8 +9,10 @@ export const addLocalhostEnsContracts = (env: Env): Chain => { LOCALHOST_ENS_NAME_WRAPPER: env.LOCALHOST_ENS_NAME_WRAPPER, LOCALHOST_ENS_UNIVERSAL_RESOLVER: env.LOCALHOST_ENS_UNIVERSAL_RESOLVER, LOCALHOST_ENS_PUBLIC_RESOLVER: env.LOCALHOST_ENS_PUBLIC_RESOLVER, - LOCALHOST_ENS_BASE_REGISTRAR_IMPLEMENTATION: env.LOCALHOST_ENS_BASE_REGISTRAR_IMPLEMENTATION, - LOCALHOST_ENS_ETH_REGISTRAR_CONTROLLER: env.LOCALHOST_ENS_ETH_REGISTRAR_CONTROLLER, + LOCALHOST_ENS_BASE_REGISTRAR_IMPLEMENTATION: + env.LOCALHOST_ENS_BASE_REGISTRAR_IMPLEMENTATION, + LOCALHOST_ENS_ETH_REGISTRAR_CONTROLLER: + env.LOCALHOST_ENS_ETH_REGISTRAR_CONTROLLER, LOCALHOST_ENS_REVERSE_REGISTRAR: env.LOCALHOST_ENS_REVERSE_REGISTRAR, LOCALHOST_MULTICALL3: env.LOCALHOST_MULTICALL3, }; @@ -22,7 +24,9 @@ export const addLocalhostEnsContracts = (env: Env): Chain => { .map(([name]) => name); if (missingContracts.length > 0) { - throw new Error(`Missing required localhost ENS contract addresses: ${missingContracts.join(", ")}`); + throw new Error( + `Missing required localhost ENS contract addresses: ${missingContracts.join(", ")}`, + ); } return { diff --git a/src/utils/media.test.ts b/src/utils/media.test.ts index 2393424..7961e85 100644 --- a/src/utils/media.test.ts +++ b/src/utils/media.test.ts @@ -1,10 +1,12 @@ -import { describe, expect, test, vi, beforeEach, assert } from "vitest"; +import { env } from "cloudflare:test"; +import { assert, beforeEach, describe, expect, test, vi } from "vitest"; + +import type { ModuleMock } from "@test/setup/meta"; + +import type { EnsPublicClient } from "@/utils/chains"; +import type { MediaType } from "@/utils/media"; import * as media from "@/utils/media"; import * as owner from "@/utils/owner"; -import { ModuleMock } from "@test/setup/meta"; -import { env } from "cloudflare:test"; -import { EnsPublicClient } from "@/utils/chains"; -import { MediaType } from "@/utils/media"; const readableStreamToString = async (stream: ReadableStream) => { const reader = stream.getReader(); @@ -15,8 +17,7 @@ const readableStreamToString = async (stream: ReadableStream) => { const { value, done: isDone } = await reader.read(); if (isDone) { done = true; - } - else { + } else { chunks.push(value); } } @@ -34,9 +35,13 @@ const readableStreamToString = async (stream: ReadableStream) => { return decoder.decode(result); }; -vi.mock("@/utils/owner", () => ({ - getOwnerAndAvailable: vi.fn(), -}) satisfies ModuleMock); +vi.mock( + "@/utils/owner", + () => + ({ + getOwnerAndAvailable: vi.fn(), + }) satisfies ModuleMock, +); describe("findAndPromoteUnregisteredMedia", () => { const MOCK_DATA = { @@ -55,14 +60,22 @@ describe("findAndPromoteUnregisteredMedia", () => { vi.resetAllMocks(); }); - const insertMockUnregisteredMedia = async (name: string, owner: string, mediaType: MediaType) => { + const insertMockUnregisteredMedia = async ( + name: string, + owner: string, + mediaType: MediaType, + ) => { const bucket = media.getMediaBucket(env, mediaType); - return bucket.put(media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, name, owner), new TextEncoder().encode(`test-image-data-${name}-${owner}`), { - httpMetadata: { - contentType: "image/jpeg", + return bucket.put( + media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, name, owner), + new TextEncoder().encode(`test-image-data-${name}-${owner}`), + { + httpMetadata: { + contentType: "image/jpeg", + }, }, - }); + ); }; describe.for(["avatar", "header"] as const)("%s media", (mediaType) => { @@ -132,7 +145,13 @@ describe("findAndPromoteUnregisteredMedia", () => { }); expect(result).toBeUndefined(); - expect(mediaBucketSpy.get).toHaveBeenCalledWith(media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, MOCK_DATA.owner)); + expect(mediaBucketSpy.get).toHaveBeenCalledWith( + media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + MOCK_DATA.owner, + ), + ); expect(mediaBucketSpy.put).not.toHaveBeenCalled(); }); @@ -143,7 +162,11 @@ describe("findAndPromoteUnregisteredMedia", () => { available: false, }); - await insertMockUnregisteredMedia(MOCK_DATA.name, MOCK_DATA.owner, mediaType); + await insertMockUnregisteredMedia( + MOCK_DATA.name, + MOCK_DATA.owner, + mediaType, + ); await insertMockUnregisteredMedia(MOCK_DATA.name, "0x0", mediaType); const result = await media.findAndPromoteUnregisteredMedia({ @@ -164,20 +187,34 @@ describe("findAndPromoteUnregisteredMedia", () => { expect(mediaBucketSpy.put).toHaveBeenCalledWith( media.MEDIA_BUCKET_KEY.registered(MOCK_DATA.network, MOCK_DATA.name), expect.anything(), - { httpMetadata: { - contentType: "image/jpeg", - } }, + { + httpMetadata: { + contentType: "image/jpeg", + }, + }, ); // Verify the list and delete operations expect(mediaBucketSpy.list).toHaveBeenCalledWith({ - prefix: media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, ""), + prefix: media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + "", + ), cursor: undefined, }); expect(mediaBucketSpy.delete).toHaveBeenCalledWith([ - media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, "0x0"), - media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, MOCK_DATA.owner), + media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + "0x0", + ), + media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + MOCK_DATA.owner, + ), ]); }); @@ -188,25 +225,31 @@ describe("findAndPromoteUnregisteredMedia", () => { available: false, }); - const unregisteredAvatarFile1 = await insertMockUnregisteredMedia(MOCK_DATA.name, MOCK_DATA.owner, mediaType); - const unregisteredAvatarFile2 = await insertMockUnregisteredMedia(MOCK_DATA.name, "0x0", mediaType); + const unregisteredAvatarFile1 = await insertMockUnregisteredMedia( + MOCK_DATA.name, + MOCK_DATA.owner, + mediaType, + ); + const unregisteredAvatarFile2 = await insertMockUnregisteredMedia( + MOCK_DATA.name, + "0x0", + mediaType, + ); // Mock R2 bucket list to return objects with pagination const mockCursor = "next-page-cursor"; - mediaBucketSpy.list.mockResolvedValueOnce({ - objects: [ - unregisteredAvatarFile1, - ], - truncated: true, - cursor: mockCursor, - delimitedPrefixes: [], - }).mockResolvedValueOnce({ - objects: [ - unregisteredAvatarFile2, - ], - truncated: false, - delimitedPrefixes: [], - }); + mediaBucketSpy.list + .mockResolvedValueOnce({ + objects: [unregisteredAvatarFile1], + truncated: true, + cursor: mockCursor, + delimitedPrefixes: [], + }) + .mockResolvedValueOnce({ + objects: [unregisteredAvatarFile2], + truncated: false, + delimitedPrefixes: [], + }); const result = await media.findAndPromoteUnregisteredMedia({ env: env, @@ -222,20 +265,36 @@ describe("findAndPromoteUnregisteredMedia", () => { // Verify the list and delete operations were called twice expect(mediaBucketSpy.list).toHaveBeenCalledTimes(2); expect(mediaBucketSpy.list).toHaveBeenCalledWith({ - prefix: media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, ""), + prefix: media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + "", + ), cursor: undefined, }); expect(mediaBucketSpy.list).toHaveBeenCalledWith({ - prefix: media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, ""), + prefix: media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + "", + ), cursor: mockCursor, }); expect(mediaBucketSpy.delete).toHaveBeenCalledTimes(2); expect(mediaBucketSpy.delete).toHaveBeenCalledWith([ - media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, MOCK_DATA.owner), + media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + MOCK_DATA.owner, + ), ]); expect(mediaBucketSpy.delete).toHaveBeenCalledWith([ - media.MEDIA_BUCKET_KEY.unregistered(MOCK_DATA.network, MOCK_DATA.name, "0x0"), + media.MEDIA_BUCKET_KEY.unregistered( + MOCK_DATA.network, + MOCK_DATA.name, + "0x0", + ), ]); }); }); diff --git a/src/utils/media.ts b/src/utils/media.ts index 7b82f94..5c7d754 100644 --- a/src/utils/media.ts +++ b/src/utils/media.ts @@ -1,4 +1,4 @@ -import { EnsPublicClient, Network } from "./chains"; +import type { EnsPublicClient, Network } from "./chains"; import { getOwnerAndAvailable } from "./owner"; export type MediaType = "avatar" | "header"; @@ -15,11 +15,9 @@ export const MEDIA_BUCKET_KEY = { export const getMediaBucket = (env: Env, mediaType: MediaType) => { if (mediaType === "avatar") { return env.AVATAR_BUCKET; - } - else if (mediaType === "header") { + } else if (mediaType === "header") { return env.HEADER_BUCKET; - } - else { + } else { throw new Error(`Invalid media type: ${mediaType}`); } }; @@ -68,7 +66,9 @@ export const findAndPromoteUnregisteredMedia = async ({ const bucket = getMediaBucket(env, mediaType); - const unregisteredMediaFile = await bucket.get(MEDIA_BUCKET_KEY.unregistered(network, name, owner)); + const unregisteredMediaFile = await bucket.get( + MEDIA_BUCKET_KEY.unregistered(network, name, owner), + ); if (!unregisteredMediaFile) { return; @@ -80,7 +80,7 @@ export const findAndPromoteUnregisteredMedia = async ({ httpMetadata: unregisteredMediaFile.httpMetadata, }); - let cursor: string | undefined = undefined; + let cursor: string | undefined; while (true) { const { objects, ...rest } = await bucket.list({ @@ -96,8 +96,7 @@ export const findAndPromoteUnregisteredMedia = async ({ await bucket.delete(fileKeys); if (rest.truncated) { cursor = rest.cursor; - } - else { + } else { break; } } diff --git a/src/utils/owner.ts b/src/utils/owner.ts index 92aacb5..21fc946 100644 --- a/src/utils/owner.ts +++ b/src/utils/owner.ts @@ -1,4 +1,4 @@ -import { EnsPublicClient } from "./chains"; +import type { EnsPublicClient } from "./chains"; export const getOwnerAndAvailable = async ({ client, diff --git a/src/utils/subname.ts b/src/utils/subname.ts index 7d7db4b..ff5acbc 100644 --- a/src/utils/subname.ts +++ b/src/utils/subname.ts @@ -1,5 +1,6 @@ -import { Address } from "viem"; -import { EnsPublicClient } from "./chains"; +import type { Address } from "viem"; + +import type { EnsPublicClient } from "./chains"; export const isSubname = (name: string) => { return name.split(".").length > 2; @@ -14,6 +15,8 @@ export const isParentOwner = async ({ client: EnsPublicClient; verifiedAddress: Address; }) => { - const parentOwner = await client.getOwner({ name: name.split(".").slice(1).join(".") }); + const parentOwner = await client.getOwner({ + name: name.split(".").slice(1).join("."), + }); return parentOwner?.owner?.toLowerCase() === verifiedAddress.toLowerCase(); }; diff --git a/test/integration/routes/avatar.test.ts b/test/integration/routes/avatar.test.ts index 247127c..8762517 100644 --- a/test/integration/routes/avatar.test.ts +++ b/test/integration/routes/avatar.test.ts @@ -1,19 +1,25 @@ -import { describe, expect, test, vi, beforeEach, assert } from "vitest"; -import * as media from "@/utils/media"; -import * as eth from "@/utils/eth"; -import * as owner from "@/utils/owner"; -import * as data from "@/utils/data"; -import { ModuleMock } from "@test/setup/meta"; import { env } from "cloudflare:test"; import { normalize } from "viem/ens"; -import app from "@/index"; -import { createTestUploadData, TEST_ACCOUNT } from "@test/setup/helpers"; import { sha256 } from "viem/utils"; +import { assert, beforeEach, describe, expect, test, vi } from "vitest"; + +import { createTestUploadData, TEST_ACCOUNT } from "@test/setup/helpers"; +import type { ModuleMock } from "@test/setup/meta"; + +import app from "@/index"; +import * as data from "@/utils/data"; +import * as eth from "@/utils/eth"; +import * as media from "@/utils/media"; +import * as owner from "@/utils/owner"; // Mocks -vi.mock("@/utils/owner", () => ({ - getOwnerAndAvailable: vi.fn(), -}) satisfies ModuleMock); +vi.mock( + "@/utils/owner", + () => + ({ + getOwnerAndAvailable: vi.fn(), + }) satisfies ModuleMock, +); // Test constants const MOCK_NAME = "test.eth"; @@ -35,15 +41,22 @@ describe("Avatar Routes", () => { put: vi.spyOn(env.AVATAR_BUCKET, "put"), }; - const findAndPromoteUnregisteredMediaSpy = vi.spyOn(media, "findAndPromoteUnregisteredMedia"); + const findAndPromoteUnregisteredMediaSpy = vi.spyOn( + media, + "findAndPromoteUnregisteredMedia", + ); describe("GET /:name", () => { test("returns 200 with the avatar image when the avatar exists in registered storage", async () => { // Mock registered avatar exists const imageContent = new Uint8Array([1, 2, 3, 4, 5]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), imageContent, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + imageContent, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Make the request const res = await app.request(`/${MOCK_NAME}`, {}, env); @@ -51,7 +64,9 @@ describe("Avatar Routes", () => { // Verify result expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("image/jpeg"); - expect(res.headers.get("Content-Length")).toBe(imageContent.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + imageContent.length.toString(), + ); // Verify correct key was used expect(bucketSpy.get).toHaveBeenCalledWith( @@ -67,9 +82,17 @@ describe("Avatar Routes", () => { // Mock unregistered avatar exists and is promoted const imageBuffer = new Uint8Array([10, 20, 30, 40]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.unregistered("mainnet", MOCK_NAME, TEST_ACCOUNT.address), imageBuffer, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.unregistered( + "mainnet", + MOCK_NAME, + TEST_ACCOUNT.address, + ), + imageBuffer, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); vi.mocked(owner.getOwnerAndAvailable).mockResolvedValue({ owner: TEST_ACCOUNT.address, @@ -82,7 +105,9 @@ describe("Avatar Routes", () => { // Verify result expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("image/jpeg"); - expect(res.headers.get("Content-Length")).toBe(imageBuffer.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + imageBuffer.length.toString(), + ); expect(bucketSpy.get).toHaveBeenCalledWith( media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), @@ -97,7 +122,9 @@ describe("Avatar Routes", () => { mediaType: "avatar", }); - const putResult = await env.AVATAR_BUCKET.get(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME)); + const putResult = await env.AVATAR_BUCKET.get( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + ); assert(putResult); expect(putResult.httpMetadata?.contentType).toBe("image/jpeg"); @@ -143,9 +170,13 @@ describe("Avatar Routes", () => { test("returns 200 when using HEAD method with correct headers but no body", async () => { // Mock registered avatar exists const imageContent = new Uint8Array([1, 2, 3, 4, 5]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), imageContent, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + imageContent, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Make the HEAD request // This tests the HTTP HEAD verb functionality which should return metadata only, not the actual image @@ -155,7 +186,9 @@ describe("Avatar Routes", () => { // Verify result expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("image/jpeg"); - expect(res.headers.get("Content-Length")).toBe(imageContent.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + imageContent.length.toString(), + ); // Verify body is empty for HEAD request const buffer = await res.arrayBuffer(); @@ -165,9 +198,13 @@ describe("Avatar Routes", () => { test("updates an existing image with a new one", async () => { // Step 1: Put the initial image into storage const initialImage = new Uint8Array([1, 2, 3]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), initialImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), + initialImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Get the initial image let res = await app.request(`/goerli/${MOCK_NAME}`, {}, env); @@ -176,14 +213,20 @@ describe("Avatar Routes", () => { // Step 2: Put a new image with different content const updatedImage = new Uint8Array([4, 5, 6, 7, 8]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), updatedImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), + updatedImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Verify updated image is returned res = await app.request(`/goerli/${MOCK_NAME}`, {}, env); expect(res.status).toBe(200); - expect(res.headers.get("Content-Length")).toBe(updatedImage.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + updatedImage.length.toString(), + ); expect(new Uint8Array(await res.arrayBuffer())).toEqual(updatedImage); }); @@ -196,17 +239,25 @@ describe("Avatar Routes", () => { // Mock registered avatar exists with wrong content type const imageContent = new Uint8Array([1, 2, 3, 4, 5]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), imageContent, { - httpMetadata: { contentType: "text/html" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + imageContent, + { + httpMetadata: { contentType: "text/html" }, + }, + ); // Make the request // NOTE: Using HEAD request here is necessary because R2 objects must be fully consumed in tests, // but our handler stops reading the response when content-type doesn't match "image/jpeg". // This approach prevents test errors from "unconsumed response" while still testing the behavior. - const res = await app.request(`/${MOCK_NAME}`, { - method: "HEAD", - }, env); + const res = await app.request( + `/${MOCK_NAME}`, + { + method: "HEAD", + }, + env, + ); // Verify result - should be 404 since it's not a jpeg expect(res.status).toBe(404); @@ -217,7 +268,10 @@ describe("Avatar Routes", () => { // This test is important to verify the service properly cleans up outdated files // when a name transitions from unregistered to registered state const imageBuffer = new Uint8Array([10, 20, 30, 40]); - const uploaders = Array.from({ length: 5 }, (_, i) => `0x${(i + 1).toString().padStart(40, "0")}` as const); + const uploaders = Array.from( + { length: 5 }, + (_, i) => `0x${(i + 1).toString().padStart(40, "0")}` as const, + ); // Create unregistered files for all uploaders for (const uploader of uploaders) { @@ -259,13 +313,21 @@ describe("Avatar Routes", () => { const mainnetImage = new Uint8Array([1, 2, 3]); const goerliImage = new Uint8Array([4, 5, 6]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), mainnetImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + mainnetImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), goerliImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), + goerliImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Test mainnet network let res = await app.request(`/mainnet/${MOCK_NAME}`, {}, env); @@ -288,24 +350,38 @@ describe("Avatar Routes", () => { // Helper function to perform avatar uploads with proper signing // This abstracts the complexity of creating valid upload requests with signatures // and returns the response along with the test data for verification - const uploadAvatar = async (name: string, dataURL: string, network: string, expiry?: string) => { + const uploadAvatar = async ( + name: string, + dataURL: string, + network: string, + expiry?: string, + ) => { const imageBuffer = data.dataURLToBytes(dataURL).bytes; const imageHash = sha256(imageBuffer); - const testData = await createTestUploadData("avatar", name, imageHash, expiry); + const testData = await createTestUploadData( + "avatar", + name, + imageHash, + expiry, + ); - const res = await app.request(`/${network}/${name}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", + const res = await app.request( + `/${network}/${name}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + expiry: testData.expiry, + dataURL: dataURL, + sig: testData.sig, + unverifiedAddress: testData.address, + }), }, - body: JSON.stringify({ - expiry: testData.expiry, - dataURL: dataURL, - sig: testData.sig, - unverifiedAddress: testData.address, - }), - }, env); + env, + ); return { res, @@ -323,7 +399,11 @@ describe("Avatar Routes", () => { }); const dataURL = "data:image/jpeg;base64,test123123"; - const { res, imageBuffer } = await uploadAvatar(NORMALIZED_NAME, dataURL, "mainnet"); + const { res, imageBuffer } = await uploadAvatar( + NORMALIZED_NAME, + dataURL, + "mainnet", + ); expect(res.status).toBe(200); expect(await res.json()).toMatchInlineSnapshot(` @@ -348,7 +428,11 @@ describe("Avatar Routes", () => { }); const dataURL = "data:image/jpeg;base64,test123123"; - const { res, imageBuffer } = await uploadAvatar(NORMALIZED_NAME, dataURL, "mainnet"); + const { res, imageBuffer } = await uploadAvatar( + NORMALIZED_NAME, + dataURL, + "mainnet", + ); expect(res.status).toBe(200); expect(await res.json()).toMatchInlineSnapshot(` @@ -359,7 +443,11 @@ describe("Avatar Routes", () => { // Verify the file was uploaded to the unregistered path expect(bucketSpy.put).toHaveBeenCalledWith( - media.MEDIA_BUCKET_KEY.unregistered("mainnet", NORMALIZED_NAME, TEST_ACCOUNT.address), + media.MEDIA_BUCKET_KEY.unregistered( + "mainnet", + NORMALIZED_NAME, + TEST_ACCOUNT.address, + ), imageBuffer, { httpMetadata: { contentType: "image/jpeg" } }, ); @@ -404,7 +492,12 @@ describe("Avatar Routes", () => { // Using an expired timestamp (past date) for the expiry to trigger the expired signature check // This is crucial for preventing replay attacks with old signatures const dataURL = "data:image/jpeg;base64,test123123"; - const { res } = await uploadAvatar(NORMALIZED_NAME, dataURL, "mainnet", (Date.now() - 1000).toString()); + const { res } = await uploadAvatar( + NORMALIZED_NAME, + dataURL, + "mainnet", + (Date.now() - 1000).toString(), + ); expect(await res.text()).toBe("Signature expired"); expect(res.status).toBe(403); @@ -424,7 +517,9 @@ describe("Avatar Routes", () => { const { res } = await uploadAvatar(NORMALIZED_NAME, dataURL, "mainnet"); expect(res.status).toBe(403); - expect(await res.text()).toBe(`Address ${TEST_ACCOUNT.address} is not the owner of ${NORMALIZED_NAME}`); + expect(await res.text()).toBe( + `Address ${TEST_ACCOUNT.address} is not the owner of ${NORMALIZED_NAME}`, + ); // Verify no upload was attempted expect(bucketSpy.put).not.toHaveBeenCalled(); @@ -452,7 +547,7 @@ describe("Avatar Routes", () => { const oversizedImageBytes = new Uint8Array(MAX_IMAGE_SIZE + 1); const base64 = btoa( Array.from(oversizedImageBytes) - .map(byte => String.fromCharCode(byte)) + .map((byte) => String.fromCharCode(byte)) .join(""), ); const dataURL = `data:image/jpeg;base64,${base64}`; @@ -488,7 +583,7 @@ describe("Avatar Routes", () => { // Mock upload failure (key mismatch) bucketSpy.put.mockResolvedValue({ key: "wrong-key", - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const dataURL = "data:image/jpeg;base64,test123123"; @@ -508,11 +603,23 @@ describe("Avatar Routes", () => { // Mock successful upload const imageBuffer = new Uint8Array([10, 20, 30, 40]); - await env.AVATAR_BUCKET.put(media.MEDIA_BUCKET_KEY.unregistered(network, NORMALIZED_NAME, TEST_ACCOUNT.address), imageBuffer, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.AVATAR_BUCKET.put( + media.MEDIA_BUCKET_KEY.unregistered( + network, + NORMALIZED_NAME, + TEST_ACCOUNT.address, + ), + imageBuffer, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); - const dataURL = `data:image/jpeg;base64,${btoa(Array.from(imageBuffer).map(byte => String.fromCharCode(byte)).join(""))}`; + const dataURL = `data:image/jpeg;base64,${btoa( + Array.from(imageBuffer) + .map((byte) => String.fromCharCode(byte)) + .join(""), + )}`; const { res } = await uploadAvatar(NORMALIZED_NAME, dataURL, network); expect(res.status).toBe(200); @@ -530,21 +637,29 @@ describe("Avatar Routes", () => { const imageBuffer = data.dataURLToBytes(dataURL).bytes; const imageHash = sha256(imageBuffer); - const testData = await createTestUploadData("avatar", NORMALIZED_NAME, imageHash); + const testData = await createTestUploadData( + "avatar", + NORMALIZED_NAME, + imageHash, + ); // Create a request without unverifiedAddress - const res = await app.request(`/mainnet/${NORMALIZED_NAME}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", + const res = await app.request( + `/mainnet/${NORMALIZED_NAME}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + expiry: testData.expiry, + dataURL: dataURL, + sig: testData.sig, + // unverifiedAddress is intentionally omitted + }), }, - body: JSON.stringify({ - expiry: testData.expiry, - dataURL: dataURL, - sig: testData.sig, - // unverifiedAddress is intentionally omitted - }), - }, env); + env, + ); expect(res.status).toBe(400); // Valibot validator should reject with the proper error message diff --git a/test/integration/routes/header.test.ts b/test/integration/routes/header.test.ts index 42c8a8a..cbd9e31 100644 --- a/test/integration/routes/header.test.ts +++ b/test/integration/routes/header.test.ts @@ -1,19 +1,25 @@ -import { describe, expect, test, vi, beforeEach, assert } from "vitest"; -import * as media from "@/utils/media"; -import * as eth from "@/utils/eth"; -import * as owner from "@/utils/owner"; -import * as data from "@/utils/data"; -import { ModuleMock } from "@test/setup/meta"; import { env } from "cloudflare:test"; import { normalize } from "viem/ens"; -import app from "@/index"; -import { createTestUploadData, TEST_ACCOUNT } from "@test/setup/helpers"; import { sha256 } from "viem/utils"; +import { assert, beforeEach, describe, expect, test, vi } from "vitest"; + +import { createTestUploadData, TEST_ACCOUNT } from "@test/setup/helpers"; +import type { ModuleMock } from "@test/setup/meta"; + +import app from "@/index"; +import * as data from "@/utils/data"; +import * as eth from "@/utils/eth"; +import * as media from "@/utils/media"; +import * as owner from "@/utils/owner"; // Mocks -vi.mock("@/utils/owner", () => ({ - getOwnerAndAvailable: vi.fn(), -}) satisfies ModuleMock); +vi.mock( + "@/utils/owner", + () => + ({ + getOwnerAndAvailable: vi.fn(), + }) satisfies ModuleMock, +); // Test constants const MOCK_NAME = "test.eth"; @@ -35,15 +41,22 @@ describe("Header Routes", () => { put: vi.spyOn(env.HEADER_BUCKET, "put"), }; - const findAndPromoteUnregisteredMediaSpy = vi.spyOn(media, "findAndPromoteUnregisteredMedia"); + const findAndPromoteUnregisteredMediaSpy = vi.spyOn( + media, + "findAndPromoteUnregisteredMedia", + ); describe("GET /:name/h", () => { test("returns 200 with the header image when the header exists in registered storage", async () => { // Mock registered header exists const imageContent = new Uint8Array([1, 2, 3, 4, 5]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), imageContent, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + imageContent, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Make the request const res = await app.request(`/${MOCK_NAME}/h`, {}, env); @@ -51,7 +64,9 @@ describe("Header Routes", () => { // Verify result expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("image/jpeg"); - expect(res.headers.get("Content-Length")).toBe(imageContent.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + imageContent.length.toString(), + ); // Verify correct key was used expect(bucketSpy.get).toHaveBeenCalledWith( @@ -67,9 +82,17 @@ describe("Header Routes", () => { // Mock unregistered header exists and is promoted const imageBuffer = new Uint8Array([10, 20, 30, 40]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.unregistered("mainnet", MOCK_NAME, TEST_ACCOUNT.address), imageBuffer, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.unregistered( + "mainnet", + MOCK_NAME, + TEST_ACCOUNT.address, + ), + imageBuffer, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); vi.mocked(owner.getOwnerAndAvailable).mockResolvedValue({ owner: TEST_ACCOUNT.address, @@ -82,7 +105,9 @@ describe("Header Routes", () => { // Verify result expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("image/jpeg"); - expect(res.headers.get("Content-Length")).toBe(imageBuffer.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + imageBuffer.length.toString(), + ); expect(bucketSpy.get).toHaveBeenCalledWith( media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), @@ -97,7 +122,9 @@ describe("Header Routes", () => { mediaType: "header", }); - const putResult = await env.HEADER_BUCKET.get(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME)); + const putResult = await env.HEADER_BUCKET.get( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + ); assert(putResult); expect(putResult.httpMetadata?.contentType).toBe("image/jpeg"); @@ -139,9 +166,13 @@ describe("Header Routes", () => { test("returns 200 when using HEAD method with correct headers but no body", async () => { // Mock registered header exists const imageContent = new Uint8Array([1, 2, 3, 4, 5]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), imageContent, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + imageContent, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Make the HEAD request // This tests the HTTP HEAD verb functionality which should return metadata only, not the actual image @@ -151,7 +182,9 @@ describe("Header Routes", () => { // Verify result expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("image/jpeg"); - expect(res.headers.get("Content-Length")).toBe(imageContent.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + imageContent.length.toString(), + ); // Verify body is empty for HEAD request const buffer = await res.arrayBuffer(); @@ -161,9 +194,13 @@ describe("Header Routes", () => { test("updates an existing image with a new one", async () => { // Step 1: Put the initial image into storage const initialImage = new Uint8Array([1, 2, 3]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), initialImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), + initialImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Get the initial image let res = await app.request(`/goerli/${MOCK_NAME}/h`, {}, env); @@ -172,14 +209,20 @@ describe("Header Routes", () => { // Step 2: Put a new image with different content const updatedImage = new Uint8Array([4, 5, 6, 7, 8]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), updatedImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), + updatedImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Verify updated image is returned res = await app.request(`/goerli/${MOCK_NAME}/h`, {}, env); expect(res.status).toBe(200); - expect(res.headers.get("Content-Length")).toBe(updatedImage.length.toString()); + expect(res.headers.get("Content-Length")).toBe( + updatedImage.length.toString(), + ); expect(new Uint8Array(await res.arrayBuffer())).toEqual(updatedImage); }); @@ -192,17 +235,25 @@ describe("Header Routes", () => { // Mock registered header exists with wrong content type const imageContent = new Uint8Array([1, 2, 3, 4, 5]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), imageContent, { - httpMetadata: { contentType: "text/html" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + imageContent, + { + httpMetadata: { contentType: "text/html" }, + }, + ); // Make the request // NOTE: Using HEAD request here is necessary because R2 objects must be fully consumed in tests, // but our handler stops reading the response when content-type doesn't match "image/jpeg". // This approach prevents test errors from "unconsumed response" while still testing the behavior. - const res = await app.request(`/${MOCK_NAME}/h`, { - method: "HEAD", - }, env); + const res = await app.request( + `/${MOCK_NAME}/h`, + { + method: "HEAD", + }, + env, + ); // Verify result - should be 404 since it's not a jpeg expect(res.status).toBe(404); @@ -213,7 +264,10 @@ describe("Header Routes", () => { // This test is important to verify the service properly cleans up outdated files // when a name transitions from unregistered to registered state const imageBuffer = new Uint8Array([10, 20, 30, 40]); - const uploaders = Array.from({ length: 5 }, (_, i) => `0x${(i + 1).toString().padStart(40, "0")}` as const); + const uploaders = Array.from( + { length: 5 }, + (_, i) => `0x${(i + 1).toString().padStart(40, "0")}` as const, + ); // Create unregistered files for all uploaders for (const uploader of uploaders) { @@ -255,13 +309,21 @@ describe("Header Routes", () => { const mainnetImage = new Uint8Array([1, 2, 3]); const goerliImage = new Uint8Array([4, 5, 6]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), mainnetImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("mainnet", MOCK_NAME), + mainnetImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), goerliImage, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.registered("goerli", MOCK_NAME), + goerliImage, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); // Test mainnet network let res = await app.request(`/mainnet/${MOCK_NAME}/h`, {}, env); @@ -284,24 +346,38 @@ describe("Header Routes", () => { // Helper function to perform header uploads with proper signing // This abstracts the complexity of creating valid upload requests with signatures // and returns the response along with the test data for verification - const uploadHeader = async (name: string, dataURL: string, network: string, expiry?: string) => { + const uploadHeader = async ( + name: string, + dataURL: string, + network: string, + expiry?: string, + ) => { const imageBuffer = data.dataURLToBytes(dataURL).bytes; const imageHash = sha256(imageBuffer); - const testData = await createTestUploadData("header", name, imageHash, expiry); + const testData = await createTestUploadData( + "header", + name, + imageHash, + expiry, + ); - const res = await app.request(`/${network}/${name}/h`, { - method: "PUT", - headers: { - "Content-Type": "application/json", + const res = await app.request( + `/${network}/${name}/h`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + expiry: testData.expiry, + dataURL: dataURL, + sig: testData.sig, + unverifiedAddress: testData.address, + }), }, - body: JSON.stringify({ - expiry: testData.expiry, - dataURL: dataURL, - sig: testData.sig, - unverifiedAddress: testData.address, - }), - }, env); + env, + ); return { res, @@ -319,7 +395,11 @@ describe("Header Routes", () => { }); const dataURL = "data:image/jpeg;base64,test123123"; - const { res, imageBuffer } = await uploadHeader(NORMALIZED_NAME, dataURL, "mainnet"); + const { res, imageBuffer } = await uploadHeader( + NORMALIZED_NAME, + dataURL, + "mainnet", + ); expect(res.status).toBe(200); expect(await res.json()).toMatchInlineSnapshot(` @@ -344,7 +424,11 @@ describe("Header Routes", () => { }); const dataURL = "data:image/jpeg;base64,test123123"; - const { res, imageBuffer } = await uploadHeader(NORMALIZED_NAME, dataURL, "mainnet"); + const { res, imageBuffer } = await uploadHeader( + NORMALIZED_NAME, + dataURL, + "mainnet", + ); expect(res.status).toBe(200); expect(await res.json()).toMatchInlineSnapshot(` @@ -355,7 +439,11 @@ describe("Header Routes", () => { // Verify the file was uploaded to the unregistered path expect(bucketSpy.put).toHaveBeenCalledWith( - media.MEDIA_BUCKET_KEY.unregistered("mainnet", NORMALIZED_NAME, TEST_ACCOUNT.address), + media.MEDIA_BUCKET_KEY.unregistered( + "mainnet", + NORMALIZED_NAME, + TEST_ACCOUNT.address, + ), imageBuffer, { httpMetadata: { contentType: "image/jpeg" } }, ); @@ -400,7 +488,12 @@ describe("Header Routes", () => { // Using an expired timestamp (past date) for the expiry to trigger the expired signature check // This is crucial for preventing replay attacks with old signatures const dataURL = "data:image/jpeg;base64,test123123"; - const { res } = await uploadHeader(NORMALIZED_NAME, dataURL, "mainnet", (Date.now() - 1000).toString()); + const { res } = await uploadHeader( + NORMALIZED_NAME, + dataURL, + "mainnet", + (Date.now() - 1000).toString(), + ); expect(await res.text()).toBe("Signature expired"); expect(res.status).toBe(403); @@ -420,7 +513,9 @@ describe("Header Routes", () => { const { res } = await uploadHeader(NORMALIZED_NAME, dataURL, "mainnet"); expect(res.status).toBe(403); - expect(await res.text()).toBe(`Address ${TEST_ACCOUNT.address} is not the owner of ${NORMALIZED_NAME}`); + expect(await res.text()).toBe( + `Address ${TEST_ACCOUNT.address} is not the owner of ${NORMALIZED_NAME}`, + ); // Verify no upload was attempted expect(bucketSpy.put).not.toHaveBeenCalled(); @@ -448,7 +543,7 @@ describe("Header Routes", () => { const oversizedImageBytes = new Uint8Array(MAX_IMAGE_SIZE + 1); const base64 = btoa( Array.from(oversizedImageBytes) - .map(byte => String.fromCharCode(byte)) + .map((byte) => String.fromCharCode(byte)) .join(""), ); const dataURL = `data:image/jpeg;base64,${base64}`; @@ -484,7 +579,7 @@ describe("Header Routes", () => { // Mock upload failure (key mismatch) bucketSpy.put.mockResolvedValue({ key: "wrong-key", - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const dataURL = "data:image/jpeg;base64,test123123"; @@ -504,11 +599,23 @@ describe("Header Routes", () => { // Mock successful upload const imageBuffer = new Uint8Array([10, 20, 30, 40]); - await env.HEADER_BUCKET.put(media.MEDIA_BUCKET_KEY.unregistered(network, NORMALIZED_NAME, TEST_ACCOUNT.address), imageBuffer, { - httpMetadata: { contentType: "image/jpeg" }, - }); + await env.HEADER_BUCKET.put( + media.MEDIA_BUCKET_KEY.unregistered( + network, + NORMALIZED_NAME, + TEST_ACCOUNT.address, + ), + imageBuffer, + { + httpMetadata: { contentType: "image/jpeg" }, + }, + ); - const dataURL = `data:image/jpeg;base64,${btoa(Array.from(imageBuffer).map(byte => String.fromCharCode(byte)).join(""))}`; + const dataURL = `data:image/jpeg;base64,${btoa( + Array.from(imageBuffer) + .map((byte) => String.fromCharCode(byte)) + .join(""), + )}`; const { res } = await uploadHeader(NORMALIZED_NAME, dataURL, network); expect(res.status).toBe(200); @@ -526,21 +633,29 @@ describe("Header Routes", () => { const imageBuffer = data.dataURLToBytes(dataURL).bytes; const imageHash = sha256(imageBuffer); - const testData = await createTestUploadData("header", NORMALIZED_NAME, imageHash); + const testData = await createTestUploadData( + "header", + NORMALIZED_NAME, + imageHash, + ); // Create a request without unverifiedAddress - const res = await app.request(`/mainnet/${NORMALIZED_NAME}/h`, { - method: "PUT", - headers: { - "Content-Type": "application/json", + const res = await app.request( + `/mainnet/${NORMALIZED_NAME}/h`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + expiry: testData.expiry, + dataURL: dataURL, + sig: testData.sig, + // unverifiedAddress is intentionally omitted + }), }, - body: JSON.stringify({ - expiry: testData.expiry, - dataURL: dataURL, - sig: testData.sig, - // unverifiedAddress is intentionally omitted - }), - }, env); + env, + ); expect(res.status).toBe(400); // Valibot validator should reject with the proper error message diff --git a/test/setup/helpers.ts b/test/setup/helpers.ts index ce4a042..08ba756 100644 --- a/test/setup/helpers.ts +++ b/test/setup/helpers.ts @@ -1,13 +1,14 @@ -import { EnsPublicClient } from "@/utils/chains"; -import { typedDataParameters } from "@/utils/eth"; -import { createEnsPublicClient, createEnsWalletClient } from "@ensdomains/ensjs"; -import { Address, Hex, http } from "viem"; - import { - mnemonicToAccount, -} from "viem/accounts"; + createEnsPublicClient, + createEnsWalletClient, +} from "@ensdomains/ensjs"; +import { type Address, type Hex, http } from "viem"; +import { mnemonicToAccount } from "viem/accounts"; import { mainnet } from "viem/chains"; +import type { EnsPublicClient } from "@/utils/chains"; +import { typedDataParameters } from "@/utils/eth"; + export const TEST_ACCOUNT = mnemonicToAccount( "test test test test test test test test test test test junk", );