From 3cdb1fd098f0bfaff570d6e8458a2eec23dcfe35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:20:10 +0000 Subject: [PATCH 01/18] refactor: split into static frontend and Cloudflare Worker API - Convert Next.js to static export (output: "export") - Move sanitization logic to worker/ with Hono - Frontend now calls /api/sanitize endpoint client-side - Remove SSR dependencies (got, js-yaml from frontend) - Update Dockerfile for static site serving --- .gitignore | 1 + Dockerfile | 17 +- next.config.ts | 5 +- package-lock.json | 306 +--- package.json | 6 +- pages/api/sanitize.ts | 27 - pages/sanitize.tsx | 88 +- tsconfig.json | 2 +- utils/sanitizer.ts | 158 -- worker/package-lock.json | 3088 ++++++++++++++++++++++++++++++++++++++ worker/package.json | 22 + worker/src/index.ts | 31 + worker/src/sanitizer.ts | 128 ++ worker/src/types.ts | 26 + worker/tsconfig.json | 12 + worker/wrangler.toml | 15 + 16 files changed, 3394 insertions(+), 538 deletions(-) delete mode 100644 pages/api/sanitize.ts delete mode 100644 utils/sanitizer.ts create mode 100644 worker/package-lock.json create mode 100644 worker/package.json create mode 100644 worker/src/index.ts create mode 100644 worker/src/sanitizer.ts create mode 100644 worker/src/types.ts create mode 100644 worker/tsconfig.json create mode 100644 worker/wrangler.toml diff --git a/.gitignore b/.gitignore index a4ef2c0..30dfde7 100755 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ yarn-error.log* # typescript *.tsbuildinfo +worker/node_modules diff --git a/Dockerfile b/Dockerfile index 0840fba..6f70bc2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci -# Rebuild the source code only when needed +# Build static site FROM node:25-alpine AS builder WORKDIR /app @@ -16,30 +16,21 @@ COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build -# Production image, copy all the files and run next +# Production image with static files FROM node:25-alpine AS runner LABEL org.opencontainers.image.source="https://github.com/Tail-WTF/Website" WORKDIR /app -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -COPY --from=builder /app/next.config.ts ./ -COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/out ./out COPY --from=builder /app/package.json ./package.json -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - USER nextjs EXPOSE 3000 ENV PORT=3000 -CMD ["node", "server.js"] +CMD ["npx", "serve", "out", "-l", "3000"] diff --git a/next.config.ts b/next.config.ts index 9f8d078..7825fab 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,8 +2,11 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, - output: "standalone", + output: "export", poweredByHeader: false, + images: { + unoptimized: true, + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index ff7d47e..e4cc7fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,12 @@ "name": "tail.wtf-website", "version": "0.1.0", "dependencies": { - "got": "^14.6.5", - "js-yaml": "4.1.1", - "linkifyjs": "4.3.2", "next": "^16.0.9", "react": "19.2.1", "react-dom": "19.2.1" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.17", - "@types/js-yaml": "^4.0.5", "@types/node": "24.10.1", "@types/react": "19.2.7", "@types/react-dom": "19.2.3", @@ -78,7 +74,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1050,12 +1045,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "license": "MIT" - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1298,24 +1287,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz", - "integrity": "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1674,19 +1645,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "license": "MIT" - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1717,7 +1675,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2070,7 +2027,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2125,6 +2081,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -2411,7 +2368,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2426,54 +2382,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/byte-counter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", - "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "13.0.15", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.15.tgz", - "integrity": "sha512-NjiSrjv37X73FmGGU5ec/M83vWQ6q1Ae3BFe+ABfdeeMy4LOMKYTpfEjrBnLedu43clKZtsYbKrHTIQE7vKq+A==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.0.4", - "get-stream": "^9.0.1", - "http-cache-semantics": "^4.2.0", - "keyv": "^5.5.4", - "mimic-response": "^4.0.0", - "normalize-url": "^8.1.0", - "responselike": "^4.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cacheable-request/node_modules/keyv": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz", - "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2712,21 +2620,6 @@ } } }, - "node_modules/decompress-response": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", - "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", - "license": "MIT", - "dependencies": { - "mimic-response": "^4.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3041,7 +2934,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3305,7 +3197,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -3485,7 +3376,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3892,15 +3782,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data-encoder": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", - "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4001,22 +3882,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4104,53 +3969,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "14.6.5", - "resolved": "https://registry.npmjs.org/got/-/got-14.6.5.tgz", - "integrity": "sha512-Su87c0NNeg97de1sO02gy9I8EmE7DCJ1gzcFLcgGpYeq2PnLg4xz73MWrp6HjqbSsjb6Glf4UBDW6JNyZA6uSg==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^7.0.1", - "byte-counter": "^0.1.0", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^13.0.12", - "decompress-response": "^10.0.0", - "form-data-encoder": "^4.0.2", - "http2-wrapper": "^2.2.1", - "keyv": "^5.5.3", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^4.0.1", - "responselike": "^4.0.2", - "type-fest": "^4.26.1" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/got/node_modules/keyv": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz", - "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/got/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4273,25 +4091,6 @@ "hermes-estree": "0.25.1" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4649,18 +4448,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -4810,6 +4597,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5186,12 +4974,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/linkifyjs": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", - "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5228,18 +5010,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5284,18 +5054,6 @@ "node": ">=8.6" } }, - "node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5454,18 +5212,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", - "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5624,15 +5370,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/p-cancelable": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", - "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5779,7 +5516,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5912,24 +5648,11 @@ ], "license": "MIT" }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react": { "version": "19.2.1", "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5939,7 +5662,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6016,12 +5738,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6042,21 +5758,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/responselike": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", - "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6635,7 +6336,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6785,7 +6485,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7037,7 +6736,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 2c11cec..de8c5a0 100644 --- a/package.json +++ b/package.json @@ -9,20 +9,16 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", + "start": "npx serve out", "lint": "eslint . && prettier --check ." }, "dependencies": { - "got": "^14.6.5", - "js-yaml": "4.1.1", - "linkifyjs": "4.3.2", "next": "^16.0.9", "react": "19.2.1", "react-dom": "19.2.1" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.17", - "@types/js-yaml": "^4.0.5", "@types/node": "24.10.1", "@types/react": "19.2.7", "@types/react-dom": "19.2.3", diff --git a/pages/api/sanitize.ts b/pages/api/sanitize.ts deleted file mode 100644 index 1b41430..0000000 --- a/pages/api/sanitize.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { sanitizeLinkInText } from "../../utils/sanitizer"; - -type ResponseData = - | { - text: string; - sanitizedURLs: string[]; - } - | { - error: string; - }; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { text = null } = req.query; - if (!text || Array.isArray(text)) { - return res.status(400).json({ error: "Invalid input" }); - } - - const sanitized = await sanitizeLinkInText(text, 3); - res.status(200).json({ - text: sanitized.text, - sanitizedURLs: sanitized.links, - }); -} diff --git a/pages/sanitize.tsx b/pages/sanitize.tsx index 0633c13..4087ba9 100644 --- a/pages/sanitize.tsx +++ b/pages/sanitize.tsx @@ -4,39 +4,69 @@ import Layout from "../components/layout"; import { LinkBox } from "../components/form"; import { H1 } from "../components/headings"; -import { useState } from "react"; -import { GetServerSideProps } from "next"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; -import { sanitizeLinkInText } from "../utils/sanitizer"; +const API_URL = process.env.NEXT_PUBLIC_API_URL || ""; -export const getServerSideProps: GetServerSideProps = async (ctx) => { - const { url = null } = ctx.query; - if (!url || Array.isArray(url)) { - return { - notFound: true, +type State = + | { status: "loading" } + | { status: "success"; text: string; sanitizedURL: string } + | { status: "error" }; + +export default function ResultWrapper() { + const router = useRouter(); + const { url } = router.query; + const validUrl = typeof url === "string" ? url : null; + + const [state, setState] = useState({ status: "loading" }); + + useEffect(() => { + if (!validUrl) return; + + let cancelled = false; + + (async () => { + try { + const params = new URLSearchParams({ text: validUrl }); + const res = await fetch(`${API_URL}/api/sanitize?${params}`); + const data = await res.json(); + + if (cancelled) return; + + if (data.sanitizedURLs?.[0]) { + setState({ + status: "success", + text: data.text, + sanitizedURL: data.sanitizedURLs[0], + }); + } else { + setState({ status: "error" }); + } + } catch { + if (!cancelled) setState({ status: "error" }); + } + })(); + + return () => { + cancelled = true; }; - } + }, [validUrl]); - const sanitized = await sanitizeLinkInText(url); - return { - props: { - text: sanitized.text, - sanitizedURL: sanitized.links?.[0] || null, - }, - }; -}; + if (!validUrl || state.status === "loading") { + return ( + +
+

Sanitizing...

+
+
+ ); + } -export default function ResultWrapper({ - text, - sanitizedURL, -}: { - text: string; - sanitizedURL: string | undefined; -}) { return ( - {sanitizedURL ? ( - + {state.status === "success" ? ( + ) : ( )} @@ -49,7 +79,7 @@ function ResultSuccess({ sanitizedURL, }: { text: string; - sanitizedURL: string | undefined; + sanitizedURL: string; }): React.ReactElement { const [copyState, setCopyState] = useState(null); @@ -58,7 +88,7 @@ function ResultSuccess({ try { navigator.clipboard.writeText(text); setCopyState(true); - } catch (error) { + } catch { setCopyState(false); } }; @@ -79,7 +109,7 @@ function ResultSuccess({ readOnly value={text} className="border-lime-200 pr-14 text-lime-200" - onClick={copyState === null ? handleInputClick : undefined} // Only trigger on first click + onClick={copyState === null ? handleInputClick : undefined} /> { - const links = linkify(text, "url"); - if (links.length == 0) return { text, links: [] }; - - const sanitizedLinks: string[] = []; - let sanitizedText = text; - let offset = 0; // Offset to manage the changes in string length - - const linksToProcess = links.slice(0, maxLinks); - for (const link of linksToProcess) { - try { - const sanitized = await sanitizeURL(link.href); - sanitizedLinks.push(sanitized); - - const start = link.start + offset; - const end = link.end + offset; - sanitizedText = - sanitizedText.substring(0, start) + - sanitized + - sanitizedText.substring(end); - - offset += sanitized.length - (end - start); - } catch (e) { - if (process.env.NODE_ENV === "development") { - console.error("Failed to sanitize URL:", e); - } - } - } - - return { - text: sanitizedText, - links: sanitizedLinks, - }; -} - -export async function sanitizeURL(originalURL: string) { - const allRules = loadSanitizationRules(); - let url: URL = new URL(originalURL); - - try { - getRuleForURL(allRules, url, "expand"); - let expandedURLs = await expandShortURL(url); - - // iterate reversely to get the last matched expanded URL - for (let i = expandedURLs.length - 1; i >= 0; i--) { - try { - getRuleForURL(allRules, expandedURLs[i], "sanitize"); - url = expandedURLs[i]; - break; - } catch (e) { - continue; - } - } - } catch (e) { - if (process.env.NODE_ENV === "development") { - console.error("Exception during URL expansion:", e); - } - } - - // Sanitize path - let [rule, matches] = getRuleForURL(allRules, url, "sanitize"); - if (rule?.sanitizePath) { - url.pathname = matches[0]; - } - - // Sanitize params - const sanitizedParams = new URLSearchParams(); - for (const param of rule.allowedParams) { - const ignorableValues = rule?.ignorableParamValues?.[param]; - for (const value of url.searchParams.getAll(param)) { - if (ignorableValues && ignorableValues.includes(value)) continue; - sanitizedParams.append(param, value); - } - } - url.search = sanitizedParams.toString(); - - return url.toString(); -} - -function loadSanitizationRules(): RuleSet { - const rules = YAML.load(fs.readFileSync(RULE_FILE).toString()) as RuleSet; - return rules; -} - -function getRuleForURL(ALL_RULES: any, url: URL, type: string) { - if (!ALL_RULES.hasOwnProperty(url.host)) { - throw new Error(`No rules found for domain ${url.host}`); - } - - const rules = ALL_RULES[url.host]; - - if (!rules.hasOwnProperty(type)) { - throw new Error(`No rules found for type ${type}`); - } - - for (const rule of rules[type]) { - // Check if rule pattern matches the URL.pathname, extract the matched parts - const matches = new RegExp(rule.pattern).exec(url.pathname); - if (matches) return [rule, matches]; - } - - throw new Error(`No rules found for path ${url.pathname}`); -} - -async function expandShortURL(shortURL: URL): Promise { - const response = await got(shortURL, { - method: "GET", - maxRedirects: 5, - followRedirect: true, - }); - - return response.redirectUrls; -} diff --git a/worker/package-lock.json b/worker/package-lock.json new file mode 100644 index 0000000..b0e559d --- /dev/null +++ b/worker/package-lock.json @@ -0,0 +1,3088 @@ +{ + "name": "@tail-wtf/worker", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@tail-wtf/worker", + "version": "0.1.0", + "dependencies": { + "hono": "^4.7.9", + "js-yaml": "^4.1.1", + "linkifyjs": "^4.3.2" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250520.0", + "@types/js-yaml": "^4.0.5", + "vitest": "^3.2.4", + "wrangler": "^4.18.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz", + "integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.7.13", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz", + "integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20251202.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251210.0.tgz", + "integrity": "sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251210.0.tgz", + "integrity": "sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251210.0.tgz", + "integrity": "sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251210.0.tgz", + "integrity": "sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251210.0.tgz", + "integrity": "sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20251225.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251225.0.tgz", + "integrity": "sha512-ZZl0cNLFcsBRFKtMftKWOsfAybUYSeiTMzpQV1NlTVlByHAs1rGQt45Jw/qz8LrfHoq9PGTieSj9W350Gi4Pvg==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.12.tgz", + "integrity": "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/hono": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz", + "integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "4.20251210.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251210.0.tgz", + "integrity": "sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", + "stoppable": "1.1.0", + "undici": "7.14.0", + "workerd": "1.20251210.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251210.0.tgz", + "integrity": "sha512-9MUUneP1BnRE9XAYi94FXxHmiLGbO75EHQZsgWqSiOXjoXSqJCw8aQbIEPxCy19TclEl/kHUFYce8ST2W+Qpjw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20251210.0", + "@cloudflare/workerd-darwin-arm64": "1.20251210.0", + "@cloudflare/workerd-linux-64": "1.20251210.0", + "@cloudflare/workerd-linux-arm64": "1.20251210.0", + "@cloudflare/workerd-windows-64": "1.20251210.0" + } + }, + "node_modules/wrangler": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.54.0.tgz", + "integrity": "sha512-bANFsjDwJLbprYoBK+hUDZsVbUv2SqJd8QvArLIcZk+fPq4h/Ohtj5vkKXD3k0s2bD1DXLk08D+hYmeNH+xC6A==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.1", + "@cloudflare/unenv-preset": "2.7.13", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.0", + "miniflare": "4.20251210.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20251210.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20251210.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "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" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/worker/package.json b/worker/package.json new file mode 100644 index 0000000..e809121 --- /dev/null +++ b/worker/package.json @@ -0,0 +1,22 @@ +{ + "name": "@tail-wtf/worker", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "test": "vitest run" + }, + "dependencies": { + "hono": "^4.7.9", + "js-yaml": "^4.1.1", + "linkifyjs": "^4.3.2" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250520.0", + "@types/js-yaml": "^4.0.5", + "vitest": "^3.2.4", + "wrangler": "^4.18.0" + } +} diff --git a/worker/src/index.ts b/worker/src/index.ts new file mode 100644 index 0000000..a6ea53d --- /dev/null +++ b/worker/src/index.ts @@ -0,0 +1,31 @@ +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { sanitizeLinkInText } from "./sanitizer"; +import type { Env, RuleSet } from "./types"; + +const app = new Hono<{ Bindings: Env }>(); + +app.use("*", cors()); + +async function loadRules(env: Env): Promise { + const cached = await env.RULES_KV.get("rules"); + if (cached) return JSON.parse(cached); + return {}; +} + +app.get("/api/sanitize", async (c) => { + const text = c.req.query("text"); + if (!text) { + return c.json({ error: "Invalid input" }, 400); + } + + const rules = await loadRules(c.env); + const sanitized = await sanitizeLinkInText(text, rules, 3); + + return c.json({ + text: sanitized.text, + sanitizedURLs: sanitized.links, + }); +}); + +export default app; diff --git a/worker/src/sanitizer.ts b/worker/src/sanitizer.ts new file mode 100644 index 0000000..12a8759 --- /dev/null +++ b/worker/src/sanitizer.ts @@ -0,0 +1,128 @@ +import { find as linkify } from "linkifyjs"; +import type { RuleSet, SanitizeRule } from "./types"; + +export async function sanitizeLinkInText( + text: string, + rules: RuleSet, + maxLinks: number = 1, +): Promise<{ text: string; links: string[] }> { + const links = linkify(text, "url"); + if (links.length === 0) return { text, links: [] }; + + const sanitizedLinks: string[] = []; + let sanitizedText = text; + let offset = 0; + + const linksToProcess = links.slice(0, maxLinks); + for (const link of linksToProcess) { + try { + const sanitized = await sanitizeURL(link.href, rules); + sanitizedLinks.push(sanitized); + + const start = link.start + offset; + const end = link.end + offset; + sanitizedText = + sanitizedText.substring(0, start) + + sanitized + + sanitizedText.substring(end); + + offset += sanitized.length - (end - start); + } catch { + // Skip URLs that can't be sanitized + } + } + + return { text: sanitizedText, links: sanitizedLinks }; +} + +export async function sanitizeURL( + originalURL: string, + allRules: RuleSet, +): Promise { + let url = new URL(originalURL); + + try { + getRuleForURL(allRules, url, "expand"); + const expandedURLs = await expandShortURL(url); + + for (let i = expandedURLs.length - 1; i >= 0; i--) { + try { + getRuleForURL(allRules, expandedURLs[i], "sanitize"); + url = expandedURLs[i]; + break; + } catch { + continue; + } + } + } catch { + // No expand rule or expansion failed + } + + const [rule, matches] = getRuleForURL(allRules, url, "sanitize"); + if (rule.sanitizePath) { + url.pathname = matches[0]; + } + + const sanitizedParams = new URLSearchParams(); + for (const param of rule.allowedParams) { + const ignorableValues = rule.ignorableParamValues?.[param]; + for (const value of url.searchParams.getAll(param)) { + if (ignorableValues && ignorableValues.includes(value)) continue; + sanitizedParams.append(param, value); + } + } + url.search = sanitizedParams.toString(); + + return url.toString(); +} + +function getRuleForURL( + allRules: RuleSet, + url: URL, + type: "expand" | "sanitize", +): [SanitizeRule, RegExpExecArray] { + if (!(url.host in allRules)) { + throw new Error(`No rules found for domain ${url.host}`); + } + + const rules = allRules[url.host]; + const typeRules = rules[type]; + + if (!typeRules) { + throw new Error(`No rules found for type ${type}`); + } + + for (const rule of typeRules) { + const matches = new RegExp(rule.pattern).exec(url.pathname); + if (matches) return [rule as SanitizeRule, matches]; + } + + throw new Error(`No rules found for path ${url.pathname}`); +} + +async function expandShortURL(shortURL: URL): Promise { + const urls: URL[] = []; + let currentUrl = shortURL.toString(); + + for (let i = 0; i < 5; i++) { + const response = await fetch(currentUrl, { + method: "HEAD", + redirect: "manual", + }); + + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("location"); + if (location) { + const nextUrl = new URL(location, currentUrl); + urls.push(nextUrl); + currentUrl = nextUrl.toString(); + } else { + break; + } + } else { + break; + } + } + + return urls; +} diff --git a/worker/src/types.ts b/worker/src/types.ts new file mode 100644 index 0000000..3b95943 --- /dev/null +++ b/worker/src/types.ts @@ -0,0 +1,26 @@ +export interface Env { + RULES_KV: KVNamespace; + AI: Ai; + BROWSER: Fetcher; +} + +interface BaseRule { + pattern: string; +} + +export interface ExpandRule extends BaseRule {} + +export interface SanitizeRule extends BaseRule { + allowedParams: string[]; + sanitizePath?: boolean; + ignorableParamValues?: Record; +} + +export interface SiteRule { + expand?: ExpandRule[]; + sanitize?: SanitizeRule[]; +} + +export interface RuleSet { + [domain: string]: SiteRule; +} diff --git a/worker/tsconfig.json b/worker/tsconfig.json new file mode 100644 index 0000000..588af55 --- /dev/null +++ b/worker/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/worker/wrangler.toml b/worker/wrangler.toml new file mode 100644 index 0000000..945dc69 --- /dev/null +++ b/worker/wrangler.toml @@ -0,0 +1,15 @@ +name = "tail-wtf-api" +main = "src/index.ts" +compatibility_date = "2024-12-01" +compatibility_flags = ["nodejs_compat"] + +[[kv_namespaces]] +binding = "RULES_KV" +id = "placeholder-id" +preview_id = "placeholder-preview-id" + +[ai] +binding = "AI" + +[browser] +binding = "BROWSER" From 7e570f930215ecc3140dbd16927e8eff39dbfdee Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:21:19 +0000 Subject: [PATCH 02/18] feat: add Telegram and Matrix bot API compatibility - POST /api/bot/telegram for Telegram webhook integration - POST /api/bot/matrix for Matrix bot integration - Both extract text, sanitize URLs, and return formatted responses --- worker/src/index.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/worker/src/index.ts b/worker/src/index.ts index a6ea53d..2f9eabf 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -28,4 +28,64 @@ app.get("/api/sanitize", async (c) => { }); }); +interface TelegramUpdate { + message?: { + chat: { id: number }; + text?: string; + message_id: number; + }; +} + +app.post("/api/bot/telegram", async (c) => { + const update = await c.req.json(); + const text = update.message?.text; + + if (!text) { + return c.json({ ok: true }); + } + + const rules = await loadRules(c.env); + const sanitized = await sanitizeLinkInText(text, rules, 3); + + if (sanitized.links.length === 0) { + return c.json({ ok: true }); + } + + return c.json({ + method: "sendMessage", + chat_id: update.message!.chat.id, + text: sanitized.text, + reply_to_message_id: update.message!.message_id, + }); +}); + +interface MatrixEvent { + room_id?: string; + sender?: string; + content?: { body?: string }; +} + +app.post("/api/bot/matrix", async (c) => { + const event = await c.req.json(); + const text = event.content?.body; + + if (!text) { + return c.json({}); + } + + const rules = await loadRules(c.env); + const sanitized = await sanitizeLinkInText(text, rules, 3); + + if (sanitized.links.length === 0) { + return c.json({}); + } + + return c.json({ + body: sanitized.text, + msgtype: "m.text", + format: "org.matrix.custom.html", + formatted_body: sanitized.text, + }); +}); + export default app; From 3cf52f23d34369b39a7ae3e610353fbd5e46f197 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:22:43 +0000 Subject: [PATCH 03/18] feat: add loading animations using Tailwind - Pulsing skeleton for loading state - Smooth transition on copy feedback --- pages/sanitize.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pages/sanitize.tsx b/pages/sanitize.tsx index 4087ba9..db1b1e5 100644 --- a/pages/sanitize.tsx +++ b/pages/sanitize.tsx @@ -57,8 +57,11 @@ export default function ResultWrapper() { return (
-

Sanitizing...

+

Sanitizing...

+
- {copyState !== null && - (copyState ? ( -

Copied!

- ) : ( -

- Unable to copy link. Please try manually. -

- ))} + {copyState !== null && ( +

+ {copyState ? "Copied!" : "Unable to copy link. Please try manually."} +

+ )} ); } From b962ef351f4a814508a394b0591172e07be8d3d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:25:12 +0000 Subject: [PATCH 04/18] feat: add AI-based URL sanitization fallback - POST /api/ai-sanitize endpoint using Cloudflare Workers AI - Uses function calling to ensure structured response - Returns sanitized URL with confidence score - Suggests sanitization rules for contribution - Frontend offers AI option when rule matching fails --- pages/sanitize.tsx | 113 +++++++++++++++++++++++++++++++++++++++++--- worker/src/index.ts | 111 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 7 deletions(-) diff --git a/pages/sanitize.tsx b/pages/sanitize.tsx index db1b1e5..6171058 100644 --- a/pages/sanitize.tsx +++ b/pages/sanitize.tsx @@ -12,7 +12,7 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || ""; type State = | { status: "loading" } | { status: "success"; text: string; sanitizedURL: string } - | { status: "error" }; + | { status: "error"; originalUrl: string }; export default function ResultWrapper() { const router = useRouter(); @@ -41,10 +41,10 @@ export default function ResultWrapper() { sanitizedURL: data.sanitizedURLs[0], }); } else { - setState({ status: "error" }); + setState({ status: "error", originalUrl: validUrl }); } } catch { - if (!cancelled) setState({ status: "error" }); + if (!cancelled) setState({ status: "error", originalUrl: validUrl }); } })(); @@ -71,7 +71,7 @@ export default function ResultWrapper() { {state.status === "success" ? ( ) : ( - + )}
); @@ -142,7 +142,55 @@ function ResultSuccess({ ); } -function ResultFailure(): React.ReactElement { +interface AiResult { + sanitizedUrl: string; + confidence: number; + removedParams: string[]; + keptParams: string[]; + suggestedRule: { pattern: string; allowedParams: string[] } | null; +} + +function ResultFailure({ + originalUrl, +}: { + originalUrl: string; +}): React.ReactElement { + const [aiState, setAiState] = useState< + "idle" | "loading" | "success" | "error" + >("idle"); + const [aiResult, setAiResult] = useState(null); + const [copyState, setCopyState] = useState(null); + + const tryAiSanitize = async () => { + setAiState("loading"); + try { + const res = await fetch(`${API_URL}/api/ai-sanitize`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: originalUrl }), + }); + const data = await res.json(); + if (data.sanitizedUrl) { + setAiResult(data); + setAiState("success"); + } else { + setAiState("error"); + } + } catch { + setAiState("error"); + } + }; + + const handleCopy = () => { + if (!aiResult) return; + try { + navigator.clipboard.writeText(aiResult.sanitizedUrl); + setCopyState(true); + } catch { + setCopyState(false); + } + }; + return ( <> @@ -152,9 +200,60 @@ function ResultFailure(): React.ReactElement {

😢 Your link is not sanitized.

- We were unable to sanitize your link.
- Please submit a rule request on GitHub to help us improve. + We were unable to sanitize your link with our rules.

+ + {aiState === "idle" && ( + + )} + + {aiState === "loading" && ( +

+ AI is analyzing your URL... +

+ )} + + {aiState === "error" && ( +

AI sanitization failed.

+ )} + + {aiState === "success" && aiResult && ( +
+

+ AI suggestion (confidence: {Math.round(aiResult.confidence * 100)} + %): +

+
+ +
+ {copyState &&

Copied!

} + + {aiResult.removedParams.length > 0 && ( +

+ Removed: {aiResult.removedParams.join(", ")} +

+ )} + + {aiResult.suggestedRule && ( +
+

Suggested rule:

+
+                {JSON.stringify(aiResult.suggestedRule, null, 2)}
+              
+
+ )} +
+ )} ); } diff --git a/worker/src/index.ts b/worker/src/index.ts index 2f9eabf..6da6764 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -88,4 +88,115 @@ app.post("/api/bot/matrix", async (c) => { }); }); +const AI_TOOLS = [ + { + name: "sanitize_url", + description: + "Sanitize a URL by identifying and removing tracking parameters while keeping essential ones", + parameters: { + type: "object" as const, + properties: { + sanitized_url: { + type: "string", + description: "The sanitized URL with tracking parameters removed", + }, + removed_params: { + type: "array", + items: { type: "string" }, + description: "List of parameter names that were removed", + }, + kept_params: { + type: "array", + items: { type: "string" }, + description: "List of parameter names that were kept as essential", + }, + confidence: { + type: "number", + description: "Confidence score from 0 to 1", + }, + suggested_rule: { + type: "object", + description: "Suggested sanitization rule for this domain", + properties: { + pattern: { type: "string" }, + allowedParams: { type: "array", items: { type: "string" } }, + }, + }, + }, + required: [ + "sanitized_url", + "removed_params", + "kept_params", + "confidence", + "suggested_rule", + ], + }, + }, +]; + +app.post("/api/ai-sanitize", async (c) => { + const { url } = await c.req.json<{ url: string }>(); + + if (!url) { + return c.json({ error: "Missing url" }, 400); + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(url); + } catch { + return c.json({ error: "Invalid URL" }, 400); + } + + const params = Array.from(parsedUrl.searchParams.keys()); + if (params.length === 0) { + return c.json({ + sanitizedUrl: url, + confidence: 1, + removedParams: [], + keptParams: [], + suggestedRule: null, + }); + } + + const prompt = `Analyze this URL and identify tracking parameters vs essential parameters. + +URL: ${url} +Domain: ${parsedUrl.hostname} +Path: ${parsedUrl.pathname} +Parameters: ${params.join(", ")} + +Common tracking parameters include: utm_*, fbclid, gclid, ref, source, campaign, track*, share*, click*, etc. +Essential parameters are those needed for the page to work correctly (like id, v, p, page, q, etc). + +Call the sanitize_url function with your analysis.`; + + const response = await c.env.AI.run("@cf/meta/llama-3.1-8b-instruct", { + messages: [{ role: "user", content: prompt }], + tools: AI_TOOLS, + }); + + const toolCall = response.tool_calls?.[0]; + if (!toolCall || toolCall.name !== "sanitize_url") { + return c.json({ + sanitizedUrl: url, + confidence: 0, + removedParams: [], + keptParams: params, + suggestedRule: null, + error: "AI could not analyze URL", + }); + } + + const args = JSON.parse(toolCall.arguments); + + return c.json({ + sanitizedUrl: args.sanitized_url, + confidence: args.confidence, + removedParams: args.removed_params, + keptParams: args.kept_params, + suggestedRule: args.suggested_rule, + }); +}); + export default app; From adc467e28c43c1637e5586d86a07da0541217b08 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:27:33 +0000 Subject: [PATCH 05/18] feat: add browser rendering for parameter binary search - POST /api/browser-sanitize endpoint using CF browser rendering - Renders pages with/without params to find minimum required set - Compares title, OpenGraph data, and body length for similarity - Returns verified sanitized URL and suggested rule - Frontend offers browser render as alternative to AI --- pages/sanitize.tsx | 87 +++++++++++++++----------- worker/src/index.ts | 149 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 35 deletions(-) diff --git a/pages/sanitize.tsx b/pages/sanitize.tsx index 6171058..7e8c700 100644 --- a/pages/sanitize.tsx +++ b/pages/sanitize.tsx @@ -142,12 +142,12 @@ function ResultSuccess({ ); } -interface AiResult { +interface SanitizeResult { sanitizedUrl: string; - confidence: number; removedParams: string[]; - keptParams: string[]; suggestedRule: { pattern: string; allowedParams: string[] } | null; + confidence?: number; + requiredParams?: string[]; } function ResultFailure({ @@ -155,36 +155,40 @@ function ResultFailure({ }: { originalUrl: string; }): React.ReactElement { - const [aiState, setAiState] = useState< - "idle" | "loading" | "success" | "error" - >("idle"); - const [aiResult, setAiResult] = useState(null); + const [method, setMethod] = useState<"idle" | "ai" | "browser">("idle"); + const [state, setState] = useState<"idle" | "loading" | "success" | "error">( + "idle", + ); + const [result, setResult] = useState(null); const [copyState, setCopyState] = useState(null); - const tryAiSanitize = async () => { - setAiState("loading"); + const trySanitize = async (type: "ai" | "browser") => { + setMethod(type); + setState("loading"); try { - const res = await fetch(`${API_URL}/api/ai-sanitize`, { + const endpoint = + type === "ai" ? "/api/ai-sanitize" : "/api/browser-sanitize"; + const res = await fetch(`${API_URL}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: originalUrl }), }); const data = await res.json(); if (data.sanitizedUrl) { - setAiResult(data); - setAiState("success"); + setResult(data); + setState("success"); } else { - setAiState("error"); + setState("error"); } } catch { - setAiState("error"); + setState("error"); } }; const handleCopy = () => { - if (!aiResult) return; + if (!result) return; try { - navigator.clipboard.writeText(aiResult.sanitizedUrl); + navigator.clipboard.writeText(result.sanitizedUrl); setCopyState(true); } catch { setCopyState(false); @@ -203,52 +207,65 @@ function ResultFailure({ We were unable to sanitize your link with our rules.

- {aiState === "idle" && ( - + {state === "idle" && ( +
+ + +
)} - {aiState === "loading" && ( + {state === "loading" && (

- AI is analyzing your URL... + {method === "ai" + ? "AI is analyzing your URL..." + : "Browser is testing parameters..."}

)} - {aiState === "error" && ( -

AI sanitization failed.

+ {state === "error" && ( +

+ {method === "ai" ? "AI" : "Browser"} sanitization failed. +

)} - {aiState === "success" && aiResult && ( + {state === "success" && result && (

- AI suggestion (confidence: {Math.round(aiResult.confidence * 100)} - %): + {method === "ai" && result.confidence !== undefined + ? `AI suggestion (confidence: ${Math.round(result.confidence * 100)}%):` + : "Browser-verified suggestion:"}

{copyState &&

Copied!

} - {aiResult.removedParams.length > 0 && ( + {result.removedParams.length > 0 && (

- Removed: {aiResult.removedParams.join(", ")} + Removed: {result.removedParams.join(", ")}

)} - {aiResult.suggestedRule && ( + {result.suggestedRule && (

Suggested rule:

-                {JSON.stringify(aiResult.suggestedRule, null, 2)}
+                {JSON.stringify(result.suggestedRule, null, 2)}
               
)} diff --git a/worker/src/index.ts b/worker/src/index.ts index 6da6764..5c96d25 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -199,4 +199,153 @@ Call the sanitize_url function with your analysis.`; }); }); +async function renderPage( + browser: Fetcher, + url: string, +): Promise<{ + title: string; + ogData: Record; + bodyLength: number; +}> { + const res = await browser.fetch( + `https://browser-render.example.com/?url=${encodeURIComponent(url)}`, + { + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!res.ok) { + throw new Error(`Browser render failed: ${res.status}`); + } + + const html = await res.text(); + + const titleMatch = html.match(/]*>([^<]*)<\/title>/i); + const title = titleMatch?.[1] || ""; + + const ogData: Record = {}; + const ogMatches = html.matchAll( + /; bodyLength: number }, + b: { title: string; ogData: Record; bodyLength: number }, +): boolean { + if (a.title !== b.title) return false; + + if (a.ogData["title"] !== b.ogData["title"]) return false; + if (a.ogData["description"] !== b.ogData["description"]) return false; + if (a.ogData["image"] !== b.ogData["image"]) return false; + + const lengthRatio = + Math.min(a.bodyLength, b.bodyLength) / Math.max(a.bodyLength, b.bodyLength); + if (lengthRatio < 0.8) return false; + + return true; +} + +app.post("/api/browser-sanitize", async (c) => { + const { url } = await c.req.json<{ url: string }>(); + + if (!url) { + return c.json({ error: "Missing url" }, 400); + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(url); + } catch { + return c.json({ error: "Invalid URL" }, 400); + } + + const allParams = Array.from(parsedUrl.searchParams.entries()); + if (allParams.length === 0) { + return c.json({ + sanitizedUrl: url, + requiredParams: [], + removedParams: [], + suggestedRule: null, + }); + } + + try { + const originalPage = await renderPage(c.env.BROWSER, url); + + const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`; + const noParamsPage = await renderPage(c.env.BROWSER, baseUrl); + + if (arePagesSimlar(originalPage, noParamsPage)) { + return c.json({ + sanitizedUrl: baseUrl, + requiredParams: [], + removedParams: allParams.map(([k]) => k), + suggestedRule: { + pattern: `^${parsedUrl.pathname.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, + allowedParams: [], + }, + }); + } + + const requiredParams: string[] = []; + + for (const [key, value] of allParams) { + const testUrl = new URL(baseUrl); + for (const p of requiredParams) { + const pValue = parsedUrl.searchParams.get(p); + if (pValue) testUrl.searchParams.set(p, pValue); + } + testUrl.searchParams.set(key, value); + + const testPage = await renderPage(c.env.BROWSER, testUrl.toString()); + + const urlWithoutThisParam = new URL(baseUrl); + for (const p of requiredParams) { + const pValue = parsedUrl.searchParams.get(p); + if (pValue) urlWithoutThisParam.searchParams.set(p, pValue); + } + const pageWithoutThisParam = await renderPage( + c.env.BROWSER, + urlWithoutThisParam.toString(), + ); + + if (!arePagesSimlar(originalPage, pageWithoutThisParam)) { + requiredParams.push(key); + } + } + + const sanitizedUrl = new URL(baseUrl); + for (const p of requiredParams) { + const pValue = parsedUrl.searchParams.get(p); + if (pValue) sanitizedUrl.searchParams.set(p, pValue); + } + + return c.json({ + sanitizedUrl: sanitizedUrl.toString(), + requiredParams, + removedParams: allParams + .filter(([k]) => !requiredParams.includes(k)) + .map(([k]) => k), + suggestedRule: { + pattern: `^${parsedUrl.pathname.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, + allowedParams: requiredParams, + }, + }); + } catch (error) { + return c.json( + { + error: "Browser rendering failed", + details: error instanceof Error ? error.message : "Unknown error", + }, + 500, + ); + } +}); + export default app; From 3ffff83c0465e73ff386d3ba97c063794b581599 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:28:43 +0000 Subject: [PATCH 06/18] feat: add GitHub issue submission for rule contribution - Submit rule button after AI/browser sanitization success - Prefills GitHub issue with URL, sanitized URL, and YAML rule - Uses rule-request label for easy filtering --- pages/sanitize.tsx | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pages/sanitize.tsx b/pages/sanitize.tsx index 7e8c700..870e1ec 100644 --- a/pages/sanitize.tsx +++ b/pages/sanitize.tsx @@ -267,6 +267,14 @@ function ResultFailure({
                 {JSON.stringify(result.suggestedRule, null, 2)}
               
+ + Submit Rule to GitHub +
)} @@ -274,3 +282,37 @@ function ResultFailure({ ); } + +function buildGitHubIssueUrl( + originalUrl: string, + result: SanitizeResult, +): string { + const domain = new URL(originalUrl).hostname; + const title = `Rule request: ${domain}`; + + const body = `## URL +\`${originalUrl}\` + +## Suggested Sanitized URL +\`${result.sanitizedUrl}\` + +## Removed Parameters +${result.removedParams.length > 0 ? result.removedParams.map((p) => `- \`${p}\``).join("\n") : "None"} + +## Suggested Rule +\`\`\`yaml +${domain}: + sanitize: + - pattern: "${result.suggestedRule?.pattern || "^/.*$"}" + allowedParams: [${result.suggestedRule?.allowedParams.map((p) => `"${p}"`).join(", ") || ""}] +\`\`\` +`; + + const params = new URLSearchParams({ + title, + body, + labels: "rule-request", + }); + + return `https://github.com/Tail-WTF/Website/issues/new?${params}`; +} From ec142ece6aec8028828da776ca69ecea2833e16e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:37:22 +0000 Subject: [PATCH 07/18] fix: improve browser sanitization and GitHub issue privacy - Remove original URL from GitHub issue submission, only include domain - Use screenshot-based page similarity comparison for accurate matching - Update page rendering to capture screenshots instead of DOM analysis - Add Cloudflare Puppeteer dependency for browser rendering --- pages/sanitize.tsx | 18 +- worker/package-lock.json | 946 ++++++++++++++++++++++++++++++++++++++- worker/package.json | 1 + worker/src/index.ts | 121 +++-- 4 files changed, 1034 insertions(+), 52 deletions(-) diff --git a/pages/sanitize.tsx b/pages/sanitize.tsx index 870e1ec..72bdc53 100644 --- a/pages/sanitize.tsx +++ b/pages/sanitize.tsx @@ -268,7 +268,10 @@ function ResultFailure({ {JSON.stringify(result.suggestedRule, null, 2)} 0 ? result.removedParams.map((p) => `- \`${p}\``).join("\n") : "None"} diff --git a/worker/package-lock.json b/worker/package-lock.json index b0e559d..46b0b94 100644 --- a/worker/package-lock.json +++ b/worker/package-lock.json @@ -13,6 +13,7 @@ "linkifyjs": "^4.3.2" }, "devDependencies": { + "@cloudflare/puppeteer": "^1.0.4", "@cloudflare/workers-types": "^4.20250520.0", "@types/js-yaml": "^4.0.5", "vitest": "^3.2.4", @@ -32,6 +33,22 @@ "node": ">=18.0.0" } }, + "node_modules/@cloudflare/puppeteer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cloudflare/puppeteer/-/puppeteer-1.0.4.tgz", + "integrity": "sha512-I7vh9Er1hQ/7OnOL/NEPaiR0alRQ62wEWstyizilmr69AcRqvu+ufNlTc3A2uXREEvFBZ6TFajdcrBbh78/mtA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.2.4", + "debug": "^4.3.5", + "devtools-protocol": "0.0.1299070", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@cloudflare/unenv-preset": { "version": "2.7.13", "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz", @@ -1043,6 +1060,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@puppeteer/browsers": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.4.tgz", + "integrity": "sha512-BdG2qiI1dn89OTUUsx2GZSpUzW+DRffR1wlMJyKxVHYrhnKoELSDxDd+2XImUkuWPEKk76H5FcM/gPFrEK1Tfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.2", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", @@ -1371,6 +1411,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1403,6 +1450,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -1541,6 +1610,42 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1557,6 +1662,162 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz", + "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1564,6 +1825,41 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1601,6 +1897,21 @@ "node": ">= 16" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1660,6 +1971,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1688,6 +2009,21 @@ "node": ">=6" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1698,6 +2034,30 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1299070", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz", + "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -1757,6 +2117,62 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1767,6 +2183,26 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/exit-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", @@ -1790,6 +2226,44 @@ "node": ">=12.0.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1823,6 +2297,47 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -1839,6 +2354,65 @@ "node": ">=16.9.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-arrayish": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", @@ -1846,6 +2420,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1888,6 +2472,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1964,6 +2558,60 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -1988,6 +2636,13 @@ "node": ">= 14.16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2037,6 +2692,64 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", @@ -2149,6 +2862,58 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2184,6 +2949,46 @@ "npm": ">=6" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -2210,6 +3015,50 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2276,8 +3125,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } }, "node_modules/undici": { "version": "7.14.0", @@ -2289,6 +3148,14 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/unenv": { "version": "2.0.0-rc.24", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", @@ -3027,6 +3894,31 @@ "@esbuild/win32-x64": "0.27.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -3049,6 +3941,56 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/worker/package.json b/worker/package.json index e809121..c04ff1c 100644 --- a/worker/package.json +++ b/worker/package.json @@ -14,6 +14,7 @@ "linkifyjs": "^4.3.2" }, "devDependencies": { + "@cloudflare/puppeteer": "^1.0.4", "@cloudflare/workers-types": "^4.20250520.0", "@types/js-yaml": "^4.0.5", "vitest": "^3.2.4", diff --git a/worker/src/index.ts b/worker/src/index.ts index 5c96d25..9f02965 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1,5 +1,6 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; +import puppeteer from "@cloudflare/puppeteer"; import { sanitizeLinkInText } from "./sanitizer"; import type { Env, RuleSet } from "./types"; @@ -34,12 +35,55 @@ interface TelegramUpdate { text?: string; message_id: number; }; + inline_query?: { + id: string; + query: string; + }; } app.post("/api/bot/telegram", async (c) => { const update = await c.req.json(); - const text = update.message?.text; + if (update.inline_query) { + const query = update.inline_query.query; + if (!query) { + return c.json({ + method: "answerInlineQuery", + inline_query_id: update.inline_query.id, + results: [], + }); + } + + const rules = await loadRules(c.env); + const sanitized = await sanitizeLinkInText(query, rules, 1); + + if (sanitized.links.length === 0) { + return c.json({ + method: "answerInlineQuery", + inline_query_id: update.inline_query.id, + results: [], + }); + } + + return c.json({ + method: "answerInlineQuery", + inline_query_id: update.inline_query.id, + results: [ + { + type: "article", + id: "1", + title: "Sanitized Link", + description: sanitized.links[0], + input_message_content: { + message_text: sanitized.text, + }, + }, + ], + cache_time: 300, + }); + } + + const text = update.message?.text; if (!text) { return c.json({ ok: true }); } @@ -200,55 +244,54 @@ Call the sanitize_url function with your analysis.`; }); async function renderPage( - browser: Fetcher, + browserBinding: Fetcher, url: string, ): Promise<{ - title: string; - ogData: Record; - bodyLength: number; + screenshot: Uint8Array; }> { - const res = await browser.fetch( - `https://browser-render.example.com/?url=${encodeURIComponent(url)}`, - { - headers: { "Content-Type": "application/json" }, - }, - ); - - if (!res.ok) { - throw new Error(`Browser render failed: ${res.status}`); - } - - const html = await res.text(); - - const titleMatch = html.match(/]*>([^<]*)<\/title>/i); - const title = titleMatch?.[1] || ""; - - const ogData: Record = {}; - const ogMatches = html.matchAll( - /; bodyLength: number }, - b: { title: string; ogData: Record; bodyLength: number }, + a: { screenshot: Uint8Array }, + b: { screenshot: Uint8Array }, + threshold = 0.95, ): boolean { - if (a.title !== b.title) return false; + // Compare screenshot bytes - if sizes differ significantly, pages are different + if ( + Math.abs(a.screenshot.length - b.screenshot.length) / + Math.max(a.screenshot.length, b.screenshot.length) > + 0.1 + ) { + return false; + } - if (a.ogData["title"] !== b.ogData["title"]) return false; - if (a.ogData["description"] !== b.ogData["description"]) return false; - if (a.ogData["image"] !== b.ogData["image"]) return false; + // Compare byte-by-byte similarity + const minLength = Math.min(a.screenshot.length, b.screenshot.length); + let matchingBytes = 0; - const lengthRatio = - Math.min(a.bodyLength, b.bodyLength) / Math.max(a.bodyLength, b.bodyLength); - if (lengthRatio < 0.8) return false; + for (let i = 0; i < minLength; i++) { + if (a.screenshot[i] === b.screenshot[i]) { + matchingBytes++; + } + } - return true; + const similarity = matchingBytes / Math.max(a.screenshot.length, b.screenshot.length); + return similarity >= threshold; } app.post("/api/browser-sanitize", async (c) => { From d4a9b29ca017d239c663817d2a81223e740a9c21 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 11:46:20 +0000 Subject: [PATCH 08/18] feat: use AI vision for page similarity comparison - Add multi-turn vision comparison using llama-3.2-11b-vision-instruct - First turn: describe screenshot A - Second turn: compare description with screenshot B - Keep quick heuristic check (title/OG metadata) as fast path - Extract title and OG metadata during page render for heuristics --- worker/src/index.ts | 158 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 28 deletions(-) diff --git a/worker/src/index.ts b/worker/src/index.ts index 9f02965..4dd81ca 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -243,12 +243,17 @@ Call the sanitize_url function with your analysis.`; }); }); +interface PageRender { + screenshot: Uint8Array; + title: string; + ogTitle: string; + ogDescription: string; +} + async function renderPage( browserBinding: Fetcher, url: string, -): Promise<{ - screenshot: Uint8Array; -}> { +): Promise { const browser = await puppeteer.launch(browserBinding); try { const page = await browser.newPage(); @@ -260,38 +265,137 @@ async function renderPage( fullPage: false, })) as Uint8Array; - return { screenshot }; + const title = await page.title(); + + const ogTitle = await page + .$eval('meta[property="og:title"]', (el) => el.getAttribute("content")) + .catch(() => null); + + const ogDescription = await page + .$eval('meta[property="og:description"]', (el) => + el.getAttribute("content"), + ) + .catch(() => null); + + return { + screenshot, + title, + ogTitle: ogTitle || "", + ogDescription: ogDescription || "", + }; } finally { await browser.close(); } } -function arePagesSimlar( - a: { screenshot: Uint8Array }, - b: { screenshot: Uint8Array }, - threshold = 0.95, -): boolean { - // Compare screenshot bytes - if sizes differ significantly, pages are different - if ( - Math.abs(a.screenshot.length - b.screenshot.length) / - Math.max(a.screenshot.length, b.screenshot.length) > - 0.1 - ) { +function quickHeuristicCheck(a: PageRender, b: PageRender): boolean | null { + // Quick check: if titles are completely different, pages are different + if (a.title && b.title && a.title !== b.title) { + // Check if difference is just tracking-related suffix + const normalize = (t: string) => + t.replace(/\s*[-|·]\s*.*$/, "").trim().toLowerCase(); + if (normalize(a.title) !== normalize(b.title)) { + return false; + } + } + + // If OG metadata differs significantly, pages are likely different + if (a.ogTitle && b.ogTitle && a.ogTitle !== b.ogTitle) { return false; } - // Compare byte-by-byte similarity - const minLength = Math.min(a.screenshot.length, b.screenshot.length); - let matchingBytes = 0; + // Can't determine from heuristics alone + return null; +} - for (let i = 0; i < minLength; i++) { - if (a.screenshot[i] === b.screenshot[i]) { - matchingBytes++; - } +function arrayBufferToBase64(buffer: Uint8Array): string { + let binary = ""; + for (let i = 0; i < buffer.length; i++) { + binary += String.fromCharCode(buffer[i]); + } + return btoa(binary); +} + +async function compareWithVision( + ai: Ai, + a: PageRender, + b: PageRender, +): Promise { + const imageA = arrayBufferToBase64(a.screenshot); + const imageB = arrayBufferToBase64(b.screenshot); + + // First turn: describe the first image + const describeA = await ai.run("@cf/meta/llama-3.2-11b-vision-instruct", { + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Describe the main content of this webpage screenshot in 2-3 sentences. Focus on the primary content, layout, and any key elements visible.", + }, + { + type: "image", + image: imageA, + }, + ], + }, + ], + }); + + const descriptionA = + typeof describeA === "object" && "response" in describeA + ? (describeA as { response: string }).response + : String(describeA); + + // Second turn: compare with the second image + const comparison = await ai.run("@cf/meta/llama-3.2-11b-vision-instruct", { + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: `I have two webpage screenshots. The first one shows: "${descriptionA}" + +Now look at this second screenshot. Are these two pages showing the SAME content (same article, same product, same page)? +Ignore minor differences like: +- Different ads or promotional banners +- Slight layout variations +- Cookie consent popups + +Answer with just "SAME" if they show the same main content, or "DIFFERENT" if they show different content.`, + }, + { + type: "image", + image: imageB, + }, + ], + }, + ], + }); + + const result = + typeof comparison === "object" && "response" in comparison + ? (comparison as { response: string }).response + : String(comparison); + + return result.toUpperCase().includes("SAME"); +} + +async function arePagesSimlar( + ai: Ai, + a: PageRender, + b: PageRender, +): Promise { + // Step 1: Quick heuristic check + const heuristicResult = quickHeuristicCheck(a, b); + if (heuristicResult !== null) { + return heuristicResult; } - const similarity = matchingBytes / Math.max(a.screenshot.length, b.screenshot.length); - return similarity >= threshold; + // Step 2: Use AI vision for accurate comparison + return compareWithVision(ai, a, b); } app.post("/api/browser-sanitize", async (c) => { @@ -324,7 +428,7 @@ app.post("/api/browser-sanitize", async (c) => { const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`; const noParamsPage = await renderPage(c.env.BROWSER, baseUrl); - if (arePagesSimlar(originalPage, noParamsPage)) { + if (await arePagesSimlar(c.env.AI, originalPage, noParamsPage)) { return c.json({ sanitizedUrl: baseUrl, requiredParams: [], @@ -346,8 +450,6 @@ app.post("/api/browser-sanitize", async (c) => { } testUrl.searchParams.set(key, value); - const testPage = await renderPage(c.env.BROWSER, testUrl.toString()); - const urlWithoutThisParam = new URL(baseUrl); for (const p of requiredParams) { const pValue = parsedUrl.searchParams.get(p); @@ -358,7 +460,7 @@ app.post("/api/browser-sanitize", async (c) => { urlWithoutThisParam.toString(), ); - if (!arePagesSimlar(originalPage, pageWithoutThisParam)) { + if (!(await arePagesSimlar(c.env.AI, originalPage, pageWithoutThisParam))) { requiredParams.push(key); } } From d02a8640fd86d0e504e78decc4eb9db63e1e2b1b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 12:11:36 +0000 Subject: [PATCH 09/18] feat: rewrite frontend from Next.js to SvelteKit - Replace React/Next.js with Svelte 5 and SvelteKit - Use static adapter for output to /out directory - Migrate all pages: index, sanitize, 404 - Migrate all components: Layout, Footer, H1, H2, LinkBox, ActionBtn - Keep exact same UI design and Tailwind styles - Update ESLint and Prettier configs for Svelte - Fix GitHub issue URL to point to /Rules repo --- .gitignore | 4 +- .prettierignore | 5 +- components/footer.tsx | 41 - components/form.tsx | 30 - components/headings.tsx | 33 - components/layout.tsx | 34 - eslint.config.mjs | 41 +- next-env.d.ts | 6 - next.config.ts | 12 - package-lock.json | 6926 +++++++---------------- package.json | 30 +- pages/404.tsx | 23 - pages/_app.tsx | 6 - pages/_document.tsx | 18 - pages/index.tsx | 121 - pages/sanitize.tsx | 314 - postcss.config.mjs | 7 - prettier.config.mjs | 3 +- styles/globals.css => src/app.css | 0 src/app.html | 14 + src/lib/components/ActionBtn.svelte | 18 + src/lib/components/Footer.svelte | 28 + src/lib/components/H1.svelte | 15 + src/lib/components/H2.svelte | 14 + src/lib/components/Layout.svelte | 20 + src/lib/components/LinkBox.svelte | 20 + src/lib/components/ResultFailure.svelte | 172 + src/lib/components/ResultSuccess.svelte | 66 + src/routes/+error.svelte | 18 + src/routes/+layout.svelte | 7 + src/routes/+page.svelte | 131 + src/routes/sanitize/+page.svelte | 78 + svelte.config.js | 21 + tsconfig.json | 21 +- vite.config.ts | 7 + 35 files changed, 2684 insertions(+), 5620 deletions(-) delete mode 100644 components/footer.tsx delete mode 100644 components/form.tsx delete mode 100644 components/headings.tsx delete mode 100644 components/layout.tsx delete mode 100755 next-env.d.ts delete mode 100644 next.config.ts delete mode 100644 pages/404.tsx delete mode 100755 pages/_app.tsx delete mode 100644 pages/_document.tsx delete mode 100755 pages/index.tsx delete mode 100644 pages/sanitize.tsx delete mode 100644 postcss.config.mjs rename styles/globals.css => src/app.css (100%) mode change 100755 => 100644 create mode 100644 src/app.html create mode 100644 src/lib/components/ActionBtn.svelte create mode 100644 src/lib/components/Footer.svelte create mode 100644 src/lib/components/H1.svelte create mode 100644 src/lib/components/H2.svelte create mode 100644 src/lib/components/Layout.svelte create mode 100644 src/lib/components/LinkBox.svelte create mode 100644 src/lib/components/ResultFailure.svelte create mode 100644 src/lib/components/ResultSuccess.svelte create mode 100644 src/routes/+error.svelte create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/sanitize/+page.svelte create mode 100644 svelte.config.js create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index 30dfde7..f74180c 100755 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,9 @@ # testing /coverage -# next.js +# sveltekit +/.svelte-kit/ /.next/ -/.swc/ /out/ # production diff --git a/.prettierignore b/.prettierignore index bb56672..41ac71a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,4 @@ -/.next/ +/.svelte-kit/ +/build/ +/out/ +/node_modules/ diff --git a/components/footer.tsx b/components/footer.tsx deleted file mode 100644 index 72153bb..0000000 --- a/components/footer.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import Link from "next/link"; - -export default function Footer() { - return ( - - ); -} - -function DesktopNavItem({ - href, - children, -}: { - href: string; - children: React.ReactNode; -}) { - return ( -
  • - {children} -
  • - ); -} diff --git a/components/form.tsx b/components/form.tsx deleted file mode 100644 index f820598..0000000 --- a/components/form.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ButtonHTMLAttributes, InputHTMLAttributes } from "react"; - -/** - * The main input component used in many pages to hold URL input/output. - */ -export function LinkBox(props: InputHTMLAttributes) { - return ( - - ); -} - -/** - * The main button component used for the primary action on a page. - */ -export function ActionBtn(props: ButtonHTMLAttributes) { - return ( - - - - )} - - {state === "loading" && ( -

    - {method === "ai" - ? "AI is analyzing your URL..." - : "Browser is testing parameters..."} -

    - )} - - {state === "error" && ( -

    - {method === "ai" ? "AI" : "Browser"} sanitization failed. -

    - )} - - {state === "success" && result && ( -
    -

    - {method === "ai" && result.confidence !== undefined - ? `AI suggestion (confidence: ${Math.round(result.confidence * 100)}%):` - : "Browser-verified suggestion:"} -

    -
    - -
    - {copyState &&

    Copied!

    } - - {result.removedParams.length > 0 && ( -

    - Removed: {result.removedParams.join(", ")} -

    - )} - - {result.suggestedRule && ( -
    -

    Suggested rule:

    -
    -                {JSON.stringify(result.suggestedRule, null, 2)}
    -              
    - - Submit Rule to GitHub - -
    - )} -
    - )} - - ); -} - -function buildGitHubIssueUrl(domain: string, result: SanitizeResult): string { - const title = `Rule request: ${domain}`; - - const body = `## Domain -\`${domain}\` - -## Removed Parameters -${result.removedParams.length > 0 ? result.removedParams.map((p) => `- \`${p}\``).join("\n") : "None"} - -## Suggested Rule -\`\`\`yaml -${domain}: - sanitize: - - pattern: "${result.suggestedRule?.pattern || "^/.*$"}" - allowedParams: [${result.suggestedRule?.allowedParams.map((p) => `"${p}"`).join(", ") || ""}] -\`\`\` -`; - - const params = new URLSearchParams({ - title, - body, - labels: "rule-request", - }); - - return `https://github.com/Tail-WTF/Website/issues/new?${params}`; -} diff --git a/postcss.config.mjs b/postcss.config.mjs deleted file mode 100644 index 61e3684..0000000 --- a/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/prettier.config.mjs b/prettier.config.mjs index 9441d15..9ca9b44 100644 --- a/prettier.config.mjs +++ b/prettier.config.mjs @@ -1,6 +1,7 @@ // prettier.config.mjs const config = { - plugins: ["prettier-plugin-tailwindcss"], + plugins: ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + overrides: [{ files: "*.svelte", options: { parser: "svelte" } }], }; export default config; diff --git a/styles/globals.css b/src/app.css old mode 100755 new mode 100644 similarity index 100% rename from styles/globals.css rename to src/app.css diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..3691d62 --- /dev/null +++ b/src/app.html @@ -0,0 +1,14 @@ + + + + + + + + + %sveltekit.head% + + +
    %sveltekit.body%
    + + diff --git a/src/lib/components/ActionBtn.svelte b/src/lib/components/ActionBtn.svelte new file mode 100644 index 0000000..f9d7da0 --- /dev/null +++ b/src/lib/components/ActionBtn.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte new file mode 100644 index 0000000..160a588 --- /dev/null +++ b/src/lib/components/Footer.svelte @@ -0,0 +1,28 @@ + + + diff --git a/src/lib/components/H1.svelte b/src/lib/components/H1.svelte new file mode 100644 index 0000000..b2640ad --- /dev/null +++ b/src/lib/components/H1.svelte @@ -0,0 +1,15 @@ + + +

    + {@render children()} +

    diff --git a/src/lib/components/H2.svelte b/src/lib/components/H2.svelte new file mode 100644 index 0000000..443ba32 --- /dev/null +++ b/src/lib/components/H2.svelte @@ -0,0 +1,14 @@ + + +

    + {@render children()} +

    diff --git a/src/lib/components/Layout.svelte b/src/lib/components/Layout.svelte new file mode 100644 index 0000000..20b5c8f --- /dev/null +++ b/src/lib/components/Layout.svelte @@ -0,0 +1,20 @@ + + +
    +
    + {@render children()} + +
    +
    +
    diff --git a/src/lib/components/LinkBox.svelte b/src/lib/components/LinkBox.svelte new file mode 100644 index 0000000..670b22a --- /dev/null +++ b/src/lib/components/LinkBox.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ResultFailure.svelte b/src/lib/components/ResultFailure.svelte new file mode 100644 index 0000000..ed144b0 --- /dev/null +++ b/src/lib/components/ResultFailure.svelte @@ -0,0 +1,172 @@ + + + +
    +

    😢 Your link is not sanitized.

    +
    +

    + We were unable to sanitize your link with our rules. +

    + + {#if fetchState === "idle"} +
    + + +
    + {/if} + + {#if fetchState === "loading"} +

    + {method === "ai" + ? "AI is analyzing your URL..." + : "Browser is testing parameters..."} +

    + {/if} + + {#if fetchState === "error"} +

    + {method === "ai" ? "AI" : "Browser"} sanitization failed. +

    + {/if} + + {#if fetchState === "success" && result} +
    +

    + {method === "ai" && result.confidence !== undefined + ? `AI suggestion (confidence: ${Math.round(result.confidence * 100)}%):` + : "Browser-verified suggestion:"} +

    +
    + +
    + {#if copyState} +

    Copied!

    + {/if} + + {#if result.removedParams.length > 0} +

    + Removed: {result.removedParams.join(", ")} +

    + {/if} + + {#if result.suggestedRule} +
    +

    Suggested rule:

    +
    +            {JSON.stringify(result.suggestedRule, null, 2)}
    +          
    + + Submit Rule to GitHub + +
    + {/if} +
    + {/if} +
    diff --git a/src/lib/components/ResultSuccess.svelte b/src/lib/components/ResultSuccess.svelte new file mode 100644 index 0000000..a63b9ce --- /dev/null +++ b/src/lib/components/ResultSuccess.svelte @@ -0,0 +1,66 @@ + + + +
    +

    + 🎉 Your link is now sanitized! Click to copy it. +

    +
    + + + {#if copyState !== null} +

    + {copyState ? "Copied!" : "Unable to copy link. Please try manually."} +

    + {/if} +
    diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..bbe1080 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,18 @@ + + + + Page Not Found - Tail.WTF + + + +

    {page.status} - Page Not Found

    +

    We cannot find the page you are looking for :-(

    + + <- Back to home page + +
    diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..801be90 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..8d70ac3 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,131 @@ + + + + Link Sanitizer - Tail.WTF + + + +
    +
    +

    Link Sanitizer

    +

    Chop trackers from shared link

    +
    + +
    +
    + + + + Sanitize + + +
    + +
    ↓ Why? ↓
    +
    + +
    +

    Why sanitize links?

    +
    +

    https://example.com/foo

    +
    +

    + ?share_source=copy_web +
    + & + track=12345abcdef67890 +

    +

    + "link generated on webpage" +
    + "and shared by user at date... " +

    +
    +
    +

    + Because online services want to monitor your digital life! When you tap + share buttons, innocent-looking random strings are placed in the links you + get - they are actually + "trackers" + tied to your account. Every time your friends open these links, service providers + learn more about your social connections such as how often you talk to a particular + friend, topics you discuss, etc. They can further exploit this information + in conjunction with other data they have about you and your friends. +
    +
    + Tail.WTF protects your privacy by chopping the annoying + "tails" + off these links, and also makes them look succinct and nicer. +

    +

    Privacy

    +

    + As a service that aims to protect your privacy, we do not collect any + information from you without your explicit consent. When we fail to + sanitize specific link(s), we may ask if you would like to share info + about them to improve this service. +
    +
    + This site is proxied through and powered by + Cloudflare. We use Cloudflare Workers for our API, Workers AI for intelligent URL + analysis, and Browser Rendering for verifying page content. Your requests + are processed through Cloudflare's infrastructure. For details on how + Cloudflare handles data, please see + Cloudflare's Privacy Policy. +

    +
    +
    diff --git a/src/routes/sanitize/+page.svelte b/src/routes/sanitize/+page.svelte new file mode 100644 index 0000000..3658f7c --- /dev/null +++ b/src/routes/sanitize/+page.svelte @@ -0,0 +1,78 @@ + + + + {#if state.status === "success"} + Result - Tail.WTF + {:else if state.status === "error"} + Rule Missing - Tail.WTF + {:else} + Sanitizing... - Tail.WTF + {/if} + + +{#if !url || state.status === "loading"} + +
    +

    Sanitizing...

    +
    +
    +
    +
    +
    +{:else if state.status === "success"} + +{:else} + +{/if} diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..0f81290 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,21 @@ +import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + pages: "out", + assets: "out", + fallback: "404.html", + precompress: false, + strict: true, + }), + prerender: { + entries: ["/", "/sanitize"], + }, + }, +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json index 95d4f5e..7d1a7c8 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,15 @@ { + "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": false, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, + "allowJs": true, + "checkJs": true, "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", + "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react-jsx", - "incremental": true + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules", "worker"] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e848adb --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], +}); From d9d1ae1af47802827a0d6849c5d3a088b5058afc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 12:21:12 +0000 Subject: [PATCH 10/18] test: add Playwright e2e tests with mocked API - Add Playwright configuration for e2e testing - Add home page tests (title, heading, form, footer) - Add sanitize page tests with mocked API responses - Add navigation tests (footer links, 404 page) - Extend GitHub Actions workflow with e2e test job - Test both success and failure API scenarios --- .github/workflows/verify.yml | 33 + e2e/home.spec.ts | 71 +++ e2e/navigation.spec.ts | 36 ++ e2e/sanitize.spec.ts | 220 +++++++ eslint.config.mjs | 3 + package-lock.json | 799 +++++++++++++++--------- package.json | 5 +- playwright.config.ts | 25 + src/lib/components/ResultFailure.svelte | 5 +- src/routes/+page.svelte | 8 +- worker/src/index.ts | 9 +- 11 files changed, 912 insertions(+), 302 deletions(-) create mode 100644 e2e/home.spec.ts create mode 100644 e2e/navigation.spec.ts create mode 100644 e2e/sanitize.spec.ts create mode 100644 playwright.config.ts diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index bcca0b5..21551b8 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -51,3 +51,36 @@ jobs: - name: Lint run: npm run lint + + test-e2e: + name: E2E Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + + - name: Build + run: npm run build + + - name: Run E2E tests + run: npm run test:e2e + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts new file mode 100644 index 0000000..07480eb --- /dev/null +++ b/e2e/home.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Home Page", () => { + test("has correct title", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle("Link Sanitizer - Tail.WTF"); + }); + + test("displays main heading", async ({ page }) => { + await page.goto("/"); + await expect( + page.getByRole("heading", { name: "Link Sanitizer" }), + ).toBeVisible(); + }); + + test("displays subtitle", async ({ page }) => { + await page.goto("/"); + await expect( + page.getByText("Chop trackers from shared link"), + ).toBeVisible(); + }); + + test("has URL input field", async ({ page }) => { + await page.goto("/"); + const input = page.getByPlaceholder("-> Paste your link here <-"); + await expect(input).toBeVisible(); + await expect(input).toHaveAttribute("name", "url"); + }); + + test("has sanitize button", async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("button", { name: "Sanitize" })).toBeVisible(); + }); + + test("navigates to sanitize page on form submit", async ({ page }) => { + await page.goto("/"); + + const input = page.getByPlaceholder("-> Paste your link here <-"); + await input.fill("https://example.com/?tracking=123"); + + await page.getByRole("button", { name: "Sanitize" }).click(); + + await expect(page).toHaveURL(/\/sanitize\?url=/); + }); + + test("displays About section", async ({ page }) => { + await page.goto("/"); + await expect( + page.getByRole("heading", { name: "Why sanitize links?" }), + ).toBeVisible(); + }); + + test("displays Privacy section with Cloudflare notice", async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("heading", { name: "Privacy" })).toBeVisible(); + await expect(page.getByText("Cloudflare")).toBeVisible(); + await expect( + page.getByRole("link", { name: "Cloudflare's Privacy Policy" }), + ).toHaveAttribute("href", "https://www.cloudflare.com/privacypolicy/"); + }); + + test("has footer with GitHub link", async ({ page }) => { + await page.goto("/"); + const githubLink = page.getByRole("link", { name: "GitHub" }); + await expect(githubLink).toBeVisible(); + await expect(githubLink).toHaveAttribute( + "href", + "https://github.com/Tail-WTF/Website", + ); + }); +}); diff --git a/e2e/navigation.spec.ts b/e2e/navigation.spec.ts new file mode 100644 index 0000000..a2e82fb --- /dev/null +++ b/e2e/navigation.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Navigation", () => { + test("footer links work correctly", async ({ page }) => { + await page.goto("/"); + + // Check About link scrolls to section + await page.getByRole("link", { name: "About" }).click(); + await expect(page).toHaveURL("/#about"); + + // Check Privacy link scrolls to section + await page.getByRole("link", { name: "Privacy" }).click(); + await expect(page).toHaveURL("/#privacy"); + + // Check Tail.WTF link goes to home + await page.goto("/sanitize?url=test"); + await page.getByRole("link", { name: "Tail.WTF" }).click(); + await expect(page).toHaveURL("/"); + }); + + test("404 page displays correctly", async ({ page }) => { + await page.goto("/nonexistent-page"); + + await expect(page.getByText("Page Not Found")).toBeVisible(); + await expect( + page.getByRole("link", { name: "<- Back to home page" }), + ).toBeVisible(); + }); + + test("404 page back link works", async ({ page }) => { + await page.goto("/nonexistent-page"); + + await page.getByRole("link", { name: "<- Back to home page" }).click(); + await expect(page).toHaveURL("/"); + }); +}); diff --git a/e2e/sanitize.spec.ts b/e2e/sanitize.spec.ts new file mode 100644 index 0000000..d793936 --- /dev/null +++ b/e2e/sanitize.spec.ts @@ -0,0 +1,220 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Sanitize Page", () => { + test.describe("with mocked successful API", () => { + test.beforeEach(async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + text: "https://example.com/page", + sanitizedURLs: ["https://example.com/page"], + }), + }); + }); + }); + + test("shows loading state initially", async ({ page }) => { + // Delay the API response to see loading state + await page.route("**/api/sanitize*", async (route) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + text: "https://example.com/page", + sanitizedURLs: ["https://example.com/page"], + }), + }); + }); + + await page.goto("/sanitize?url=https://example.com/page?tracking=123"); + await expect(page.getByText("Sanitizing...")).toBeVisible(); + }); + + test("displays sanitized URL on success", async ({ page }) => { + await page.goto("/sanitize?url=https://example.com/page?tracking=123"); + + await expect(page.getByText("Your link is now sanitized!")).toBeVisible(); + await expect(page.getByRole("textbox")).toHaveValue( + "https://example.com/page", + ); + }); + + test("has link to open sanitized URL", async ({ page }) => { + await page.goto("/sanitize?url=https://example.com/page?tracking=123"); + + await expect(page.getByRole("textbox")).toBeVisible(); + const openLink = page.getByRole("link", { name: "Open Sanitized Link" }); + await expect(openLink).toHaveAttribute( + "href", + "https://example.com/page", + ); + }); + }); + + test.describe("with mocked failed API", () => { + test.beforeEach(async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + text: "https://example.com/?unknown=param", + sanitizedURLs: [], + }), + }); + }); + }); + + test("shows error state when no sanitization rules match", async ({ + page, + }) => { + await page.goto("/sanitize?url=https://example.com/?unknown=param"); + + await expect(page.getByText("Your link is not sanitized.")).toBeVisible(); + await expect( + page.getByText("We were unable to sanitize your link"), + ).toBeVisible(); + }); + + test("shows Try AI and Try Browser Render buttons", async ({ page }) => { + await page.goto("/sanitize?url=https://example.com/?unknown=param"); + + await expect(page.getByRole("button", { name: "Try AI" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Try Browser Render" }), + ).toBeVisible(); + }); + + test("Try AI button triggers AI sanitization", async ({ page }) => { + await page.route("**/api/ai-sanitize", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + sanitizedUrl: "https://example.com/", + confidence: 0.85, + removedParams: ["unknown"], + suggestedRule: { + pattern: "^/.*$", + allowedParams: [], + }, + }), + }); + }); + + await page.goto("/sanitize?url=https://example.com/?unknown=param"); + await page.getByRole("button", { name: "Try AI" }).click(); + + await expect(page.getByText("AI is analyzing your URL...")).toBeVisible(); + await expect( + page.getByText("AI suggestion (confidence: 85%)"), + ).toBeVisible({ timeout: 5000 }); + await expect(page.getByRole("textbox")).toHaveValue( + "https://example.com/", + ); + }); + + test("shows suggested rule and GitHub submit link", async ({ page }) => { + await page.route("**/api/ai-sanitize", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + sanitizedUrl: "https://example.com/", + confidence: 0.9, + removedParams: ["tracking"], + suggestedRule: { + pattern: "^/.*$", + allowedParams: [], + }, + }), + }); + }); + + await page.goto("/sanitize?url=https://example.com/?tracking=123"); + await page.getByRole("button", { name: "Try AI" }).click(); + + await expect(page.getByText("Suggested rule:")).toBeVisible({ + timeout: 5000, + }); + const submitLink = page.getByRole("link", { + name: "Submit Rule to GitHub", + }); + await expect(submitLink).toBeVisible(); + await expect(submitLink).toHaveAttribute( + "href", + /github\.com\/Tail-WTF\/Rules\/issues\/new/, + ); + }); + + test("Try Browser Render button triggers browser sanitization", async ({ + page, + }) => { + await page.route("**/api/browser-sanitize", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + sanitizedUrl: "https://example.com/", + requiredParams: [], + removedParams: ["tracking"], + suggestedRule: { + pattern: "^/.*$", + allowedParams: [], + }, + }), + }); + }); + + await page.goto("/sanitize?url=https://example.com/?tracking=123"); + await page.getByRole("button", { name: "Try Browser Render" }).click(); + + await expect( + page.getByText("Browser is testing parameters..."), + ).toBeVisible(); + await expect(page.getByText("Browser-verified suggestion:")).toBeVisible({ + timeout: 5000, + }); + }); + }); + + test.describe("error handling", () => { + test("handles API network error", async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.abort("failed"); + }); + + await page.goto("/sanitize?url=https://example.com/?tracking=123"); + + await expect(page.getByText("Your link is not sanitized.")).toBeVisible(); + }); + + test("handles AI sanitization failure", async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ text: "test", sanitizedURLs: [] }), + }); + }); + + await page.route("**/api/ai-sanitize", async (route) => { + await route.fulfill({ + status: 500, + contentType: "application/json", + body: JSON.stringify({ error: "AI failed" }), + }); + }); + + await page.goto("/sanitize?url=https://example.com/?tracking=123"); + await page.getByRole("button", { name: "Try AI" }).click(); + + await expect(page.getByText("AI sanitization failed.")).toBeVisible({ + timeout: 5000, + }); + }); + }); +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index 93412d7..8dc3c6f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,16 +36,19 @@ export default ts.config( }, ], "unused-imports/no-unused-imports": "error", + "svelte/no-navigation-without-resolve": "off", }, }, { ignores: [ "build/", ".svelte-kit/", + ".next/", "dist/", "out/", "node_modules/", "worker/", + "playwright-report/", ], }, ); diff --git a/package-lock.json b/package-lock.json index 0e7cee9..a1fb55f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "devDependencies": { "@eslint/js": "^9.28.0", + "@playwright/test": "^1.52.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.3", @@ -33,17 +34,6 @@ "node": ">=24" } }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -505,6 +495,19 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", @@ -594,9 +597,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -732,6 +735,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1153,6 +1172,46 @@ "vite": "^6.0.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, "node_modules/@tailwindcss/oxide-android-arm64": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", @@ -1272,6 +1331,40 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", @@ -1302,116 +1395,87 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.7.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", - "cpu": [ - "x64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.7.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", - "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", "dev": true, + "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "tailwindcss": "4.1.18" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" + "tslib": "^2.4.0" } }, - "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", "dev": true, + "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" } }, - "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", "dev": true, + "inBundle": true, "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": ">= 10" } }, - "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-musl": { + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "cpu": [ "x64" ], @@ -1419,18 +1483,26 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": ">= 10" } }, - "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "node_modules/@tailwindcss/vite": { "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } }, "node_modules/@types/cookie": { "version": "0.6.0", @@ -1719,19 +1791,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1823,9 +1882,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2004,9 +2063,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2166,48 +2225,18 @@ } } }, - "node_modules/eslint-plugin-svelte/node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "node_modules/eslint-plugin-unused-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz", + "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==", "dev": true, "license": "MIT", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-unused-imports": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz", - "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", - "eslint": "^9.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { + "@typescript-eslint/eslint-plugin": { "optional": true } } @@ -2230,29 +2259,29 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/esm-env": { @@ -2280,19 +2309,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2370,6 +2386,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2422,9 +2456,9 @@ "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2671,6 +2705,153 @@ "lightningcss-win32-x64-msvc": "1.30.2" } }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", @@ -2713,6 +2894,48 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -2919,6 +3142,51 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2948,6 +3216,46 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-safe-parser": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", @@ -3366,39 +3674,6 @@ "typescript": ">=5.0.0" } }, - "node_modules/svelte-check/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.1.tgz", @@ -3429,23 +3704,10 @@ } } }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/tailwindcss": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", - "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "dev": true, "license": "MIT" }, @@ -3480,37 +3742,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -3534,14 +3765,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3692,35 +3915,19 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/vitefu": { @@ -3770,13 +3977,21 @@ } }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { diff --git a/package.json b/package.json index 2f1f5d3..a8608e6 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,13 @@ "preview": "vite preview", "start": "npx serve out", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "lint": "eslint . && prettier --check ." + "lint": "eslint . && prettier --check .", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "devDependencies": { "@eslint/js": "^9.28.0", + "@playwright/test": "^1.52.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d87cee0 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: "http://localhost:4173", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "npm run preview", + url: "http://localhost:4173", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/lib/components/ResultFailure.svelte b/src/lib/components/ResultFailure.svelte index ed144b0..f0e75ec 100644 --- a/src/lib/components/ResultFailure.svelte +++ b/src/lib/components/ResultFailure.svelte @@ -56,10 +56,7 @@ } } - function buildGitHubIssueUrl( - domain: string, - result: SanitizeResult, - ): string { + function buildGitHubIssueUrl(domain: string, result: SanitizeResult): string { const title = `Rule request: ${domain}`; const body = `## Domain diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8d70ac3..9ba9015 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -17,7 +17,9 @@ if (url === "") { try { url = await navigator.clipboard.readText(); - } catch {} + } catch { + // Clipboard access denied or unavailable - ignore + } } } @@ -94,8 +96,8 @@ "trackers" tied to your account. Every time your friends open these links, service providers learn more about your social connections such as how often you talk to a particular - friend, topics you discuss, etc. They can further exploit this information - in conjunction with other data they have about you and your friends. + friend, topics you discuss, etc. They can further exploit this information in + conjunction with other data they have about you and your friends.

    Tail.WTF protects your privacy by chopping the annoying diff --git a/worker/src/index.ts b/worker/src/index.ts index 4dd81ca..22811d7 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -293,7 +293,10 @@ function quickHeuristicCheck(a: PageRender, b: PageRender): boolean | null { if (a.title && b.title && a.title !== b.title) { // Check if difference is just tracking-related suffix const normalize = (t: string) => - t.replace(/\s*[-|·]\s*.*$/, "").trim().toLowerCase(); + t + .replace(/\s*[-|·]\s*.*$/, "") + .trim() + .toLowerCase(); if (normalize(a.title) !== normalize(b.title)) { return false; } @@ -460,7 +463,9 @@ app.post("/api/browser-sanitize", async (c) => { urlWithoutThisParam.toString(), ); - if (!(await arePagesSimlar(c.env.AI, originalPage, pageWithoutThisParam))) { + if ( + !(await arePagesSimlar(c.env.AI, originalPage, pageWithoutThisParam)) + ) { requiredParams.push(key); } } From 3dec0b43b138a827e75243d096982a6292584fbf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 12:45:06 +0000 Subject: [PATCH 11/18] refactor: merge sanitize page into home for privacy - Remove /sanitize page and query params to prevent URL exposure in browser history - Show sanitization results inline on the same page - Add "Sanitize another link" button to reset state - Improve ARIA labels in footer for better accessibility and testing - Update ESLint config with ignoreLinks for navigation rule - Simplify Cloudflare notice to only link privacy policy - Consolidate e2e tests into home.spec.ts --- e2e/home.spec.ts | 287 +++++++++++++++++++++++- e2e/navigation.spec.ts | 3 +- e2e/sanitize.spec.ts | 220 ------------------ eslint.config.mjs | 2 +- src/lib/components/Footer.svelte | 8 +- src/lib/components/ResultFailure.svelte | 11 +- src/lib/components/ResultSuccess.svelte | 11 +- src/routes/+page.svelte | 270 +++++++++++++--------- src/routes/sanitize/+page.svelte | 78 ------- 9 files changed, 466 insertions(+), 424 deletions(-) delete mode 100644 e2e/sanitize.spec.ts delete mode 100644 src/routes/sanitize/+page.svelte diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts index 07480eb..b7863e7 100644 --- a/e2e/home.spec.ts +++ b/e2e/home.spec.ts @@ -32,17 +32,6 @@ test.describe("Home Page", () => { await expect(page.getByRole("button", { name: "Sanitize" })).toBeVisible(); }); - test("navigates to sanitize page on form submit", async ({ page }) => { - await page.goto("/"); - - const input = page.getByPlaceholder("-> Paste your link here <-"); - await input.fill("https://example.com/?tracking=123"); - - await page.getByRole("button", { name: "Sanitize" }).click(); - - await expect(page).toHaveURL(/\/sanitize\?url=/); - }); - test("displays About section", async ({ page }) => { await page.goto("/"); await expect( @@ -53,7 +42,7 @@ test.describe("Home Page", () => { test("displays Privacy section with Cloudflare notice", async ({ page }) => { await page.goto("/"); await expect(page.getByRole("heading", { name: "Privacy" })).toBeVisible(); - await expect(page.getByText("Cloudflare")).toBeVisible(); + await expect(page.getByText("powered by Cloudflare")).toBeVisible(); await expect( page.getByRole("link", { name: "Cloudflare's Privacy Policy" }), ).toHaveAttribute("href", "https://www.cloudflare.com/privacypolicy/"); @@ -69,3 +58,277 @@ test.describe("Home Page", () => { ); }); }); + +test.describe("Sanitization", () => { + test.describe("with mocked successful API", () => { + test.beforeEach(async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + text: "https://example.com/page", + sanitizedURLs: ["https://example.com/page"], + }), + }); + }); + }); + + test("shows loading state on submit", async ({ page }) => { + // Delay the API response to see loading state + await page.route("**/api/sanitize*", async (route) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + text: "https://example.com/page", + sanitizedURLs: ["https://example.com/page"], + }), + }); + }); + + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/page?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + await expect(page.getByText("Sanitizing...")).toBeVisible(); + }); + + test("displays sanitized URL on success", async ({ page }) => { + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/page?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + + await expect(page.getByText("Your link is now sanitized!")).toBeVisible(); + await expect(page.getByRole("textbox")).toHaveValue( + "https://example.com/page", + ); + }); + + test("has link to open sanitized URL", async ({ page }) => { + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/page?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + + await expect(page.getByRole("textbox")).toBeVisible(); + const openLink = page.getByRole("link", { name: "Open Sanitized Link" }); + await expect(openLink).toHaveAttribute( + "href", + "https://example.com/page", + ); + }); + + test("can sanitize another link", async ({ page }) => { + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/page?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + + await expect(page.getByText("Your link is now sanitized!")).toBeVisible(); + await page.getByRole("button", { name: "Sanitize another link" }).click(); + + await expect( + page.getByRole("heading", { name: "Link Sanitizer" }), + ).toBeVisible(); + await expect( + page.getByPlaceholder("-> Paste your link here <-"), + ).toBeVisible(); + }); + }); + + test.describe("with mocked failed API", () => { + test.beforeEach(async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + text: "https://example.com/?unknown=param", + sanitizedURLs: [], + }), + }); + }); + }); + + test("shows error state when no sanitization rules match", async ({ + page, + }) => { + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/?unknown=param"); + await page.getByRole("button", { name: "Sanitize" }).click(); + + await expect(page.getByText("Your link is not sanitized.")).toBeVisible(); + await expect( + page.getByText("We were unable to sanitize your link"), + ).toBeVisible(); + }); + + test("shows Try AI and Try Browser Render buttons", async ({ page }) => { + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/?unknown=param"); + await page.getByRole("button", { name: "Sanitize" }).click(); + + await expect(page.getByRole("button", { name: "Try AI" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Try Browser Render" }), + ).toBeVisible(); + }); + + test("Try AI button triggers AI sanitization", async ({ page }) => { + await page.route("**/api/ai-sanitize", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + sanitizedUrl: "https://example.com/", + confidence: 0.85, + removedParams: ["unknown"], + suggestedRule: { + pattern: "^/.*$", + allowedParams: [], + }, + }), + }); + }); + + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/?unknown=param"); + await page.getByRole("button", { name: "Sanitize" }).click(); + await page.getByRole("button", { name: "Try AI" }).click(); + + await expect(page.getByText("AI is analyzing your URL...")).toBeVisible(); + await expect( + page.getByText("AI suggestion (confidence: 85%)"), + ).toBeVisible({ timeout: 5000 }); + }); + + test("shows suggested rule and GitHub submit link", async ({ page }) => { + await page.route("**/api/ai-sanitize", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + sanitizedUrl: "https://example.com/", + confidence: 0.9, + removedParams: ["tracking"], + suggestedRule: { + pattern: "^/.*$", + allowedParams: [], + }, + }), + }); + }); + + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + await page.getByRole("button", { name: "Try AI" }).click(); + + await expect(page.getByText("Suggested rule:")).toBeVisible({ + timeout: 5000, + }); + const submitLink = page.getByRole("link", { + name: "Submit Rule to GitHub", + }); + await expect(submitLink).toBeVisible(); + await expect(submitLink).toHaveAttribute( + "href", + /github\.com\/Tail-WTF\/Rules\/issues\/new/, + ); + }); + + test("Try Browser Render button triggers browser sanitization", async ({ + page, + }) => { + await page.route("**/api/browser-sanitize", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + sanitizedUrl: "https://example.com/", + requiredParams: [], + removedParams: ["tracking"], + suggestedRule: { + pattern: "^/.*$", + allowedParams: [], + }, + }), + }); + }); + + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + await page.getByRole("button", { name: "Try Browser Render" }).click(); + + await expect( + page.getByText("Browser is testing parameters..."), + ).toBeVisible(); + await expect(page.getByText("Browser-verified suggestion:")).toBeVisible({ + timeout: 5000, + }); + }); + }); + + test.describe("error handling", () => { + test("handles API network error", async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.abort("failed"); + }); + + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + + await expect(page.getByText("Your link is not sanitized.")).toBeVisible(); + }); + + test("handles AI sanitization failure", async ({ page }) => { + await page.route("**/api/sanitize*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ text: "test", sanitizedURLs: [] }), + }); + }); + + await page.route("**/api/ai-sanitize", async (route) => { + await route.fulfill({ + status: 500, + contentType: "application/json", + body: JSON.stringify({ error: "AI failed" }), + }); + }); + + await page.goto("/"); + await page + .getByPlaceholder("-> Paste your link here <-") + .fill("https://example.com/?tracking=123"); + await page.getByRole("button", { name: "Sanitize" }).click(); + await page.getByRole("button", { name: "Try AI" }).click(); + + await expect(page.getByText("AI sanitization failed.")).toBeVisible({ + timeout: 5000, + }); + }); + }); +}); diff --git a/e2e/navigation.spec.ts b/e2e/navigation.spec.ts index a2e82fb..168c027 100644 --- a/e2e/navigation.spec.ts +++ b/e2e/navigation.spec.ts @@ -9,11 +9,10 @@ test.describe("Navigation", () => { await expect(page).toHaveURL("/#about"); // Check Privacy link scrolls to section - await page.getByRole("link", { name: "Privacy" }).click(); + await page.getByRole("link", { name: "Go to Privacy section" }).click(); await expect(page).toHaveURL("/#privacy"); // Check Tail.WTF link goes to home - await page.goto("/sanitize?url=test"); await page.getByRole("link", { name: "Tail.WTF" }).click(); await expect(page).toHaveURL("/"); }); diff --git a/e2e/sanitize.spec.ts b/e2e/sanitize.spec.ts deleted file mode 100644 index d793936..0000000 --- a/e2e/sanitize.spec.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("Sanitize Page", () => { - test.describe("with mocked successful API", () => { - test.beforeEach(async ({ page }) => { - await page.route("**/api/sanitize*", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - text: "https://example.com/page", - sanitizedURLs: ["https://example.com/page"], - }), - }); - }); - }); - - test("shows loading state initially", async ({ page }) => { - // Delay the API response to see loading state - await page.route("**/api/sanitize*", async (route) => { - await new Promise((resolve) => setTimeout(resolve, 100)); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - text: "https://example.com/page", - sanitizedURLs: ["https://example.com/page"], - }), - }); - }); - - await page.goto("/sanitize?url=https://example.com/page?tracking=123"); - await expect(page.getByText("Sanitizing...")).toBeVisible(); - }); - - test("displays sanitized URL on success", async ({ page }) => { - await page.goto("/sanitize?url=https://example.com/page?tracking=123"); - - await expect(page.getByText("Your link is now sanitized!")).toBeVisible(); - await expect(page.getByRole("textbox")).toHaveValue( - "https://example.com/page", - ); - }); - - test("has link to open sanitized URL", async ({ page }) => { - await page.goto("/sanitize?url=https://example.com/page?tracking=123"); - - await expect(page.getByRole("textbox")).toBeVisible(); - const openLink = page.getByRole("link", { name: "Open Sanitized Link" }); - await expect(openLink).toHaveAttribute( - "href", - "https://example.com/page", - ); - }); - }); - - test.describe("with mocked failed API", () => { - test.beforeEach(async ({ page }) => { - await page.route("**/api/sanitize*", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - text: "https://example.com/?unknown=param", - sanitizedURLs: [], - }), - }); - }); - }); - - test("shows error state when no sanitization rules match", async ({ - page, - }) => { - await page.goto("/sanitize?url=https://example.com/?unknown=param"); - - await expect(page.getByText("Your link is not sanitized.")).toBeVisible(); - await expect( - page.getByText("We were unable to sanitize your link"), - ).toBeVisible(); - }); - - test("shows Try AI and Try Browser Render buttons", async ({ page }) => { - await page.goto("/sanitize?url=https://example.com/?unknown=param"); - - await expect(page.getByRole("button", { name: "Try AI" })).toBeVisible(); - await expect( - page.getByRole("button", { name: "Try Browser Render" }), - ).toBeVisible(); - }); - - test("Try AI button triggers AI sanitization", async ({ page }) => { - await page.route("**/api/ai-sanitize", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - sanitizedUrl: "https://example.com/", - confidence: 0.85, - removedParams: ["unknown"], - suggestedRule: { - pattern: "^/.*$", - allowedParams: [], - }, - }), - }); - }); - - await page.goto("/sanitize?url=https://example.com/?unknown=param"); - await page.getByRole("button", { name: "Try AI" }).click(); - - await expect(page.getByText("AI is analyzing your URL...")).toBeVisible(); - await expect( - page.getByText("AI suggestion (confidence: 85%)"), - ).toBeVisible({ timeout: 5000 }); - await expect(page.getByRole("textbox")).toHaveValue( - "https://example.com/", - ); - }); - - test("shows suggested rule and GitHub submit link", async ({ page }) => { - await page.route("**/api/ai-sanitize", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - sanitizedUrl: "https://example.com/", - confidence: 0.9, - removedParams: ["tracking"], - suggestedRule: { - pattern: "^/.*$", - allowedParams: [], - }, - }), - }); - }); - - await page.goto("/sanitize?url=https://example.com/?tracking=123"); - await page.getByRole("button", { name: "Try AI" }).click(); - - await expect(page.getByText("Suggested rule:")).toBeVisible({ - timeout: 5000, - }); - const submitLink = page.getByRole("link", { - name: "Submit Rule to GitHub", - }); - await expect(submitLink).toBeVisible(); - await expect(submitLink).toHaveAttribute( - "href", - /github\.com\/Tail-WTF\/Rules\/issues\/new/, - ); - }); - - test("Try Browser Render button triggers browser sanitization", async ({ - page, - }) => { - await page.route("**/api/browser-sanitize", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - sanitizedUrl: "https://example.com/", - requiredParams: [], - removedParams: ["tracking"], - suggestedRule: { - pattern: "^/.*$", - allowedParams: [], - }, - }), - }); - }); - - await page.goto("/sanitize?url=https://example.com/?tracking=123"); - await page.getByRole("button", { name: "Try Browser Render" }).click(); - - await expect( - page.getByText("Browser is testing parameters..."), - ).toBeVisible(); - await expect(page.getByText("Browser-verified suggestion:")).toBeVisible({ - timeout: 5000, - }); - }); - }); - - test.describe("error handling", () => { - test("handles API network error", async ({ page }) => { - await page.route("**/api/sanitize*", async (route) => { - await route.abort("failed"); - }); - - await page.goto("/sanitize?url=https://example.com/?tracking=123"); - - await expect(page.getByText("Your link is not sanitized.")).toBeVisible(); - }); - - test("handles AI sanitization failure", async ({ page }) => { - await page.route("**/api/sanitize*", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ text: "test", sanitizedURLs: [] }), - }); - }); - - await page.route("**/api/ai-sanitize", async (route) => { - await route.fulfill({ - status: 500, - contentType: "application/json", - body: JSON.stringify({ error: "AI failed" }), - }); - }); - - await page.goto("/sanitize?url=https://example.com/?tracking=123"); - await page.getByRole("button", { name: "Try AI" }).click(); - - await expect(page.getByText("AI sanitization failed.")).toBeVisible({ - timeout: 5000, - }); - }); - }); -}); diff --git a/eslint.config.mjs b/eslint.config.mjs index 8dc3c6f..17e8593 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,7 +36,7 @@ export default ts.config( }, ], "unused-imports/no-unused-imports": "error", - "svelte/no-navigation-without-resolve": "off", + "svelte/no-navigation-without-resolve": ["error", { ignoreLinks: true }], }, }, { diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index 160a588..2421c4e 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -1,8 +1,8 @@ -