diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..63ec2d2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + { + "name": "Wrangler Dev", + "type": "node", + "request": "attach", + "preLaunchTask": "Wrangler Dev", + "cwd": "${workspaceFolder}", + "resolveSourceMapLocations": [ + "**", + "!**/node_modules/**" + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index c65547b..bc4f1e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,5 @@ { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit", - "source.fixAll": "never" - }, + "editor.formatOnSave": false, "eslint.format.enable": true, "editor.defaultFormatter": "dbaeumer.vscode-eslint", "prettier.enable": false diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..4da2764 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,36 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Wrangler Dev", + "detail": "Starts your Cloudflare Worker in local development mode.", + "type": "shell", + "command": "npm run dev", + "isBackground": true, + "problemMatcher": { + "owner": "wrangler", + "source": "wrangler", + "pattern": { + "regexp": "^(?.+\\.rs):(?\\d+):(?\\d+):\\s+(?.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "(Starting|Reloading) local server\\.\\.\\.", + "endsPattern": "(Ready|Updated and ready) on" + } + }, + "group": { + "kind": "test", + "isDefault": true + }, + "icon": { + "color": "terminal.ansiYellow", + "id": "cloud" + } + } + ] +} diff --git a/package-lock.json b/package-lock.json index d1a232c..e088d77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,10 @@ "hono": "^4.6.16" }, "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.5.40", + "@cloudflare/vitest-pool-workers": "^0.11.1", "@cloudflare/workers-types": "^4.20241230.0", "@eslint/js": "^9.11.1", + "cheerio": "^1.1.2", "eslint": "^9.11.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", @@ -97,31 +98,60 @@ } }, "node_modules/@cloudflare/vitest-pool-workers": { - "version": "0.5.40", - "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.5.40.tgz", - "integrity": "sha512-aBHNj55l6G07+ZJuhJsuElDYOEKcGJ4nEdE+X7XmyCRxiw7eRjc1iPQOfEFqprzKQ/2tPEOO8hL0mgRIgt8K3g==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.11.1.tgz", + "integrity": "sha512-2ATtR5pb5Q6R19ByHN/Dbe1JY5JWuqGMz3yI2MusDqPcDvL1o5OPlPKJAXphOMEIilXLeATPYrNamdqLBJu3sg==", "dev": true, "license": "MIT", "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", - "devalue": "^4.3.0", - "esbuild": "0.17.19", - "miniflare": "3.20241218.0", - "semver": "^7.5.1", - "wrangler": "3.99.0", + "devalue": "^5.3.2", + "esbuild": "0.27.0", + "miniflare": "4.20251217.0", + "semver": "^7.7.1", + "wrangler": "4.56.0", "zod": "^3.22.3" }, "peerDependencies": { - "@vitest/runner": "2.0.x - 2.1.x", - "@vitest/snapshot": "2.0.x - 2.1.x", - "vitest": "2.0.x - 2.1.x" + "@vitest/runner": "2.0.x - 3.2.x", + "@vitest/snapshot": "2.0.x - 3.2.x", + "vitest": "2.0.x - 3.2.x" } }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20241218.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241218.0.tgz", - "integrity": "sha512-8rveQoxtUvlmORKqTWgjv2ycM8uqWox0u9evn3zd2iWKdou5sncFwH517ZRLI3rq9P31ZLmCQBZ0gloFsTeY6w==", + "node_modules/@cloudflare/vitest-pool-workers/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/vitest-pool-workers/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/vitest-pool-workers/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20251217.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251217.0.tgz", + "integrity": "sha512-DN6vT+9ho61d/1/YuILW4VS+N1JBLaixWRL1vqNmhgbf8J8VHwWWotrRruEUYigJKx2yZyw6YsasE+yLXgx/Fw==", "cpu": [ "x64" ], @@ -135,10 +165,10 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20241218.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241218.0.tgz", - "integrity": "sha512-be59Ad9nmM9lCkhHqmTs/uZ3JVZt8NJ9Z0PY+B0xnc5z6WwmV2lj0RVLtq7xJhQsQJA189zt5rXqDP6J+2mu7Q==", + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20251217.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251217.0.tgz", + "integrity": "sha512-5nZOpRTkHmtcTc4Wbr1mj/O3dLb6aHZSiJuVBgtdbVcVmOXueSay3hnw1PXEyR+vpTKGUPkM+omUIslKHWnXDw==", "cpu": [ "arm64" ], @@ -152,10 +182,10 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20241218.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241218.0.tgz", - "integrity": "sha512-MzpSBcfZXRxrYWxQ4pVDYDrUbkQuM62ssl4ZtHH8J35OAeGsWFAYji6MkS2SpVwVcvacPwJXIF4JSzp4xKImKw==", + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20251217.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251217.0.tgz", + "integrity": "sha512-uoPGhMaZVXPpCsU0oG3HQzyVpXCGi5rU+jcHRjUI7DXM4EwctBGvZ380Knkja36qtl+ZvSKVR1pUFSGdK+45Pg==", "cpu": [ "x64" ], @@ -169,10 +199,10 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20241218.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241218.0.tgz", - "integrity": "sha512-RIuJjPxpNqvwIs52vQsXeRMttvhIjgg9NLjjFa3jK8Ijnj8c3ZDru9Wqi48lJP07yDFIRr4uDMMqh/y29YQi2A==", + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20251217.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251217.0.tgz", + "integrity": "sha512-ixHnHKsiz1Xko+eDgCJOZ7EEUZKtmnYq3AjW3nkVcLFypSLks4C29E45zVewdaN4wq8sCLeyQCl6r1kS17+DQQ==", "cpu": [ "arm64" ], @@ -186,10 +216,10 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20241218.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241218.0.tgz", - "integrity": "sha512-tO1VjlvK3F6Yb2d1jgEy/QBYl//9Pyv3K0j+lq8Eu7qdfm0IgKwSRgDWLept84/qmNsQfausZ4JdNGxTf9xsxQ==", + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20251217.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251217.0.tgz", + "integrity": "sha512-rP6USX+7ctynz3AtmKi+EvlLP3Xdr1ETrSdcnv693/I5QdUwBxq4yE1Lj6CV7GJizX6opXKYg8QMq0Q4eB9zRQ==", "cpu": [ "x64" ], @@ -203,54 +233,10 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20241230.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20241230.0.tgz", - "integrity": "sha512-dtLD4jY35Lb750cCVyO1i/eIfdZJg2Z0i+B1RYX6BVeRPlgaHx/H18ImKAkYmy0g09Ow8R2jZy3hIxMgXun0WQ==", - "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/@esbuild-plugins/node-globals-polyfill": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", - "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild-plugins/node-modules-polyfill": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", - "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", - "dev": true, - "license": "ISC", - "dependencies": { - "escape-string-regexp": "^4.0.0", - "rollup-plugin-node-polyfills": "^0.2.1" - }, - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -261,13 +247,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -278,13 +264,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -295,13 +281,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -312,13 +298,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -329,13 +315,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -346,13 +332,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -363,13 +349,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -380,13 +366,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -397,13 +383,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -414,13 +400,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -431,13 +417,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -448,13 +434,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -465,13 +451,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -482,13 +468,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -499,13 +485,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -516,13 +502,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -533,13 +519,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -550,13 +536,13 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -567,13 +553,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -584,13 +570,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -601,13 +587,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -618,13 +604,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "node_modules/@cloudflare/vitest-pool-workers/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" ], @@ -635,179 +621,1345 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@cloudflare/vitest-pool-workers/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", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=0.4.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==", + "node_modules/@cloudflare/vitest-pool-workers/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": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "node_modules/@cloudflare/vitest-pool-workers/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": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "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/@cloudflare/vitest-pool-workers/node_modules/miniflare": { + "version": "4.20251217.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251217.0.tgz", + "integrity": "sha512-8xsTQbPS6YV+ABZl9qiJIbsum6hbpbhqiyKpOVdzZrhK+1N8EFpT8R6aBZff7kezGmxYZSntjgjqTwJmj3JLgA==", + "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.20251217.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/@cloudflare/vitest-pool-workers/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/@cloudflare/vitest-pool-workers/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/@cloudflare/vitest-pool-workers/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", + "peer": true, + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/workerd": { + "version": "1.20251217.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251217.0.tgz", + "integrity": "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20251217.0", + "@cloudflare/workerd-darwin-arm64": "1.20251217.0", + "@cloudflare/workerd-linux-64": "1.20251217.0", + "@cloudflare/workerd-linux-arm64": "1.20251217.0", + "@cloudflare/workerd-windows-64": "1.20251217.0" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.56.0.tgz", + "integrity": "sha512-Nqi8duQeRbA+31QrD6QlWHW3IZVnuuRxMy7DEg46deUzywivmaRV/euBN5KKXDPtA24VyhYsK7I0tkb7P5DM2w==", + "deprecated": "Version 4.55.0 and 4.56.0 can incorrectly automatically delegate 'wrangler deploy' to 'opennextjs-cloudflare'. Use an older or newer version.", + "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.20251217.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20251217.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.20251217.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vitest-pool-workers/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/@cloudflare/vitest-pool-workers/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" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20241218.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241218.0.tgz", + "integrity": "sha512-8rveQoxtUvlmORKqTWgjv2ycM8uqWox0u9evn3zd2iWKdou5sncFwH517ZRLI3rq9P31ZLmCQBZ0gloFsTeY6w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20241218.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241218.0.tgz", + "integrity": "sha512-be59Ad9nmM9lCkhHqmTs/uZ3JVZt8NJ9Z0PY+B0xnc5z6WwmV2lj0RVLtq7xJhQsQJA189zt5rXqDP6J+2mu7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20241218.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241218.0.tgz", + "integrity": "sha512-MzpSBcfZXRxrYWxQ4pVDYDrUbkQuM62ssl4ZtHH8J35OAeGsWFAYji6MkS2SpVwVcvacPwJXIF4JSzp4xKImKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20241218.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241218.0.tgz", + "integrity": "sha512-RIuJjPxpNqvwIs52vQsXeRMttvhIjgg9NLjjFa3jK8Ijnj8c3ZDru9Wqi48lJP07yDFIRr4uDMMqh/y29YQi2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20241218.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241218.0.tgz", + "integrity": "sha512-tO1VjlvK3F6Yb2d1jgEy/QBYl//9Pyv3K0j+lq8Eu7qdfm0IgKwSRgDWLept84/qmNsQfausZ4JdNGxTf9xsxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20251229.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251229.0.tgz", + "integrity": "sha512-LgzxDZaT9bQhycQInf7S/fcZCQRTvWWQPE9xnEyedI+CXxWsXAD7hg84kvVyr+KUz+W9Oblzo75g6XZ3HdI5Yg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peer": true + }, + "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-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "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/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "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/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "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.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", + "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@eslint/core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", - "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "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.18.0 || ^20.9.0 || >=21.1.0" + "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/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "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": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "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": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, - "node_modules/@eslint/js": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", - "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "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": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "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/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "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.18.0 || ^20.9.0 || >=21.1.0" + "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/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "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", - "dependencies": { - "levn": "^0.4.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "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/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "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": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, "engines": { - "node": ">=14" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "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", + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "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", + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, "node_modules/@jridgewell/resolve-uri": { @@ -886,9 +2038,51 @@ "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/unts" + } + }, + "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/dumper/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/@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.24.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", @@ -1113,6 +2307,26 @@ "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/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1133,6 +2347,7 @@ "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -1187,6 +2402,7 @@ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -1454,6 +2670,7 @@ "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "2.1.3", "pathe": "^1.1.2" @@ -1468,6 +2685,7 @@ "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/pretty-format": "2.1.3", "magic-string": "^0.30.11", @@ -1516,11 +2734,12 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1651,6 +2870,13 @@ "dev": true, "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1750,6 +2976,60 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1773,6 +3053,20 @@ "dev": true, "license": "MIT" }, + "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", @@ -1793,6 +3087,17 @@ "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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1825,6 +3130,36 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/data-uri-to-buffer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", @@ -1885,13 +3220,119 @@ "dev": true, "license": "MIT" }, + "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/devalue": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", - "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", + "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==", "dev": true, "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/esbuild": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", @@ -1899,6 +3340,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2323,6 +3765,7 @@ "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -2386,6 +3829,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -2777,6 +4221,52 @@ "node": ">=16.9.0" } }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2814,6 +4304,13 @@ "node": ">=0.8.19" } }, + "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/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -2937,6 +4434,16 @@ "json-buffer": "3.0.1" } }, + "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/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3127,6 +4634,19 @@ "node": ">= 6.13.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/ohash": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", @@ -3197,6 +4717,59 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3521,6 +5094,13 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/selfsigned": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", @@ -3536,9 +5116,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "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": { @@ -3548,6 +5128,46 @@ "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/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3571,6 +5191,16 @@ "node": ">=8" } }, + "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": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3785,6 +5415,7 @@ "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3886,6 +5517,7 @@ "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4007,6 +5639,7 @@ "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "2.1.3", "@vitest/mocker": "2.1.3", @@ -4131,6 +5764,30 @@ "node": ">=8" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4276,6 +5933,17 @@ "stacktracey": "^2.1.8" } }, + "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.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index 237933f..6ef861c 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,10 @@ "lint:check": "eslint src/**/*.ts" }, "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.5.40", + "@cloudflare/vitest-pool-workers": "^0.11.1", "@cloudflare/workers-types": "^4.20241230.0", "@eslint/js": "^9.11.1", + "cheerio": "^1.1.2", "eslint": "^9.11.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", diff --git a/src/util.ts b/src/util.ts index 9f7ab38..6364f61 100644 --- a/src/util.ts +++ b/src/util.ts @@ -206,6 +206,14 @@ export function getPostImages(thread: ThreadViewPost): BskyeImage | undefined { // TODO: add warn if has more than 1 embed const image = quotedEmbeds[0]; + return { + author: quotedAuthor, + images: mapImages(image), + quotedPost: quotedPost + }; + } else if (quotedEmbeds && quotedEmbeds.length > 0 && isViewRecordWithMedia(quotedEmbeds[0]) && isViewImage(quotedEmbeds[0].media)) { + const image = quotedEmbeds[0].media; + return { author: quotedAuthor, images: mapImages(image), diff --git a/test/discord_functional.spec.ts b/test/discord_functional.spec.ts new file mode 100644 index 0000000..ec1682d --- /dev/null +++ b/test/discord_functional.spec.ts @@ -0,0 +1,567 @@ +import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test'; +import { describe, expect, it } from 'vitest'; +import app from '../src'; +import * as cheerio from 'cheerio'; +import { Element } from 'domhandler'; + +const userAgent = 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)'; + +describe('Discord Functional Tests (Real API)', () => { + it('should handle image post', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/botafogo.com.br/post/3l4zmomzlla2x?abc=123', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/botafogo.com.br/post/3l4zmomzlla2x'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/botafogo.com.br/post/3l4zmomzlla2x'); + expect($('meta[property="og:description"]').attr('content')).toBeDefined(); + expect($('meta[name="twitter:card"]').attr('content')).toBe('summary_large_image'); + expect($('meta[property="twitter:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:secure_url"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:type"]').attr('content')).toBe('image/jpeg'); + expect($('meta[property="og:image:width"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:height"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:alt"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); + + it('should handle gif post', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/ousincera.bsky.social/post/3l5axoea6ic2l?abc=123', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/ousincera.bsky.social/post/3l5axoea6ic2l'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe( + '0; url = https://bsky.app/profile/ousincera.bsky.social/post/3l5axoea6ic2l' + ); + expect($('meta[property="og:description"]').attr('content')).toBeDefined(); + expect($('meta[name="twitter:card"]').attr('content')).toBe('summary_large_image'); + expect($('meta[property="twitter:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:secure_url"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:type"]').attr('content')).toBe('image/jpeg'); + expect($('meta[property="og:image:width"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:height"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:alt"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); + + it('should handle video post', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/furia.gg/post/3l5dbuctdcf2g?abc=123', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/furia.gg/post/3l5dbuctdcf2g'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/furia.gg/post/3l5dbuctdcf2g'); + expect($('meta[name="twitter:card"]').attr('content')).toBe('player'); + expect($('meta[name="twitter:player:width"]').attr('content')).toBe('0'); + expect($('meta[name="twitter:player:height"]').attr('content')).toBe('0'); + expect($('meta[name="twitter:player:stream"]').attr('content')).toBeDefined(); + expect($('meta[name="twitter:player:stream:content_type"]').attr('content')).toBe('video/mp4'); + expect($('meta[property="og:video:secure_url"]').attr('content')).toBeDefined(); + expect($('meta[property="og:video:type"]').attr('content')).toBe('video/mp4'); + expect($('meta[property="og:video:height"]').attr('content')).toBe('0'); + expect($('meta[property="og:video:width"]').attr('content')).toBe('0'); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).not.toBe(''); + expect(oembed.author_name).length.above(0); + }); + + it('should handle multiple images post', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/fittyforum.bsky.social/post/3l5cl4enhou2r', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/fittyforum.bsky.social/post/3l5cl4enhou2r'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe( + '0; url = https://bsky.app/profile/fittyforum.bsky.social/post/3l5cl4enhou2r' + ); + expect($('meta[property="og:description"]').attr('content')).toBeDefined(); + + expect($('meta[name="twitter:card"]').length).toBe(4); + $('meta[name="twitter:card"]').each((_, element) => { + expect($(element).attr('content')).toBe('summary_large_image'); + }); + $('meta[property="twitter:image"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image:secure_url"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image:type"]').each((_, element) => { + expect($(element).attr('content')).toBe('image/jpeg'); + }); + $('meta[property="og:image:width"]').each((_, element) => { + expect($(element).attr('content')).toBe('0'); + }); + $('meta[property="og:image:height"]').each((_, element) => { + expect($(element).attr('content')).toBe('0'); + }); + $('meta[property="og:image:alt"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); + + it('should handle text with quote with multiple images', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/pfrazee.com/post/3m4vhttbo6k26', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Log all meta tags for debugging + const metaTags = $('head meta') + .map((_, el) => ({ + name: $(el).attr('name'), + property: $(el).attr('property'), + content: $(el).attr('content') + })) + .get(); + console.log('Text with quote (multiple images) meta tags:', JSON.stringify(metaTags, null, 2)); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/pfrazee.com/post/3m4vhttbo6k26'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/pfrazee.com/post/3m4vhttbo6k26'); + expect($('meta[property="og:description"]').attr('content')).toContain('🗨️Quoting'); + + expect($('meta[name="twitter:card"]').length).toBe(2); + $('meta[name="twitter:card"]').each((_, element) => { + expect($(element).attr('content')).toBe('summary_large_image'); + }); + $('meta[property="twitter:image"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image:secure_url"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + $('meta[property="og:image:type"]').each((_, element) => { + expect($(element).attr('content')).toBe('image/jpeg'); + }); + $('meta[property="og:image:width"]').each((_, element) => { + expect($(element).attr('content')).toBe('0'); + }); + $('meta[property="og:image:height"]').each((_, element) => { + expect($(element).attr('content')).toBe('0'); + }); + $('meta[property="og:image:alt"]').each((_, element) => { + expect($(element).attr('content')).toBeDefined(); + }); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); + + it('should handle image with text quote', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/pfrazee.com/post/3m6fypvdct22z', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/pfrazee.com/post/3m6fypvdct22z'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/pfrazee.com/post/3m6fypvdct22z'); + expect($('meta[property="og:description"]').attr('content')).toBeDefined(); + expect($('meta[name="twitter:card"]').attr('content')).toBe('summary_large_image'); + expect($('meta[property="twitter:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:secure_url"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:type"]').attr('content')).toBe('image/jpeg'); + expect($('meta[property="og:image:width"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:height"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:alt"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); + + it('should handle text with image quote', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/guabiroba.bsky.social/post/3l5rv72jtwo22', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/guabiroba.bsky.social/post/3l5rv72jtwo22'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe( + '0; url = https://bsky.app/profile/guabiroba.bsky.social/post/3l5rv72jtwo22' + ); + expect($('meta[property="og:description"]').attr('content')).toContain('Quoting:'); + expect($('meta[name="twitter:card"]').attr('content')).toBe('summary_large_image'); + expect($('meta[property="twitter:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:secure_url"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:type"]').attr('content')).toBe('image/jpeg'); + expect($('meta[property="og:image:width"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:height"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:alt"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toContain(''); + }); + + it('should handle text with image quote 2', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/danabra.mov/post/3matfymosas2e', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/danabra.mov/post/3matfymosas2e'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/danabra.mov/post/3matfymosas2e'); + expect($('meta[property="og:description"]').attr('content')).toContain('🗨️Quoting:'); + expect($('meta[name="twitter:card"]').attr('content')).toBe('summary_large_image'); + expect($('meta[property="twitter:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:secure_url"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:type"]').attr('content')).toBe('image/jpeg'); + expect($('meta[property="og:image:width"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:height"]').attr('content')).toBe('0'); + expect($('meta[property="og:image:alt"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); + + it('should handle text with video quote', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/pdsls.dev/post/3m3djmmtlac2t', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/pdsls.dev/post/3m3djmmtlac2t'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/pdsls.dev/post/3m3djmmtlac2t'); + // expect($('meta[property="og:description"]').attr('content')).toContain('Quoting:'); + + // This could be either a player card (if the video is directly embedded) or summary_large_image (if just showing thumbnail) + expect($('meta[name="twitter:card"]').attr('content')).toBe('player'); + expect($('meta[name="twitter:player:width"]').attr('content')).toBe('0'); + expect($('meta[name="twitter:player:height"]').attr('content')).toBe('0'); + expect($('meta[name="twitter:player:stream"]').attr('content')).toBeDefined(); + expect($('meta[name="twitter:player:stream:content_type"]').attr('content')).toBeDefined(); + expect($('meta[property="og:video"]').attr('content')).toBeDefined(); + expect($('meta[property="og:video:secure_url"]').attr('content')).toBeDefined(); + expect($('meta[property="og:video:type"]').attr('content')).toBeDefined(); + expect($('meta[property="og:video:width"]').attr('content')).toBe('0'); + expect($('meta[property="og:video:height"]').attr('content')).toBe('0'); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toContain('🗨️Quoting'); + }); + + it('should handle text with special character (%)', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/jefinhomenes.bsky.social/post/3l5qdyb7kzh26?abc=123', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/jefinhomenes.bsky.social/post/3l5qdyb7kzh26'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe( + '0; url = https://bsky.app/profile/jefinhomenes.bsky.social/post/3l5qdyb7kzh26' + ); + // expect($('meta[property="og:description"]').attr('content')).toContain('%'); + + // This is a text-only post, so it should have a basic card type + expect($('meta[name="twitter:card"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); + + it('should handle text with special character (")', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/jamellebouie.net/post/3l7jqhbl3yg26', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/jamellebouie.net/post/3l7jqhbl3yg26'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/jamellebouie.net/post/3l7jqhbl3yg26'); + + // Cheerio unescapes entities when using .attr('content') + // expect($('meta[property="og:description"]').attr('content')).toContain('"'); + + // This is a text-only post, so it should have a basic card type + expect($('meta[name="twitter:card"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).not.contains('"'); + expect(oembed.author_name).toContain('"'); + }); + + it('should handle direct image with index', async () => { + const ctx = createExecutionContext(); + // Using the d. prefix in Host header to trigger direct media middleware + const res = await app.request( + '/profile/pfrazee.com/post/3lech75aa7k2f/4', + { + headers: { + 'User-Agent': userAgent, + Host: 'd.bskye.app' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(302); + const location = res.headers.get('Location'); + expect(location).toBeDefined(); + expect(location).toContain('cdn.bsky.app'); + }); + + it('should handle profile route', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/pfrazee.com', + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + const $ = cheerio.load(await res.text()); + + // Validate each meta tag individually + expect($('meta[name="theme-color"]').attr('content')).toBe('#0a7aff'); + expect($('meta[name="twitter:title"]').attr('content')).toBeDefined(); + expect($('meta[name="twitter:title"]').attr('content')).toContain('Paul Frazee'); + expect($('meta[property="og:site_name"]').attr('content')).toBe('bskye'); + expect($('meta[property="og:url"]').attr('content')).toBe('https://bsky.app/profile/pfrazee.com/'); + expect($('meta[http-equiv="refresh"]').attr('content')).toBe('0; url = https://bsky.app/profile/pfrazee.com/'); + expect($('meta[property="og:description"]').attr('content')).toBeDefined(); + expect($('meta[name="twitter:card"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image"]').attr('content')).toBeDefined(); + expect($('meta[property="og:image:secure_url"]').attr('content')).toBeDefined(); + + const oembedElement = $('link[type="application/json+oembed"]'); + const oembed = await validateOembed(oembedElement); + expect(oembed.author_name).toBe(''); + }); +}); + +type OEmbedResponse = { + author_name: string; + author_url: string; + provider_name: string; + provider_url: string; + title: string; + type: string; + version: string; +}; + +const validateOembed = async (oembedElement: cheerio.Cheerio): Promise => { + expect(oembedElement.attr('href')).toBeDefined(); + expect(oembedElement.attr('title')).toBeDefined(); + expect(oembedElement.attr('rel')).toBe('alternate'); + const fullOembedUrl = new URL(oembedElement.attr('href')!); + const oembedPath = fullOembedUrl.pathname + fullOembedUrl.search; + + const ctx = createExecutionContext(); + const res = await app.request( + oembedPath, + { + headers: { 'User-Agent': userAgent } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + expect(res.status).toBe(200); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const oembed: any = await res.json(); + + expect(oembed.author_name).toBeDefined(); + expect(oembed.author_url).toBeDefined(); + expect(oembed.provider_name).toBeDefined(); + expect(oembed.provider_url).toBeDefined(); + expect(oembed.title).toBeDefined(); + expect(oembed.type).toBe('link'); + expect(oembed.version).toBe('1.0'); + + return { + author_name: oembed.author_name, + author_url: oembed.author_url, + provider_name: oembed.provider_name, + provider_url: oembed.provider_url, + title: oembed.title, + type: oembed.type, + version: oembed.version + }; +}; diff --git a/test/index.spec.ts b/test/index.spec.ts index dae4a71..72d344a 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,10 +1,12 @@ -import {} from 'cloudflare:test'; +import { env } from 'cloudflare:test'; import { describe, expect, it } from 'vitest'; +import app from '../src'; -const IncomingRequest = Request; +describe('Example', () => { + it('Should return 200 response', async () => { + const res = await app.request('/', {}, env); -describe('worker', () => { - it('success', async () => { - expect('hello').toBe('hello'); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('https://github.com/FerroEduardo/bskye'); }); }); diff --git a/test/post/discord.spec.ts b/test/post/discord.spec.ts new file mode 100644 index 0000000..a869374 --- /dev/null +++ b/test/post/discord.spec.ts @@ -0,0 +1,546 @@ +import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import app from '../../src'; +import type { ThreadViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs'; +import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; +import * as bluesky from '../../src/bluesky'; +import * as cheerio from 'cheerio'; + +vi.mock('../../src/bluesky'); + +const mockAuthor: ProfileViewDetailed = { + did: 'did:plc:z72i4hdhw56rfsilqqqyqj2m', + handle: 'example.bsky.social', + displayName: 'Example User', + avatar: 'https://example.com/avatar.jpg', + description: 'This is a test profile', + followersCount: 100, + followsCount: 50, + postsCount: 25, + indexedAt: '2024-01-01T00:00:00.000Z', + createdAt: '2023-01-01T00:00:00.000Z' +}; + +function validateMetaTags( + $: cheerio.CheerioAPI, + expected: { + title: string; + description?: string; + image?: string; + images?: string[]; + video?: string; + card: string; + } +) { + // Log all meta tags for debugging + console.log( + 'Discord meta tags:', + $('head meta') + .map((_, el) => ({ + name: $(el).attr('name'), + property: $(el).attr('property'), + content: $(el).attr('content') + })) + .get() + ); + + const actualTitle = $('meta[name="twitter:title"]').attr('content'); + expect(actualTitle).toBe(expected.title); + + if (expected.description) { + const actualDescription = $('meta[property="og:description"]').attr('content'); + expect(actualDescription).toContain(expected.description); + } + + const actualCard = $('meta[name="twitter:card"]').attr('content'); + expect(actualCard).toBe(expected.card); + + if (expected.image) { + const actualImage = $('meta[property="og:image"]').attr('content'); + expect(actualImage).toBe(expected.image); + } + + if (expected.images) { + $('meta[property="og:image"]').each((_, element) => { + const actualImage = $(element).attr('content'); + expect(expected.images?.includes(actualImage!)).toBe(true); + }); + } + + if (expected.video) { + const actualVideo = $('meta[name="twitter:player:stream"]').attr('content'); + expect(actualVideo).toContain(expected.video); + } +} + +describe('Post Route - Discord', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should redirect to bsky.app when no platform user agent', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'unknown' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('https://bsky.app/profile/example.bsky.social/post/123'); + }); + + it('should return 400 when post is not found', async () => { + vi.mocked(bluesky.getPostThread).mockRejectedValue(new Error('Post not found')); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/nonexistent', + { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(400); + expect(res.headers.get('Content-Type')).toContain('application/json'); + expect(await res.json()).toEqual({ message: 'Failed to get post from Bluesky API' }); + }); + + it('should render text-only post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'This is a test post', + createdAt: '2024-01-01T00:00:00.000Z' + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(res.headers.get('platform-name')).toBe('discord'); + + const $ = cheerio.load(await res.text()); + + const title = $('meta[name="twitter:title"]').attr('content'); + expect(title).toBe('Example User (@example.bsky.social)'); + + const description = $('meta[property="og:description"]').attr('content'); + expect(description).toBe('This is a test post'); + }); + + it('should render image post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Check out this image', + embed: { + $type: 'app.bsky.embed.images', + images: [ + { + alt: 'A beautiful sunset', + image: { + ref: { $link: 'mock-image-cid' }, + mimeType: 'image/jpeg', + size: 1024 + } + } + ] + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://example.com/thumb.jpg', + fullsize: 'https://example.com/fullsize.jpg@jpeg', + alt: 'A beautiful sunset' + } + ] + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + description: 'Check out this image', + image: 'https://example.com/fullsize.jpg@jpeg', + card: 'summary_large_image' + }); + + // Check image-specific meta tags + const imageType = $('meta[property="og:image:type"]').attr('content'); + expect(imageType).toBe('image/jpeg'); + + const imageAlt = $('meta[property="og:image:alt"]').attr('content'); + expect(imageAlt).toBe('A beautiful sunset'); + }); + + it('should render multiple image post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Check out this image', + embed: { + $type: 'app.bsky.embed.images', + images: [ + { + alt: 'A beautiful sunset', + image: { + ref: { $link: 'mock-image-cid' }, + mimeType: 'image/jpeg', + size: 1024 + } + }, + { + alt: 'A beautiful night sky', + image: { + ref: { $link: 'mock-image-cid' }, + mimeType: 'image/jpeg', + size: 512 + } + } + ] + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://example.com/thumb.jpg', + fullsize: 'https://example.com/fullsize.jpg@jpeg', + alt: 'A beautiful sunset' + }, + { + thumb: 'https://example.com/thumb2.jpg', + fullsize: 'https://example.com/fullsize2.jpg@jpeg', + alt: 'A beautiful night sky' + } + ] + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + description: 'Check out this image', + images: ['https://example.com/fullsize.jpg@jpeg', 'https://example.com/fullsize2.jpg@jpeg'], + card: 'summary_large_image' + }); + + // Check image-specific meta tags + const imageType = $('meta[property="og:image:type"]').attr('content'); + expect(imageType).toBe('image/jpeg'); + + const imageAlt = $('meta[property="og:image:alt"]').attr('content'); + expect(imageAlt).toBe('A beautiful sunset'); + }); + + it('should render video post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Check out this video', + embed: { + $type: 'app.bsky.embed.video', + video: { + ref: { $link: 'mock-video-cid' }, + mimeType: 'video/mp4', + size: 1024000 + } + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.video#view', + cid: 'mock-video-cid', + playlist: 'https://video.example.com/playlist.m3u8' + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + video: 'https://bsky.social/xrpc/com.atproto.sync.getBlob?did=did:plc:z72i4hdhw56rfsilqqqyqj2m&cid=mock-video-cid&r=', + card: 'player' + }); + + // Check video-specific meta tags + const videoType = $('meta[name="twitter:player:stream:content_type"]').attr('content'); + expect(videoType).toBe('video/mp4'); + + const videoWidth = $('meta[name="twitter:player:width"]').attr('content'); + expect(videoWidth).toBe('0'); + }); + + it('should render GIF post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Check out this GIF', + embed: { + $type: 'app.bsky.embed.external', + external: { + uri: 'https://media.tenor.com/1234567890/tenor.gif', + title: 'Funny GIF', + description: 'A funny animated GIF' + } + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.external#view', + external: { + uri: 'https://media.tenor.com/1234567890/tenor.gif', + title: 'Funny GIF', + description: 'A funny animated GIF', + thumb: 'https://media.tenor.com/1234567890/thumb.jpg' + } + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + description: 'Check out this GIF', + image: 'https://media.tenor.com/1234567890/tenor.gif', + card: 'summary_large_image' + }); + + // Check GIF-specific meta tags + const imageType = $('meta[property="og:image:type"]').attr('content'); + expect(imageType).toBe('image/jpeg'); + + const imageAlt = $('meta[property="og:image:alt"]').attr('content'); + expect(imageAlt).toBe('Funny GIF'); + }); + + it('should handle direct media access with mediaIndex', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Multiple images', + embed: { + $type: 'app.bsky.embed.images', + images: [ + { + alt: 'Image 1', + image: { + ref: { $link: 'image-cid-1' }, + mimeType: 'image/jpeg', + size: 1024 + } + }, + { + alt: 'Image 2', + image: { + ref: { $link: 'image-cid-2' }, + mimeType: 'image/jpeg', + size: 1024 + } + } + ] + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://example.com/thumb1.jpg', + fullsize: 'https://example.com/image1.jpg@jpeg', + alt: 'Image 1' + }, + { + thumb: 'https://example.com/thumb2.jpg', + fullsize: 'https://example.com/image2.jpg@jpeg', + alt: 'Image 2' + } + ] + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123/2', + { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)', + Host: 'd.example.com' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('https://example.com/image2.jpg@jpeg'); + }); +}); diff --git a/test/post/whatsapp.spec.ts b/test/post/whatsapp.spec.ts new file mode 100644 index 0000000..59526df --- /dev/null +++ b/test/post/whatsapp.spec.ts @@ -0,0 +1,445 @@ +import { createExecutionContext, env, waitOnExecutionContext } from 'cloudflare:test'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import app from '../../src'; +import type { ThreadViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs'; +import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; +import * as bluesky from '../../src/bluesky'; +import * as cheerio from 'cheerio'; + +vi.mock('../../src/bluesky'); + +const mockAuthor: ProfileViewDetailed = { + did: 'did:plc:z72i4hdhw56rfsilqqqyqj2m', + handle: 'example.bsky.social', + displayName: 'Example User', + avatar: 'https://example.com/avatar.jpg', + description: 'This is a test profile', + followersCount: 100, + followsCount: 50, + postsCount: 25, + indexedAt: '2024-01-01T00:00:00.000Z', + createdAt: '2023-01-01T00:00:00.000Z' +}; + +function validateMetaTags( + $: cheerio.CheerioAPI, + expected: { + title: string; + description?: string; + image?: string; + video?: string; + card: string; + } +) { + const actualTitle = $('meta[property="og:title"]').attr('content'); + expect(actualTitle).toBe(expected.title); + + if (expected.description) { + const actualDescription = $('meta[property="og:description"]').attr('content'); + expect(actualDescription).toContain(expected.description); + } + + const actualCard = $('meta[name="twitter:card"]').attr('content'); + expect(actualCard).toBe(expected.card); + + if (expected.image) { + const actualImage = $('meta[property="og:image"]').attr('content'); + expect(actualImage).toBe(expected.image); + } + + if (expected.video) { + const actualVideo = $('meta[property="og:video"]').attr('content'); + // Video URLs have random numbers, so just check it contains the base URL + expect(actualVideo).toContain(expected.video); + } + + // Log all meta tags for debugging + console.log( + 'WhatsApp meta tags:', + $('head meta') + .map((_, el) => ({ + name: $(el).attr('name'), + property: $(el).attr('property'), + content: $(el).attr('content') + })) + .get() + ); +} + +describe('Post Route - WhatsApp', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should redirect to bsky.app when no platform user agent', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'unknown' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('https://bsky.app/profile/example.bsky.social/post/123'); + }); + + it('should return 400 when post is not found', async () => { + vi.mocked(bluesky.getPostThread).mockRejectedValue(new Error('Post not found')); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/nonexistent', + { + method: 'GET', + headers: { + 'User-Agent': 'WhatsApp/2.23.20.0' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(400); + expect(res.headers.get('Content-Type')).toContain('application/json'); + expect(await res.json()).toEqual({ message: 'Failed to get post from Bluesky API' }); + }); + + it('should render text-only post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'This is a test post', + createdAt: '2024-01-01T00:00:00.000Z' + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'WhatsApp/2.23.20.0' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(res.headers.get('platform-name')).toBe('whatsapp'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + description: 'This is a test post', + card: 'summary_large_image' + }); + }); + + it('should render image post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Check out this image', + embed: { + $type: 'app.bsky.embed.images', + images: [ + { + alt: 'A beautiful sunset', + image: { + ref: { $link: 'mock-image-cid' }, + mimeType: 'image/jpeg', + size: 1024 + } + } + ] + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://example.com/thumb.jpg', + fullsize: 'https://example.com/fullsize.jpg@jpeg', + alt: 'A beautiful sunset' + } + ] + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'WhatsApp/2.23.20.0' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + description: 'Check out this image', + image: 'https://example.com/fullsize.jpg@jpeg', + card: 'summary_large_image' + }); + + // Check image-specific meta tags + const imageType = $('meta[property="og:image:type"]').attr('content'); + expect(imageType).toBe('image/jpeg'); + + const imageAlt = $('meta[property="og:image:alt"]').attr('content'); + expect(imageAlt).toBe('A beautiful sunset'); + }); + + it('should render video post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Check out this video', + embed: { + $type: 'app.bsky.embed.video', + video: { + ref: { $link: 'mock-video-cid' }, + mimeType: 'video/mp4', + size: 1024000 + } + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.video#view', + cid: 'mock-video-cid', + playlist: 'https://video.example.com/playlist.m3u8' + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'WhatsApp/2.23.20.0' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + description: 'Check out this video', + video: 'https://bsky.social/xrpc/com.atproto.sync.getBlob?did=did:plc:z72i4hdhw56rfsilqqqyqj2m&cid=mock-video-cid&r=', + card: 'summary_large_image' + }); + + // Check video-specific meta tags + const videoType = $('meta[property="og:video:type"]').attr('content'); + expect(videoType).toBe('video/mp4'); + }); + + it('should render GIF post', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Check out this GIF', + embed: { + $type: 'app.bsky.embed.external', + external: { + uri: 'https://media.tenor.com/1234567890/tenor.gif', + title: 'Funny GIF', + description: 'A funny animated GIF' + } + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.external#view', + external: { + uri: 'https://media.tenor.com/1234567890/tenor.gif', + title: 'Funny GIF', + description: 'A funny animated GIF', + thumb: 'https://media.tenor.com/1234567890/thumb.jpg' + } + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123', + { + method: 'GET', + headers: { + 'User-Agent': 'WhatsApp/2.23.20.0' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + + const $ = cheerio.load(await res.text()); + validateMetaTags($, { + title: 'Example User (@example.bsky.social)', + card: 'summary_large_image' + }); + + // Check GIF-specific meta tags + const imageType = $('meta[property="og:image:type"]').attr('content'); + expect(imageType).toBe('image/jpeg'); // WhatsApp template uses jpeg for GIFs + + const imageAlt = $('meta[property="og:image:alt"]').attr('content'); + expect(imageAlt).toBe('Funny GIF'); + }); + + it('should handle direct media access with mediaIndex', async () => { + const mockThread: ThreadViewPost = { + post: { + uri: 'at://example.bsky.social/app.bsky.feed.post/123', + cid: 'mock-cid', + author: mockAuthor, + record: { + $type: 'app.bsky.feed.post', + text: 'Multiple images', + embed: { + $type: 'app.bsky.embed.images', + images: [ + { + alt: 'Image 1', + image: { + ref: { $link: 'image-cid-1' }, + mimeType: 'image/jpeg', + size: 1024 + } + }, + { + alt: 'Image 2', + image: { + ref: { $link: 'image-cid-2' }, + mimeType: 'image/jpeg', + size: 1024 + } + } + ] + }, + createdAt: '2024-01-01T00:00:00.000Z' + }, + embed: { + $type: 'app.bsky.embed.images#view', + images: [ + { + thumb: 'https://example.com/thumb1.jpg', + fullsize: 'https://example.com/image1.jpg@jpeg', + alt: 'Image 1' + }, + { + thumb: 'https://example.com/thumb2.jpg', + fullsize: 'https://example.com/image2.jpg@jpeg', + alt: 'Image 2' + } + ] + }, + replyCount: 5, + repostCount: 3, + likeCount: 10, + indexedAt: '2024-01-01T00:00:00.000Z' + } + }; + + vi.mocked(bluesky.getPostThread).mockResolvedValue(mockThread); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social/post/123/2', + { + method: 'GET', + headers: { + 'User-Agent': 'WhatsApp/2.23.20.0', + Host: 'd.example.com' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('https://example.com/image2.jpg@jpeg'); + }); +}); diff --git a/test/profile/discord.spec.ts b/test/profile/discord.spec.ts new file mode 100644 index 0000000..b5f42af --- /dev/null +++ b/test/profile/discord.spec.ts @@ -0,0 +1,293 @@ +import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import app from '../../src'; +import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; +import * as bluesky from '../../src/bluesky'; +import * as cheerio from 'cheerio'; +import { toUSVString } from 'node:util'; + +vi.mock('../../src/bluesky'); + +const mockProfile: ProfileViewDetailed = { + did: 'did:plc:z72i4hdhw56rfsilqqqyqj2m', + handle: 'example.bsky.social', + displayName: 'Example User', + description: 'This is a test profile', + followersCount: 100, + followsCount: 50, + postsCount: 25, + indexedAt: '2024-01-01T00:00:00.000Z', + createdAt: '2023-01-01T00:00:00.000Z' +}; + +const mockProfileWithAvatar: ProfileViewDetailed = { + ...mockProfile, + avatar: 'https://example.com/avatar.jpg' +}; + +const userAgent = 'Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)'; + +describe('Profile Route - Discord', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should redirect to bsky.app when no platform user agent', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': 'unknown' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('https://bsky.app/profile/example.bsky.social'); + }); + + it('should return 400 when is not found', async () => { + vi.mocked(bluesky.getProfile).mockRejectedValue(new Error('Profile not found')); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/nonexistent.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(400); + expect(res.headers.get('Content-Type')).toContain('application/json'); + expect(await res.json()).toEqual({ message: 'Failed to get user from Bluesky API' }); + }); + + it('should set platform-name header for Discord user agent', async () => { + vi.mocked(bluesky.getProfile).mockResolvedValue(mockProfile); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(res.headers.get('platform-name')).toBe('discord'); + + const $ = cheerio.load(await res.text()); + + // Validate meta tags + expect(validateProfile($)).toBe(true); + }); + + it('should call getProfile with correct actor parameter', async () => { + vi.mocked(bluesky.getProfile).mockResolvedValue(mockProfile); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(bluesky.getProfile).toHaveBeenCalledWith('example.bsky.social'); + + const $ = cheerio.load(await res.text()); + + // Validate meta tags + expect(validateProfile($)).toBe(true); + }); + + it('should set meta tags for the avatar', async () => { + vi.mocked(bluesky.getProfile).mockResolvedValue(mockProfileWithAvatar); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(res.headers.get('platform-name')).toBe('discord'); + + const $ = cheerio.load(await res.text()); + + // Validate meta tags + expect(validateProfile($, true)).toBe(true); + }); +}); + +export const validateProfile = (html: cheerio.CheerioAPI, hasAvatar: boolean = false): boolean => { + const values = [ + { + attributeName: 'name', + attributeValue: 'theme-color', + content: '#0a7aff' + }, + { + attributeName: 'name', + attributeValue: 'twitter:title', + content: 'Example User (@example.bsky.social)' + }, + { + attributeName: 'property', + attributeValue: 'og:description', + content: 'This is a test profile' + }, + { + attributeName: 'property', + attributeValue: 'og:site_name', + content: 'bskye' + }, + { + attributeName: 'property', + attributeValue: 'og:url', + content: 'https://bsky.app/profile/example.bsky.social/' + }, + { + attributeName: 'http-equiv', + attributeValue: 'refresh', + content: '0; url = https://bsky.app/profile/example.bsky.social/' + } + ]; + + if (hasAvatar) { + values.push( + { + attributeName: 'name', + attributeValue: 'twitter:card', + content: 'summary_large_image' + }, + { + attributeName: 'property', + attributeValue: 'twitter:image', + content: 'https://example.com/avatar.jpg' + }, + { + attributeName: 'property', + attributeValue: 'og:image', + content: 'https://example.com/avatar.jpg' + }, + { + attributeName: 'property', + attributeValue: 'og:image:secure_url', + content: 'https://example.com/avatar.jpg' + }, + { + attributeName: 'property', + attributeValue: 'og:image:type', + content: 'image/jpeg' + }, + { + attributeName: 'property', + attributeValue: 'og:image:width', + content: '0' + }, + { + attributeName: 'property', + attributeValue: 'og:image:height', + content: '0' + }, + { + attributeName: 'property', + attributeValue: 'og:image:alt', + content: 'Example User' + } + ); + } + + for (const value of values) { + const meta = html(`meta[${value.attributeName}="${value.attributeValue}"]`); + if (!meta || meta.length === 0) { + console.error(`Attribute '${value.attributeName}="${value.attributeValue}"' not found`); + return false; + } + const content = meta.attr('content'); + if (content !== value.content) { + console.error(`Attribute '${value.attributeName}="${value.attributeValue}"' has content '${content}', expected '${value.content}'`); + return false; + } + } + + if (!validateOembed(html)) { + return false; + } + + return true; +}; + +const validateOembed = (html: cheerio.CheerioAPI): boolean => { + const link = html('head link'); + if (!link || link.length === 0) { + console.error(`Alternate link tag not found`); + return false; + } + + const rel = link.attr('rel'); + if (rel !== 'alternate') { + console.error(`Alternate link tag has rel '${rel}', expected 'alternate'`); + return false; + } + + const linkType = link.attr('type'); + if (linkType !== 'application/json+oembed') { + console.error(`Alternate link tag has type '${linkType}', expected 'application/json+oembed'`); + return false; + } + + const href = link.attr('href'); + const params = new URLSearchParams({ + author: '', + link: encodeURIComponent(toUSVString('https://bsky.app/profile/example.bsky.social/')), + title: encodeURIComponent(toUSVString('Example User (@example.bsky.social)')), + provider: encodeURIComponent(toUSVString(`👥 100 ➡️ 50 📸 25`)) + }); + const expectedHref = `http://localhost/oembed?${params}`; + if (href !== expectedHref) { + console.error(`Alternate link tag has href '${href}', expected '${expectedHref}'`); + return false; + } + + return true; +}; diff --git a/test/profile/whatsapp.spec.ts b/test/profile/whatsapp.spec.ts new file mode 100644 index 0000000..c97abc6 --- /dev/null +++ b/test/profile/whatsapp.spec.ts @@ -0,0 +1,275 @@ +import { createExecutionContext, env, waitOnExecutionContext } from 'cloudflare:test'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import app from '../../src'; +import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; +import * as bluesky from '../../src/bluesky'; +import * as cheerio from 'cheerio'; + +vi.mock('../../src/bluesky'); + +const mockProfile: ProfileViewDetailed = { + did: 'did:plc:z72i4hdhw56rfsilqqqyqj2m', + handle: 'example.bsky.social', + displayName: 'Example User', + description: 'This is a test profile', + followersCount: 100, + followsCount: 50, + postsCount: 25, + indexedAt: '2024-01-01T00:00:00.000Z', + createdAt: '2023-01-01T00:00:00.000Z' +}; + +const mockProfileWithAvatar: ProfileViewDetailed = { + ...mockProfile, + avatar: 'https://example.com/avatar.jpg' +}; + +const userAgent = 'WhatsApp/2.23.20.0'; + +describe('Profile Route - WhatsApp', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should redirect to bsky.app when no platform user agent', async () => { + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': 'unknown' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('https://bsky.app/profile/example.bsky.social'); + }); + + it('should return 400 when profile is not found', async () => { + vi.mocked(bluesky.getProfile).mockRejectedValue(new Error('Profile not found')); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/nonexistent.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(400); + expect(res.headers.get('Content-Type')).toContain('application/json'); + expect(await res.json()).toEqual({ message: 'Failed to get user from Bluesky API' }); + }); + + it('should set platform-name header for WhatsApp user agent', async () => { + vi.mocked(bluesky.getProfile).mockResolvedValue(mockProfile); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(res.headers.get('platform-name')).toBe('whatsapp'); + + const $ = cheerio.load(await res.text()); + + // Validate meta tags + expect(validateProfile($)).toBe(true); + }); + + it('should call getProfile with correct actor parameter', async () => { + vi.mocked(bluesky.getProfile).mockResolvedValue(mockProfile); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(bluesky.getProfile).toHaveBeenCalledWith('example.bsky.social'); + + const $ = cheerio.load(await res.text()); + + // Validate meta tags + expect(validateProfile($)).toBe(true); + }); + + it('should work with TelegramBot user agent (mapped to WhatsApp)', async () => { + vi.mocked(bluesky.getProfile).mockResolvedValue(mockProfile); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': 'TelegramBot (like TwitterBot)' + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(res.headers.get('platform-name')).toBe('whatsapp'); + + const $ = cheerio.load(await res.text()); + + // Validate meta tags + expect(validateProfile($)).toBe(true); + }); + + it('should set meta tags for the avatar', async () => { + vi.mocked(bluesky.getProfile).mockResolvedValue(mockProfileWithAvatar); + + const ctx = createExecutionContext(); + const res = await app.request( + '/profile/example.bsky.social', + { + method: 'GET', + headers: { + 'User-Agent': userAgent + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toContain('text/html'); + expect(res.headers.get('platform-name')).toBe('whatsapp'); + + const $ = cheerio.load(await res.text()); + + // Validate meta tags + expect(validateProfile($, true)).toBe(true); + }); +}); + +export const validateProfile = (html: cheerio.CheerioAPI, hasAvatar: boolean = false): boolean => { + const values = [ + { + attributeName: 'name', + attributeValue: 'theme-color', + content: '#0a7aff' + }, + { + attributeName: 'property', + attributeValue: 'og:title', + content: 'Example User (@example.bsky.social)' + }, + { + attributeName: 'property', + attributeValue: 'og:description', + content: 'This is a test profile' + }, + { + attributeName: 'property', + attributeValue: 'og:site_name', + content: 'bskye' + }, + { + attributeName: 'property', + attributeValue: 'og:url', + content: 'https://bsky.app/profile/example.bsky.social/' + }, + { + attributeName: 'http-equiv', + attributeValue: 'refresh', + content: '0; url = https://bsky.app/profile/example.bsky.social/' + }, + { + attributeName: 'name', + attributeValue: 'twitter:card', + content: 'summary_large_image' + } + ]; + + if (hasAvatar) { + values.push( + { + attributeName: 'property', + attributeValue: 'og:image', + content: 'https://example.com/avatar.jpg' + }, + { + attributeName: 'property', + attributeValue: 'og:image:secure_url', + content: 'https://example.com/avatar.jpg' + }, + { + attributeName: 'property', + attributeValue: 'og:image:type', + content: 'image/jpeg' + }, + { + attributeName: 'property', + attributeValue: 'og:image:width', + content: '600' + }, + { + attributeName: 'property', + attributeValue: 'og:image:height', + content: '600' + }, + { + attributeName: 'property', + attributeValue: 'og:image:alt', + content: 'Example User' + } + ); + } + + for (const value of values) { + const meta = html(`meta[${value.attributeName}="${value.attributeValue}"]`); + if (!meta || meta.length === 0) { + console.error(`Attribute '${value.attributeName}="${value.attributeValue}"' not found`); + return false; + } + const content = meta.attr('content'); + if (content !== value.content) { + console.error(`Attribute '${value.attributeName}="${value.attributeValue}"' has content '${content}', expected '${value.content}'`); + return false; + } + } + + return true; +}; diff --git a/vitest.config.mts b/vitest.config.mts index 98b629f..7ecc769 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -2,6 +2,7 @@ import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; export default defineWorkersConfig({ test: { + globals: true, poolOptions: { workers: { wrangler: { configPath: './wrangler.toml' }