diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml new file mode 100644 index 0000000..333d1c1 --- /dev/null +++ b/.github/workflows/test-dev.yml @@ -0,0 +1,29 @@ +name: Test Dev + +on: + pull_request: + branches: + - main + # Allows you to run this workflow manually from the Actions tab on GitHub. + workflow_dispatch: + +# Allow this job to clone the repo and create a page deployment +permissions: + contents: read + +jobs: + test-api: + name: Test API + runs-on: ubuntu-latest + steps: + - name: Checkout your repository using git + uses: actions/checkout@v6.0.2 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2.2.0 + + - name: Install dependencies + run: bun install + + - name: Run tests with coverage gate + run: bun run test diff --git a/.gitignore b/.gitignore index 4e2b288..1692880 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ dist-ssr # Cloudflare/Wrangler .wrangler -.env \ No newline at end of file +.env +coverage diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cfadbf7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Reporting a Vulnerability + +Please contact admin@2dtiler.com with information about the security vulnerability. + +We highly appreciate any vulnerability feedback! diff --git a/TODO.txt b/TODO.txt index 1ec19d2..9eadbc5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,3 @@ -CI/CD -Create API endpoint for fetching releases -https://lospec.com/palette-list/load?colorNumberFilterType=any&page=0&tag=&sortingType=newest \ No newline at end of file +Add unit tests and add to CI/CD +Update README.md +Push and deploy \ No newline at end of file diff --git a/bun.lock b/bun.lock index 50a835f..6dfa657 100644 --- a/bun.lock +++ b/bun.lock @@ -8,20 +8,59 @@ "hono": "^4.12.12", }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.14.3", "@cloudflare/workers-types": "^4.20260410.1", "@eslint/js": "^10.0.1", + "@vitest/coverage-istanbul": "^4.1.4", "eslint": "^10.2.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", + "vitest": "^4.1.4", "wrangler": "^4.81.1", }, }, }, "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="], + "@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.14.3", "", { "dependencies": { "cjs-module-lexer": "^1.2.3", "esbuild": "0.27.3", "miniflare": "4.20260409.0", "wrangler": "4.81.1", "zod": "^3.25.76" }, "peerDependencies": { "@vitest/runner": "^4.1.0", "@vitest/snapshot": "^4.1.0", "vitest": "^4.1.0" } }, "sha512-7J0K3f9iS2u6k2J/bY7/vJJcaLgEGXcfNrs2fSti6vc0l/L/I4XmYtvZ1JwmFa5xqiHG4tF0ktGSKUZbkqvzEw=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260409.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-h/bkaC0HJL63aqAGnV0oagqpBiTSstabODThkeMSbG8kctl0Jb4jlq1pNHJPmYGazFNtfyagrUZFb6HN22GX7w=="], "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260409.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HTAC+B9uSYcm+GjN3UYJjuun19GqYtK1bAFJ0KECXyfsgIDwH1MTzxbTxzJpZUbWLw8s0jcwCU06MWZj6cgnxQ=="], @@ -36,8 +75,12 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -164,11 +207,21 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="], "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], @@ -176,10 +229,50 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -206,18 +299,52 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@vitest/coverage-istanbul": ["@vitest/coverage-istanbul@4.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@istanbuljs/schema": "^0.1.3", "@jridgewell/gen-mapping": "^0.3.13", "@jridgewell/trace-mapping": "0.3.31", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "vitest": "4.1.4" } }, "sha512-Pyi4F8RnqU6hBGiIDhS/e8gVD4FRcUvZJ2AbFiIlmIxHlEIsKyCxGOqufCECobty/dXELcN8oIH4Gms3hVOCYA=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.4", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.4", "vitest": "4.1.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w=="], + + "@vitest/expect": ["@vitest/expect@4.1.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.4", "", { "dependencies": { "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.4", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A=="], + + "@vitest/runner": ["@vitest/runner@4.1.4", "", { "dependencies": { "@vitest/utils": "4.1.4", "pathe": "^2.0.3" } }, "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw=="], + + "@vitest/spy": ["@vitest/spy@4.1.4", "", {}, "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -228,10 +355,16 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="], + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="], @@ -248,8 +381,12 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -268,10 +405,16 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -282,28 +425,78 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "miniflare": ["miniflare@4.20260409.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.4", "workerd": "1.20260409.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-ayl6To4av0YuXsSivGgWLj+Ug8xZ0Qz3sGV8+Ok2LhNVl6m8m5ktEBM3LX9iT9MtLZRJwBlJrKcraNs/DlZQfA=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -318,13 +511,19 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], @@ -332,10 +531,24 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -350,10 +563,18 @@ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], + + "vitest": ["vitest@4.1.4", "", { "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", "@vitest/pretty-format": "4.1.4", "@vitest/runner": "4.1.4", "@vitest/snapshot": "4.1.4", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.4", "@vitest/browser-preview": "4.1.4", "@vitest/browser-webdriverio": "4.1.4", "@vitest/coverage-istanbul": "4.1.4", "@vitest/coverage-v8": "4.1.4", "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "workerd": ["workerd@1.20260409.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260409.1", "@cloudflare/workerd-darwin-arm64": "1.20260409.1", "@cloudflare/workerd-linux-64": "1.20260409.1", "@cloudflare/workerd-linux-arm64": "1.20260409.1", "@cloudflare/workerd-windows-64": "1.20260409.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kuWP20fAaqaLBqLbvUfY9nCF6c3C78L60G9lS6eVwBf+v8trVFIsAdLB/FtrnKm7vgVvpDzvFAfB80VIiVj95w=="], @@ -362,16 +583,36 @@ "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="], + + "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], } } diff --git a/package.json b/package.json index f6dc5c6..00a54d5 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,22 @@ "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", - "lint": "eslint src" + "lint": "eslint src", + "typecheck": "tsc --noEmit", + "test": "vitest run --coverage" }, "dependencies": { "hono": "^4.12.12" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.14.3", "@cloudflare/workers-types": "^4.20260410.1", "@eslint/js": "^10.0.1", + "@vitest/coverage-istanbul": "^4.1.4", "eslint": "^10.2.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", + "vitest": "^4.1.4", "wrangler": "^4.81.1" } } \ No newline at end of file diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..cc15cdd --- /dev/null +++ b/src/app.ts @@ -0,0 +1,15 @@ +import { Hono } from "hono"; + +import { registerHealthRoutes } from "./app/routes/health"; +import { registerLospecPaletteRoutes } from "./app/routes/lospec-palettes"; +import { corsMiddleware } from "./app/middleware/cors"; +import type { AppBindings } from "./config/types"; + +const app = new Hono(); + +app.use("*", corsMiddleware); + +registerHealthRoutes(app); +registerLospecPaletteRoutes(app); + +export default app; diff --git a/src/app/controllers/health.ts b/src/app/controllers/health.ts new file mode 100644 index 0000000..fc5bb4f --- /dev/null +++ b/src/app/controllers/health.ts @@ -0,0 +1,7 @@ +import type { Context } from "hono"; + +import type { AppBindings } from "../../config/types"; + +export function getHealth(c: Context): Response { + return c.json({}); +} diff --git a/src/app/controllers/lospec-palettes.ts b/src/app/controllers/lospec-palettes.ts new file mode 100644 index 0000000..7d461ee --- /dev/null +++ b/src/app/controllers/lospec-palettes.ts @@ -0,0 +1,67 @@ +import type { Context } from "hono"; + +import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants"; +import type { AppBindings } from "../../config/types"; +import { + mapRowToResponse, + type ListLospecPalettesOptions, +} from "../models/lospec-palette"; +import { listPalettes } from "../services/lospec-palettes-repository"; + +function parsePage(value: string | undefined): number | null { + if (value === undefined) { + return 0; + } + + if (!/^\d+$/.test(value)) { + return null; + } + + return Number.parseInt(value, 10); +} + +function normalizeQueryValue(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function parseListLospecPalettesOptions( + query: Record, +): ListLospecPalettesOptions | null { + const page = parsePage(query.page); + if (page === null) { + return null; + } + + return { + page, + search: normalizeQueryValue(query.search), + tag: normalizeQueryValue(query.tags), + }; +} + +export async function getLospecPalettes( + c: Context, +): Promise { + const options = parseListLospecPalettesOptions(c.req.query()); + if (!options) { + return c.json( + { + error: `Invalid page parameter. Expected a non-negative integer with ${LOSPEC_PALETTES_PAGE_SIZE} results per page.`, + }, + 400, + ); + } + + const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown"; + const { success } = await c.env.RATE_LIMITER.limit({ key: ip }); + if (!success) { + return c.json( + { error: "Rate limit exceeded. Try again in a minute." }, + 429, + ); + } + + const palettes = await listPalettes(c.env.DB, options); + return c.json(palettes.map(mapRowToResponse)); +} diff --git a/src/app/middleware/cors.ts b/src/app/middleware/cors.ts new file mode 100644 index 0000000..c9f11ac --- /dev/null +++ b/src/app/middleware/cors.ts @@ -0,0 +1,45 @@ +import type { MiddlewareHandler } from "hono"; + +import { + ALLOWED_ORIGINS, + INTERNAL_API_KEY_HEADER, +} from "../../config/constants"; +import type { AppBindings } from "../../config/types"; + +function applyCorsHeaders(headers: Headers, origin: string): void { + headers.set("Access-Control-Allow-Origin", origin); + headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); + headers.set("Access-Control-Allow-Headers", "Content-Type"); + headers.set("Vary", "Origin"); +} + +export const corsMiddleware: MiddlewareHandler = async ( + c, + next, +) => { + const origin = c.req.header("Origin"); + const internalApiKey = c.req.header(INTERNAL_API_KEY_HEADER); + + if (c.env.INTERNAL_API_KEY && internalApiKey === c.env.INTERNAL_API_KEY) { + await next(); + return; + } + + if (!origin) { + await next(); + return; + } + + if (!ALLOWED_ORIGINS.has(origin)) { + return c.json({ error: "Forbidden origin" }, 403); + } + + if (c.req.method === "OPTIONS") { + const headers = new Headers(); + applyCorsHeaders(headers, origin); + return new Response(null, { status: 204, headers }); + } + + await next(); + applyCorsHeaders(c.res.headers, origin); +}; diff --git a/src/app/models/lospec-palette.ts b/src/app/models/lospec-palette.ts new file mode 100644 index 0000000..1899968 --- /dev/null +++ b/src/app/models/lospec-palette.ts @@ -0,0 +1,157 @@ +import { LOSPEC_CDN_BASE_URL } from "../../config/constants"; + +export interface LospecPaletteApiItem { + _id: string; + title?: string; + slug?: string; + description?: string; + tags?: unknown; + user?: unknown; + colors?: unknown; + examples?: unknown; + publishedAt?: string; +} + +export interface LospecPaletteRow { + id: string; + title: string | null; + slug: string | null; + description: string | null; + tags: string | null; + user: string | null; + colors: string | null; + examples: string | null; + published_at: string | null; +} + +export interface LospecPaletteExample { + image: string; + description: string | null; +} + +export interface LospecPaletteResponse { + id: string; + title: string | null; + slug: string | null; + description: string | null; + tags: unknown | null; + user: string | null; + colors: unknown | null; + examples: LospecPaletteExample[] | null; + published_at: string | null; +} + +export interface ListLospecPalettesOptions { + page: number; + search?: string; + tag?: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +export function isLospecPaletteApiItem( + value: unknown, +): value is LospecPaletteApiItem { + return isRecord(value) && typeof value._id === "string"; +} + +function asNullableString(value: unknown): string | null { + return typeof value === "string" ? value : null; +} + +function normalizePaletteUser(value: unknown): string | null { + if (typeof value === "string") { + return value; + } + + if (!isRecord(value)) { + return null; + } + + return asNullableString(value.name); +} + +function normalizePaletteExampleImage(value: string): string { + return new URL(value, LOSPEC_CDN_BASE_URL).toString(); +} + +function normalizePaletteExamples( + value: unknown, +): LospecPaletteExample[] | null { + if (!Array.isArray(value)) { + return null; + } + + return value.flatMap((example) => { + if (!isRecord(example)) { + return []; + } + + const image = asNullableString(example.image); + if (!image) { + return []; + } + + return [ + { + image: normalizePaletteExampleImage(image), + description: asNullableString(example.description), + }, + ]; + }); +} + +function serializeJsonField(value: unknown): string | null { + if (value === undefined || value === null) { + return null; + } + + return JSON.stringify(value); +} + +function deserializeJsonField(value: string | null): unknown | null { + if (!value) { + return null; + } + + try { + return JSON.parse(value) as unknown; + } catch { + return value; + } +} + +export function mapPaletteToRow( + palette: LospecPaletteApiItem, +): LospecPaletteRow { + return { + id: palette._id, + title: asNullableString(palette.title), + slug: asNullableString(palette.slug), + description: asNullableString(palette.description), + tags: serializeJsonField(palette.tags), + user: normalizePaletteUser(palette.user), + colors: serializeJsonField(palette.colors), + examples: serializeJsonField(normalizePaletteExamples(palette.examples)), + published_at: asNullableString(palette.publishedAt), + }; +} + +export function mapRowToResponse(row: LospecPaletteRow): LospecPaletteResponse { + const user = normalizePaletteUser(deserializeJsonField(row.user)); + const examples = normalizePaletteExamples(deserializeJsonField(row.examples)); + + return { + id: row.id, + title: row.title, + slug: row.slug, + description: row.description, + tags: deserializeJsonField(row.tags), + user, + colors: deserializeJsonField(row.colors), + examples, + published_at: row.published_at, + }; +} diff --git a/src/app/routes/health.ts b/src/app/routes/health.ts new file mode 100644 index 0000000..912bb45 --- /dev/null +++ b/src/app/routes/health.ts @@ -0,0 +1,8 @@ +import type { Hono } from "hono"; + +import type { AppBindings } from "../../config/types"; +import { getHealth } from "../controllers/health"; + +export function registerHealthRoutes(app: Hono): void { + app.get("/", getHealth); +} diff --git a/src/app/routes/lospec-palettes.ts b/src/app/routes/lospec-palettes.ts new file mode 100644 index 0000000..26ac878 --- /dev/null +++ b/src/app/routes/lospec-palettes.ts @@ -0,0 +1,8 @@ +import type { Hono } from "hono"; + +import type { AppBindings } from "../../config/types"; +import { getLospecPalettes } from "../controllers/lospec-palettes"; + +export function registerLospecPaletteRoutes(app: Hono): void { + app.get("/lospec_palettes", getLospecPalettes); +} diff --git a/src/app/services/lospec-api.ts b/src/app/services/lospec-api.ts new file mode 100644 index 0000000..71c86b1 --- /dev/null +++ b/src/app/services/lospec-api.ts @@ -0,0 +1,28 @@ +import { LOSPEC_PALETTE_LIST_URL } from "../../config/constants"; +import { + isLospecPaletteApiItem, + type LospecPaletteApiItem, +} from "../models/lospec-palette"; + +export async function fetchLospecPalettePage( + page: number, + signal: AbortSignal, +): Promise { + const url = new URL(LOSPEC_PALETTE_LIST_URL); + url.searchParams.set("colorNumberFilterType", "any"); + url.searchParams.set("page", String(page)); + url.searchParams.set("tag", ""); + url.searchParams.set("sortingType", "newest"); + + const response = await fetch(url, { signal }); + if (!response.ok) { + throw new Error(`Failed to fetch page ${page}: HTTP ${response.status}`); + } + + const payload = (await response.json()) as { palettes?: unknown }; + if (!Array.isArray(payload.palettes)) { + return []; + } + + return payload.palettes.filter(isLospecPaletteApiItem); +} diff --git a/src/app/services/lospec-palettes-repository.ts b/src/app/services/lospec-palettes-repository.ts new file mode 100644 index 0000000..e8f2b4e --- /dev/null +++ b/src/app/services/lospec-palettes-repository.ts @@ -0,0 +1,114 @@ +import { + mapPaletteToRow, + type ListLospecPalettesOptions, + type LospecPaletteApiItem, + type LospecPaletteRow, +} from "../models/lospec-palette"; +import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants"; + +export async function getExistingPaletteIds( + db: D1Database, + ids: string[], +): Promise> { + if (ids.length === 0) { + return new Set(); + } + + const placeholders = ids.map(() => "?").join(", "); + const statement = db.prepare( + `SELECT id FROM lospec_palettes WHERE id IN (${placeholders})`, + ); + const result = await statement.bind(...ids).all<{ id: string }>(); + + return new Set(result.results.map((row) => row.id)); +} + +export async function insertPalettes( + db: D1Database, + palettes: LospecPaletteApiItem[], +): Promise { + if (palettes.length === 0) { + return 0; + } + + const statements = palettes.map((palette) => { + const row = mapPaletteToRow(palette); + + return db + .prepare( + `INSERT OR IGNORE INTO lospec_palettes ( + id, + title, + slug, + description, + tags, + user, + colors, + examples, + published_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + row.id, + row.title, + row.slug, + row.description, + row.tags, + row.user, + row.colors, + row.examples, + row.published_at, + ); + }); + + await db.batch(statements); + return palettes.length; +} + +export async function listPalettes( + db: D1Database, + options: ListLospecPalettesOptions, +): Promise { + const whereClauses: string[] = []; + const bindings: Array = []; + + if (options.search) { + whereClauses.push("LOWER(COALESCE(title, '')) LIKE ?"); + bindings.push(`%${options.search.toLowerCase()}%`); + } + + if (options.tag) { + whereClauses.push(`EXISTS ( + SELECT 1 + FROM json_each(lospec_palettes.tags) + WHERE LOWER(CAST(json_each.value AS TEXT)) = ? + )`); + bindings.push(options.tag.toLowerCase()); + } + + const whereSql = + whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : ""; + const offset = options.page * LOSPEC_PALETTES_PAGE_SIZE; + + const result = await db + .prepare( + `SELECT + id, + title, + slug, + description, + tags, + user, + colors, + examples, + published_at + FROM lospec_palettes + ${whereSql} + ORDER BY published_at DESC, id DESC + LIMIT ? OFFSET ?`, + ) + .bind(...bindings, LOSPEC_PALETTES_PAGE_SIZE, offset) + .all(); + + return result.results; +} diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..7563945 --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,13 @@ +export const ALLOWED_ORIGINS = new Set([ + "https://2dtiler.com", + "https://app.2dtiler.com", + "http://localhost:4321", + "http://127.0.0.1:4321", + "http://localhost:8787", + "http://127.0.0.1:8787", +]); + +export const INTERNAL_API_KEY_HEADER = "X-Internal-Api-Key"; +export const LOSPEC_PALETTE_LIST_URL = "https://lospec.com/palette-list/load"; +export const LOSPEC_CDN_BASE_URL = "https://cdn.lospec.com/"; +export const LOSPEC_PALETTES_PAGE_SIZE = 100; diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 0000000..389d5e0 --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,11 @@ +export interface RateLimiter { + limit(options: { key: string }): Promise<{ success: boolean }>; +} + +export interface Env { + DB: D1Database; + RATE_LIMITER: RateLimiter; + INTERNAL_API_KEY: string; +} + +export type AppBindings = { Bindings: Env }; diff --git a/src/index.ts b/src/index.ts index 46af6cc..d8372a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,180 +1,6 @@ -import { Hono } from "hono"; - -interface RateLimiter { - limit(options: { key: string }): Promise<{ success: boolean }>; -} - -interface Env { - LOSPEC_PALETTES: KVNamespace; - RATE_LIMITER: RateLimiter; -} - -interface Palette { - name: string; - author: string; - colors: string[]; -} - -const app = new Hono<{ Bindings: Env }>(); - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -/** Returns all keys from a KV namespace, transparently handling pagination. */ -async function listAllKeys(kv: KVNamespace): Promise { - const keys: string[] = []; - let cursor: string | undefined; - - do { - const result = await kv.list({ cursor }); - for (const key of result.keys) { - keys.push(key.name); - } - cursor = result.list_complete ? undefined : result.cursor; - } while (cursor !== undefined); - - return keys; -} - -const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - -// ─── HTTP Routes ───────────────────────────────────────────────────────────── - -/** - * GET / - * Returns a blank API route to verify the worker is running. - */ -app.get("/", async (c) => { - return c.json({}); -}); - -/** - * GET /lospec_palettes - * Returns all fully-populated palette entries as a JSON array. - * Entries that are still pending fetch (stored as "{}") are omitted. - */ -app.get("/lospec_palettes", async (c) => { - const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown"; - const { success } = await c.env.RATE_LIMITER.limit({ key: ip }); - if (!success) { - return c.json({ error: "Rate limit exceeded. Try again in an hour." }, 429); - } - - const kv = c.env.LOSPEC_PALETTES; - const palettes: Palette[] = []; - const keys = await listAllKeys(kv); - - for (const key of keys) { - const value = await kv.get(key); - if (!value || value === "{}") continue; - - try { - const palette = JSON.parse(value) as Palette; - if (palette.name && Array.isArray(palette.colors)) { - palettes.push(palette); - } - } catch { - // skip malformed KV entries - } - } - - return c.json(palettes); -}); - -// ─── Scheduled Job ─────────────────────────────────────────────────────────── - -/** - * Runs daily at 12:00 UTC (see wrangler.toml). - * - * Phase 1 – Discover: fetch the Lospec sitemap and add any unknown palette - * slugs to KV as empty stubs ("{}"). - * - * Phase 2 – Populate: for every slug that still holds an empty stub, fetch - * the palette JSON from the Lospec API and persist the real data. - * A 3-second delay is inserted between requests to avoid flooding the server. - */ -async function syncPalettes(env: Env, signal: AbortSignal): Promise { - // ── Phase 1: discover new slugs ────────────────────────────────────────── - - const sitemapRes = await fetch("https://lospec.com/sitemap.xml", { signal }); - if (!sitemapRes.ok) { - console.error(`Failed to fetch sitemap: ${sitemapRes.status}`); - return; - } - - const sitemapText = await sitemapRes.text(); - const slugs = new Set(); - const locRegex = /https:\/\/lospec\.com\/palette-list\/([^/g; - let match: RegExpExecArray | null; - - while ((match = locRegex.exec(sitemapText)) !== null) { - slugs.add(match[1]); - } - - console.log(`Sitemap: found ${slugs.size} palette URLs`); - - const existingKeys = new Set(await listAllKeys(env.LOSPEC_PALETTES)); - - let added = 0; - for (const slug of slugs) { - if (!existingKeys.has(slug)) { - await env.LOSPEC_PALETTES.put(slug, "{}"); - added++; - } - } - - console.log(`Seeded ${added} new palette stubs into KV`); - - // ── Phase 2: populate empty stubs ──────────────────────────────────────── - - // New slugs we just seeded are guaranteed to be "{}". - const newEmptySlugs = [...slugs].filter((s) => !existingKeys.has(s)); - - // Check pre-existing keys in parallel to find any leftover empty stubs from - // a previous partial run — avoids a redundant listAllKeys call and - // serialised per-key gets. - const existingEmptyKeys = ( - await Promise.all( - [...existingKeys].map(async (key) => { - const value = await env.LOSPEC_PALETTES.get(key); - return value === "{}" ? key : null; - }), - ) - ).filter((k): k is string => k !== null); - - const emptyKeys = [...existingEmptyKeys, ...newEmptySlugs]; - - console.log(`Fetching data for ${emptyKeys.length} empty palette entries`); - - for (let i = 0; i < emptyKeys.length; i++) { - if (signal.aborted) { - console.warn("Palette sync timed out; stopping early"); - break; - } - - if (i > 0) await sleep(3000); - - const slug = emptyKeys[i]; - try { - const res = await fetch(`https://lospec.com/palette-list/${slug}.json`, { - signal, - }); - if (res.ok) { - const data = await res.text(); - await env.LOSPEC_PALETTES.put(slug, data); - console.log(`✓ ${slug}`); - } else { - console.warn(`✗ ${slug} (HTTP ${res.status})`); - } - } catch (err) { - console.error(`✗ ${slug} (error):`, err); - } - } - - console.log("Palette sync complete"); -} - -// ─── Worker Export ─────────────────────────────────────────────────────────── +import app from "./app"; +import { syncPalettes } from "./jobs/sync-palettes"; +import type { Env } from "./config/types"; export default { fetch: app.fetch, @@ -184,7 +10,7 @@ export default { env: Env, ctx: ExecutionContext, ): Promise { - const signal = AbortSignal.timeout(15 * 60 * 1000); // 15 minutes + const signal = AbortSignal.timeout(15 * 60 * 1000); ctx.waitUntil(syncPalettes(env, signal)); }, }; diff --git a/src/jobs/sync-palettes.ts b/src/jobs/sync-palettes.ts new file mode 100644 index 0000000..e068b69 --- /dev/null +++ b/src/jobs/sync-palettes.ts @@ -0,0 +1,62 @@ +import type { Env } from "../config/types"; +import { fetchLospecPalettePage } from "../app/services/lospec-api"; +import { + getExistingPaletteIds, + insertPalettes, +} from "../app/services/lospec-palettes-repository"; + +export async function syncPalettes( + env: Env, + signal: AbortSignal, +): Promise { + let page = 0; + let pagesProcessed = 0; + let totalInserted = 0; + + while (!signal.aborted) { + try { + const palettes = await fetchLospecPalettePage(page, signal); + if (palettes.length === 0) { + console.log(`Page ${page}: no palettes returned; stopping pagination`); + break; + } + + const existingIds = await getExistingPaletteIds( + env.DB, + palettes.map((palette) => palette._id), + ); + const unseenPalettes = palettes.filter( + (palette) => !existingIds.has(palette._id), + ); + const inserted = await insertPalettes(env.DB, unseenPalettes); + + pagesProcessed++; + totalInserted += inserted; + + console.log( + `Page ${page}: fetched ${palettes.length}, inserted ${inserted}, existing ${existingIds.size}`, + ); + + if (existingIds.size > 0) { + console.log( + `Page ${page}: encountered existing palette IDs; stopping pagination`, + ); + break; + } + + page++; + } catch (error) { + if (signal.aborted) { + console.warn("Palette sync timed out; stopping early"); + break; + } + + console.error(`Page ${page}: failed to sync palettes`, error); + return; + } + } + + console.log( + `Palette sync complete: inserted ${totalInserted} palettes across ${pagesProcessed} page(s)`, + ); +} diff --git a/tests/app.test.ts b/tests/app.test.ts new file mode 100644 index 0000000..e646b60 --- /dev/null +++ b/tests/app.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, it } from "vitest"; + +import app from "../src/app"; +import type { LospecPaletteRow } from "../src/app/models/lospec-palette"; +import { LOSPEC_PALETTES_PAGE_SIZE } from "../src/config/constants"; +import { createTestEnv } from "./helpers/env"; + +function createListPalettesDb(rows: LospecPaletteRow[]) { + let sql = ""; + let bindings: Array = []; + + const db = { + prepare(statement: string) { + sql = statement; + + return { + bind(...values: Array) { + bindings = values; + + return { + all: async () => ({ results: rows }), + }; + }, + }; + }, + } as unknown as D1Database; + + return { + db, + getSql: () => sql, + getBindings: () => bindings, + }; +} + +describe("app", () => { + it("returns an empty JSON body for GET /", async () => { + const response = await app.request("/", {}, createTestEnv()); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({}); + }); + + it("applies CORS headers for allowed origins", async () => { + const response = await app.request( + "/", + { + headers: new Headers({ Origin: "http://localhost:4321" }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe( + "http://localhost:4321", + ); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "GET, OPTIONS", + ); + expect(response.headers.get("Access-Control-Allow-Headers")).toBe( + "Content-Type", + ); + expect(response.headers.get("Vary")).toBe("Origin"); + }); + + it("handles OPTIONS preflight requests for allowed origins", async () => { + const response = await app.request( + "/", + { + method: "OPTIONS", + headers: new Headers({ Origin: "http://localhost:4321" }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(204); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe( + "http://localhost:4321", + ); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "GET, OPTIONS", + ); + expect(response.headers.get("Access-Control-Allow-Headers")).toBe( + "Content-Type", + ); + expect(await response.text()).toBe(""); + }); + + it("rejects disallowed origins", async () => { + const response = await app.request( + "/", + { + headers: new Headers({ Origin: "https://evil.example" }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: "Forbidden origin" }); + }); + + it("allows internal requests to bypass origin checks", async () => { + const response = await app.request( + "/", + { + headers: new Headers({ + Origin: "https://evil.example", + "X-Internal-Api-Key": "test-internal-api-key", + }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("Access-Control-Allow-Origin")).toBeNull(); + }); + + it("returns 400 for an invalid lospec page query", async () => { + const response = await app.request( + "/lospec_palettes?page=abc", + {}, + createTestEnv(), + ); + + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ + error: `Invalid page parameter. Expected a non-negative integer with ${LOSPEC_PALETTES_PAGE_SIZE} results per page.`, + }); + }); + + it("returns 429 when the rate limiter rejects the request", async () => { + const response = await app.request( + "/lospec_palettes", + {}, + createTestEnv({ + RATE_LIMITER: { + limit: async () => ({ success: false }), + }, + }), + ); + + expect(response.status).toBe(429); + expect(await response.json()).toEqual({ + error: "Rate limit exceeded. Try again in a minute.", + }); + }); + + it("lists palettes with normalized response data and query filters", async () => { + const database = createListPalettesDb([ + { + id: "sunset-1", + title: "Sunset", + slug: "sunset", + description: "Warm palette", + tags: JSON.stringify(["warm", "sky"]), + user: JSON.stringify({ name: "alice" }), + colors: JSON.stringify(["#ff6600", "#220044"]), + examples: JSON.stringify([ + { + image: "/pixel-art/sunset.png", + description: "Preview", + }, + ]), + published_at: "2026-04-01T00:00:00.000Z", + }, + ]); + + const response = await app.request( + "/lospec_palettes?page=1&search=%20Sunset%20&tags=%20Warm%20", + {}, + createTestEnv({ DB: database.db }), + ); + + expect(response.status).toBe(200); + expect(database.getSql()).toContain("LOWER(COALESCE(title, '')) LIKE ?"); + expect(database.getSql()).toContain("FROM json_each(lospec_palettes.tags)"); + expect(database.getBindings()).toEqual(["%sunset%", "warm", 100, 100]); + expect(await response.json()).toEqual([ + { + id: "sunset-1", + title: "Sunset", + slug: "sunset", + description: "Warm palette", + tags: ["warm", "sky"], + user: "alice", + colors: ["#ff6600", "#220044"], + examples: [ + { + image: "https://cdn.lospec.com/pixel-art/sunset.png", + description: "Preview", + }, + ], + published_at: "2026-04-01T00:00:00.000Z", + }, + ]); + }); +}); diff --git a/tests/helpers/env.ts b/tests/helpers/env.ts new file mode 100644 index 0000000..167612b --- /dev/null +++ b/tests/helpers/env.ts @@ -0,0 +1,31 @@ +import type { Env, RateLimiter } from "../../src/config/types"; + +function createDbStub(): D1Database { + return { + prepare() { + throw new Error("DB stub should not be used in this test."); + }, + batch() { + throw new Error("DB stub should not be used in this test."); + }, + dump() { + throw new Error("DB stub should not be used in this test."); + }, + exec() { + throw new Error("DB stub should not be used in this test."); + }, + } as unknown as D1Database; +} + +export function createTestEnv(overrides: Partial = {}): Env { + const rateLimiter: RateLimiter = { + limit: async () => ({ success: true }), + }; + + return { + DB: createDbStub(), + RATE_LIMITER: rateLimiter, + INTERNAL_API_KEY: "test-internal-api-key", + ...overrides, + }; +} diff --git a/tests/models-and-services.test.ts b/tests/models-and-services.test.ts new file mode 100644 index 0000000..f046f6c --- /dev/null +++ b/tests/models-and-services.test.ts @@ -0,0 +1,212 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + isLospecPaletteApiItem, + mapRowToResponse, +} from "../src/app/models/lospec-palette"; +import { fetchLospecPalettePage } from "../src/app/services/lospec-api"; +import { + getExistingPaletteIds, + insertPalettes, +} from "../src/app/services/lospec-palettes-repository"; + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("lospec palette models", () => { + it("accepts API items with an id", () => { + expect(isLospecPaletteApiItem({ _id: "palette-1" })).toBe(true); + expect(isLospecPaletteApiItem({ title: "missing id" })).toBe(false); + }); + + it("maps rows back into API responses", () => { + const response = mapRowToResponse({ + id: "palette-1", + title: "Palette", + slug: "palette", + description: "Example", + tags: "not-json", + user: JSON.stringify({ name: "alice" }), + colors: JSON.stringify(["#ffffff"]), + examples: JSON.stringify([ + { + image: "/sprites/example.png", + description: "Preview", + }, + ]), + published_at: "2026-04-02T00:00:00.000Z", + }); + + expect(response).toEqual({ + id: "palette-1", + title: "Palette", + slug: "palette", + description: "Example", + tags: "not-json", + user: "alice", + colors: ["#ffffff"], + examples: [ + { + image: "https://cdn.lospec.com/sprites/example.png", + description: "Preview", + }, + ], + published_at: "2026-04-02T00:00:00.000Z", + }); + }); +}); + +describe("lospec API service", () => { + it("fetches and filters a page of Lospec palettes", async () => { + const fetchMock = vi.fn(async (url: URL, init?: RequestInit) => { + expect(url.searchParams.get("colorNumberFilterType")).toBe("any"); + expect(url.searchParams.get("page")).toBe("3"); + expect(url.searchParams.get("tag")).toBe(""); + expect(url.searchParams.get("sortingType")).toBe("newest"); + expect(init?.signal).toBeDefined(); + + return new Response( + JSON.stringify({ + palettes: [{ _id: "palette-1" }, { title: "missing id" }], + }), + { status: 200 }, + ); + }); + + vi.stubGlobal("fetch", fetchMock); + + const result = await fetchLospecPalettePage( + 3, + new AbortController().signal, + ); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(result).toEqual([{ _id: "palette-1" }]); + }); + + it("returns an empty array when the payload has no palettes list", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(JSON.stringify({}), { status: 200 })), + ); + + const result = await fetchLospecPalettePage( + 0, + new AbortController().signal, + ); + + expect(result).toEqual([]); + }); + + it("throws when Lospec returns a non-success response", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response("upstream error", { status: 503 })), + ); + + await expect( + fetchLospecPalettePage(4, new AbortController().signal), + ).rejects.toThrow("Failed to fetch page 4: HTTP 503"); + }); +}); + +describe("lospec palette repository", () => { + it("skips querying when there are no ids", async () => { + const prepare = vi.fn(); + const database = { prepare } as unknown as D1Database; + + const result = await getExistingPaletteIds(database, []); + + expect(prepare).not.toHaveBeenCalled(); + expect(result).toEqual(new Set()); + }); + + it("loads existing palette ids from D1", async () => { + const all = vi.fn(async () => ({ + results: [{ id: "palette-1" }, { id: "palette-2" }], + })); + const bind = vi.fn(() => ({ all })); + const prepare = vi.fn(() => ({ bind })); + const database = { prepare } as unknown as D1Database; + + const result = await getExistingPaletteIds(database, [ + "palette-1", + "palette-2", + ]); + + expect(prepare).toHaveBeenCalledWith( + "SELECT id FROM lospec_palettes WHERE id IN (?, ?)", + ); + expect(bind).toHaveBeenCalledWith("palette-1", "palette-2"); + expect(result).toEqual(new Set(["palette-1", "palette-2"])); + }); + + it("inserts normalized palette rows in a batch", async () => { + const preparedStatements: Array<{ sql: string; bindings: unknown[] }> = []; + const batch = vi.fn(async (statements: unknown[]) => statements); + const database = { + prepare(sql: string) { + return { + bind(...bindings: unknown[]) { + const statement = { sql, bindings }; + preparedStatements.push(statement); + return statement; + }, + }; + }, + batch, + } as unknown as D1Database; + + const inserted = await insertPalettes(database, [ + { + _id: "palette-1", + title: "Palette", + slug: "palette", + description: "Example", + tags: ["warm"], + user: { name: "alice" }, + colors: ["#ffffff"], + examples: [ + { image: "/sprites/example.png", description: "Preview" }, + { description: "Ignored because it has no image" }, + ], + publishedAt: "2026-04-02T00:00:00.000Z", + }, + ]); + + expect(inserted).toBe(1); + expect(batch).toHaveBeenCalledWith(preparedStatements); + expect(preparedStatements).toHaveLength(1); + expect(preparedStatements[0].sql).toContain( + "INSERT OR IGNORE INTO lospec_palettes", + ); + expect(preparedStatements[0].bindings).toEqual([ + "palette-1", + "Palette", + "palette", + "Example", + JSON.stringify(["warm"]), + "alice", + JSON.stringify(["#ffffff"]), + JSON.stringify([ + { + image: "https://cdn.lospec.com/sprites/example.png", + description: "Preview", + }, + ]), + "2026-04-02T00:00:00.000Z", + ]); + }); + + it("returns zero inserts without calling batch for an empty palette list", async () => { + const batch = vi.fn(); + const database = { batch } as unknown as D1Database; + + const inserted = await insertPalettes(database, []); + + expect(inserted).toBe(0); + expect(batch).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/sync-palettes.test.ts b/tests/sync-palettes.test.ts new file mode 100644 index 0000000..af5c2b0 --- /dev/null +++ b/tests/sync-palettes.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { Env } from "../src/config/types"; +import { fetchLospecPalettePage } from "../src/app/services/lospec-api"; +import { + getExistingPaletteIds, + insertPalettes, +} from "../src/app/services/lospec-palettes-repository"; +import { syncPalettes } from "../src/jobs/sync-palettes"; + +vi.mock("../src/app/services/lospec-api", () => ({ + fetchLospecPalettePage: vi.fn(), +})); + +vi.mock("../src/app/services/lospec-palettes-repository", () => ({ + getExistingPaletteIds: vi.fn(), + insertPalettes: vi.fn(), +})); + +function createEnv(): Env { + return { + DB: {} as D1Database, + RATE_LIMITER: { + limit: async () => ({ success: true }), + }, + INTERNAL_API_KEY: "test-key", + }; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); +}); + +describe("syncPalettes", () => { + it("syncs pages until Lospec returns an empty page", async () => { + const env = createEnv(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const existingIdsMock = vi.mocked(getExistingPaletteIds); + const insertMock = vi.mocked(insertPalettes); + + fetchMock + .mockResolvedValueOnce([{ _id: "palette-1" }]) + .mockResolvedValueOnce([]); + existingIdsMock.mockResolvedValue(new Set()); + insertMock.mockResolvedValue(1); + vi.spyOn(console, "log").mockImplementation(() => {}); + + await syncPalettes(env, new AbortController().signal); + + expect(fetchMock).toHaveBeenNthCalledWith(1, 0, expect.any(AbortSignal)); + expect(fetchMock).toHaveBeenNthCalledWith(2, 1, expect.any(AbortSignal)); + expect(existingIdsMock).toHaveBeenCalledWith(env.DB, ["palette-1"]); + expect(insertMock).toHaveBeenCalledWith(env.DB, [{ _id: "palette-1" }]); + }); + + it("stops when it encounters existing palette ids", async () => { + const env = createEnv(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const existingIdsMock = vi.mocked(getExistingPaletteIds); + const insertMock = vi.mocked(insertPalettes); + + fetchMock.mockResolvedValueOnce([{ _id: "palette-1" }]); + existingIdsMock.mockResolvedValueOnce(new Set(["palette-1"])); + insertMock.mockResolvedValue(0); + vi.spyOn(console, "log").mockImplementation(() => {}); + + await syncPalettes(env, new AbortController().signal); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(insertMock).toHaveBeenCalledWith(env.DB, []); + }); + + it("warns instead of erroring when the sync times out", async () => { + const env = createEnv(); + const controller = new AbortController(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => {}); + + fetchMock.mockImplementationOnce(async () => { + controller.abort(); + throw new Error("timed out"); + }); + + await syncPalettes(env, controller.signal); + + expect(warnSpy).toHaveBeenCalledWith( + "Palette sync timed out; stopping early", + ); + expect(errorSpy).not.toHaveBeenCalled(); + }); + + it("logs and returns when a page fails to sync", async () => { + const env = createEnv(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => {}); + + fetchMock.mockRejectedValueOnce(new Error("boom")); + + await syncPalettes(env, new AbortController().signal); + + expect(errorSpy).toHaveBeenCalledWith( + "Page 0: failed to sync palettes", + expect.any(Error), + ); + }); +}); diff --git a/tests/worker.test.ts b/tests/worker.test.ts new file mode 100644 index 0000000..4730617 --- /dev/null +++ b/tests/worker.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { Env } from "../src/config/types"; +import worker from "../src/index"; +import { syncPalettes } from "../src/jobs/sync-palettes"; + +vi.mock("../src/jobs/sync-palettes", () => ({ + syncPalettes: vi.fn(async () => {}), +})); + +function createEnv(): Env { + return { + DB: {} as D1Database, + RATE_LIMITER: { + limit: async () => ({ success: true }), + }, + INTERNAL_API_KEY: "test-key", + }; +} + +describe("worker entrypoint", () => { + it("schedules palette sync with a timeout signal", async () => { + const env = createEnv(); + const waitUntil = vi.fn(); + + await worker.scheduled({} as ScheduledEvent, env, { + waitUntil, + } as ExecutionContext); + + expect(vi.mocked(syncPalettes)).toHaveBeenCalledWith( + env, + expect.any(AbortSignal), + ); + expect(waitUntil).toHaveBeenCalledTimes(1); + await expect(waitUntil.mock.calls[0][0]).resolves.toBeUndefined(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index b7eb1cb..0cda447 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,5 +9,5 @@ "noEmit": true, "skipLibCheck": true }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts"] } \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0193e87 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,28 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.toml", + }, + }), + ], + test: { + include: ["tests/**/*.test.ts"], + coverage: { + provider: "istanbul", + reporter: ["text", "lcov"], + include: ["src/**/*.ts"], + exclude: ["src/config/types.ts"], + reportOnFailure: true, + thresholds: { + lines: 80.01, + functions: 80.01, + branches: 80.01, + statements: 80.01, + }, + }, + }, +}); diff --git a/wrangler.toml b/wrangler.toml index 254071d..91be431 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,22 +6,23 @@ compatibility_date = "2026-04-09" # The zone (2dtiler.com) must be on your Cloudflare account. route = { pattern = "api.2dtiler.com/*", zone_name = "2dtiler.com" } +[[d1_databases]] +binding = "DB" +database_name = "2dtiler" +database_id = "bd9d2b49-65a8-4684-9911-38c6095c6f0b" + [triggers] # Run daily at 14:00 UTC crons = ["0 14 * * *"] -[[kv_namespaces]] -binding = "LOSPEC_PALETTES" -id = "88adee5ffcfc4b36a3743f95fc18bc67" - -# Rate limiting: 10 requests per hour per IP +# Rate limiting: 10 requests per minute per IP [[unsafe.bindings]] name = "RATE_LIMITER" type = "ratelimit" namespace_id = "1001" [unsafe.bindings.simple] limit = 10 -period = 3600 +period = 60 [observability] enabled = false