├── .envrc ├── .github ├── dependabot.yml └── workflows │ └── deploy.yml ├── .gitignore ├── .tool-versions ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json ├── src └── index.ts ├── tsconfig.json └── wrangler.toml /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | direnv_load mise direnv exec 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # See https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | dependabot: 10 | patterns: 11 | - "*" 12 | - package-ecosystem: "npm" 13 | versioning-strategy: "lockfile-only" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | groups: 18 | dependabot: 19 | patterns: 20 | - "*" 21 | - package-ecosystem: "elm" 22 | directory: "/" 23 | schedule: 24 | interval: "weekly" 25 | groups: 26 | dependabot: 27 | patterns: 28 | - "*" 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | repository_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Publish 16 | uses: cloudflare/wrangler-action@v3.14.1 17 | with: 18 | preCommands: | 19 | npm ci 20 | command: publish 21 | apiToken: ${{ secrets.CF_API_TOKEN }} 22 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | .wrangler 171 | .dev.vars 172 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.10.0 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yu Matsuzawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # link-preview 2 | 3 | ## What is this? 4 | 5 | This is a **light-weight, no-auth "link-preview" service** powered by [Cloudflare Workers](https://www.cloudflare.com/products/workers/) 6 | and [node-html-parser](https://github.com/taoqf/node-html-parser) 7 | 8 | - For small- or single-purpose services, Cloudflare Workers are very good environment to work with. 9 | - It enables small-start from free tier, with generous feature offerings 10 | - It allows us exactly what we want in this context: writing in TypeScript, serving public HTTP APIs to the world 11 | - It has well-made development experience: rich documents, wrangler CLI, easy deploy from GitHub Actions, consice user-land directory structure, etc. 12 | - `node-html-parser` fits here as well. 13 | - Reasonably small set of dependencies 14 | - Sufficiently fast (subjectively though, not benchmarked) 15 | - Well-known `querySelector` API 16 | 17 | ## Why a Service, not a Library? 18 | 19 | There are libraries like [link-preview-js](https://github.com/ospfranco/link-preview-js) and they serve the purpose as long as you know what you are doing. 20 | 21 | However, there are [gotchas](https://github.com/ospfranco/link-preview-js#gotchas): 22 | 23 | - When you attempt to fetch a website of another origin from JavaScript run on a web browser, the browser ask the targeted website for **cross-origin request allowances (CORS; Cross-Origin Resource Sharing)** due to the same-origin policies 24 | - However, not all websites allow CORS. Or rather, most websites just don't (default behavior of ordinary web servers) 25 | - In this scenario, non-browser agent must fetch the website, and pass the result to the initiating script 26 | - **This service exactly does that** 27 | - Fetch websites of another domain on behalf of browser-run script 28 | - Extract essential info for preview purpose from the website (`title` and `url`, optional `description` and `image`) 29 | - Give it back to the requester in JSON format 30 | 31 | This service itself **allows** CORS, so you can just pitch requests from whatever environment and they should work. 32 | 33 | Also it comes in rescue when your preferred language does not have well-maintained link-preview libraries. 34 | You just need some HTTP client capability and JSON handling. 35 | 36 | ## How to deploy 37 | 38 | 1. Prepare your Cloudflare Account 39 | 2. Click: [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/ymtszw/link-preview) 40 | - It should fork this repository and set up your Cloudflare Account, then deploys the service 41 | 42 | ## Develop locally 43 | 44 | ```sh 45 | npm install 46 | npm start 47 | ``` 48 | 49 | Then, from another terminal: 50 | 51 | ```sh 52 | $ curl 'http://localhost:8787?q=https://cloudflare.com' | jq . 53 | { 54 | "title": "Cloudflare - The Web Performance & Security Company", 55 | "description": "Here at Cloudflare, we make the Internet work the way it should. Offering CDN, DNS, DDoS protection and security, find out how we can help your site.", 56 | "url": "https://www.cloudflare.com/", 57 | "image": "https://www.cloudflare.com/static/b30a57477bde900ba55c0b5f98c4e524/Cloudflare_default_OG_.png" 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link-preview", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "link-preview", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "node-html-parser": "^6.1" 13 | }, 14 | "devDependencies": { 15 | "@cloudflare/workers-types": "^4.20221111", 16 | "typescript": "^5.4", 17 | "wrangler": "^3.52" 18 | } 19 | }, 20 | "node_modules/@cloudflare/kv-asset-handler": { 21 | "version": "0.3.4", 22 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", 23 | "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", 24 | "dev": true, 25 | "dependencies": { 26 | "mime": "^3.0.0" 27 | }, 28 | "engines": { 29 | "node": ">=16.13" 30 | } 31 | }, 32 | "node_modules/@cloudflare/unenv-preset": { 33 | "version": "2.0.2", 34 | "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", 35 | "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", 36 | "dev": true, 37 | "license": "MIT OR Apache-2.0", 38 | "peerDependencies": { 39 | "unenv": "2.0.0-rc.14", 40 | "workerd": "^1.20250124.0" 41 | }, 42 | "peerDependenciesMeta": { 43 | "workerd": { 44 | "optional": true 45 | } 46 | } 47 | }, 48 | "node_modules/@cloudflare/workerd-darwin-64": { 49 | "version": "1.20250408.0", 50 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250408.0.tgz", 51 | "integrity": "sha512-bxhIwBWxaNItZLXDNOKY2dCv0FHjDiDkfJFpwv4HvtvU5MKcrivZHVmmfDzLW85rqzfcDOmKbZeMPVfiKxdBZw==", 52 | "cpu": [ 53 | "x64" 54 | ], 55 | "dev": true, 56 | "license": "Apache-2.0", 57 | "optional": true, 58 | "os": [ 59 | "darwin" 60 | ], 61 | "engines": { 62 | "node": ">=16" 63 | } 64 | }, 65 | "node_modules/@cloudflare/workerd-darwin-arm64": { 66 | "version": "1.20250408.0", 67 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250408.0.tgz", 68 | "integrity": "sha512-5XZ2Oykr8bSo7zBmERtHh18h5BZYC/6H1YFWVxEj3PtalF3+6SHsO4KZsbGvDml9Pu7sHV277jiZE5eny8Hlyw==", 69 | "cpu": [ 70 | "arm64" 71 | ], 72 | "dev": true, 73 | "license": "Apache-2.0", 74 | "optional": true, 75 | "os": [ 76 | "darwin" 77 | ], 78 | "engines": { 79 | "node": ">=16" 80 | } 81 | }, 82 | "node_modules/@cloudflare/workerd-linux-64": { 83 | "version": "1.20250408.0", 84 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250408.0.tgz", 85 | "integrity": "sha512-WbgItXWln6G5d7GvYLWcuOzAVwafysZaWunH3UEfsm95wPuRofpYnlDD861gdWJX10IHSVgMStGESUcs7FLerQ==", 86 | "cpu": [ 87 | "x64" 88 | ], 89 | "dev": true, 90 | "license": "Apache-2.0", 91 | "optional": true, 92 | "os": [ 93 | "linux" 94 | ], 95 | "engines": { 96 | "node": ">=16" 97 | } 98 | }, 99 | "node_modules/@cloudflare/workerd-linux-arm64": { 100 | "version": "1.20250408.0", 101 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250408.0.tgz", 102 | "integrity": "sha512-pAhEywPPvr92SLylnQfZEPgXz+9pOG9G9haAPLpEatncZwYiYd9yiR6HYWhKp2erzCoNrOqKg9IlQwU3z1IDiw==", 103 | "cpu": [ 104 | "arm64" 105 | ], 106 | "dev": true, 107 | "license": "Apache-2.0", 108 | "optional": true, 109 | "os": [ 110 | "linux" 111 | ], 112 | "engines": { 113 | "node": ">=16" 114 | } 115 | }, 116 | "node_modules/@cloudflare/workerd-windows-64": { 117 | "version": "1.20250408.0", 118 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250408.0.tgz", 119 | "integrity": "sha512-nJ3RjMKGae2aF2rZ/CNeBvQPM+W5V1SUK0FYWG/uomyr7uQ2l4IayHna1ODg/OHHTEgIjwom0Mbn58iXb0WOcQ==", 120 | "cpu": [ 121 | "x64" 122 | ], 123 | "dev": true, 124 | "license": "Apache-2.0", 125 | "optional": true, 126 | "os": [ 127 | "win32" 128 | ], 129 | "engines": { 130 | "node": ">=16" 131 | } 132 | }, 133 | "node_modules/@cloudflare/workers-types": { 134 | "version": "4.20250601.0", 135 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250601.0.tgz", 136 | "integrity": "sha512-foAgsuo+u+swy5I+xzPwo4MquPhLZW0fuLLsl4uZlZv2k10WziSvZ4wTIkK/AADFtCVRjLNduTT8E/b7DDoInA==", 137 | "dev": true, 138 | "license": "MIT OR Apache-2.0" 139 | }, 140 | "node_modules/@cspotcode/source-map-support": { 141 | "version": "0.8.1", 142 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 143 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 144 | "dev": true, 145 | "license": "MIT", 146 | "dependencies": { 147 | "@jridgewell/trace-mapping": "0.3.9" 148 | }, 149 | "engines": { 150 | "node": ">=12" 151 | } 152 | }, 153 | "node_modules/@emnapi/runtime": { 154 | "version": "1.3.1", 155 | "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", 156 | "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", 157 | "dev": true, 158 | "license": "MIT", 159 | "optional": true, 160 | "dependencies": { 161 | "tslib": "^2.4.0" 162 | } 163 | }, 164 | "node_modules/@esbuild-plugins/node-globals-polyfill": { 165 | "version": "0.2.3", 166 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", 167 | "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", 168 | "dev": true, 169 | "peerDependencies": { 170 | "esbuild": "*" 171 | } 172 | }, 173 | "node_modules/@esbuild-plugins/node-modules-polyfill": { 174 | "version": "0.2.2", 175 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", 176 | "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", 177 | "dev": true, 178 | "dependencies": { 179 | "escape-string-regexp": "^4.0.0", 180 | "rollup-plugin-node-polyfills": "^0.2.1" 181 | }, 182 | "peerDependencies": { 183 | "esbuild": "*" 184 | } 185 | }, 186 | "node_modules/@esbuild/android-arm": { 187 | "version": "0.17.19", 188 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 189 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 190 | "cpu": [ 191 | "arm" 192 | ], 193 | "dev": true, 194 | "optional": true, 195 | "os": [ 196 | "android" 197 | ], 198 | "engines": { 199 | "node": ">=12" 200 | } 201 | }, 202 | "node_modules/@esbuild/android-arm64": { 203 | "version": "0.17.19", 204 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 205 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 206 | "cpu": [ 207 | "arm64" 208 | ], 209 | "dev": true, 210 | "optional": true, 211 | "os": [ 212 | "android" 213 | ], 214 | "engines": { 215 | "node": ">=12" 216 | } 217 | }, 218 | "node_modules/@esbuild/android-x64": { 219 | "version": "0.17.19", 220 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 221 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 222 | "cpu": [ 223 | "x64" 224 | ], 225 | "dev": true, 226 | "optional": true, 227 | "os": [ 228 | "android" 229 | ], 230 | "engines": { 231 | "node": ">=12" 232 | } 233 | }, 234 | "node_modules/@esbuild/darwin-arm64": { 235 | "version": "0.17.19", 236 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 237 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 238 | "cpu": [ 239 | "arm64" 240 | ], 241 | "dev": true, 242 | "optional": true, 243 | "os": [ 244 | "darwin" 245 | ], 246 | "engines": { 247 | "node": ">=12" 248 | } 249 | }, 250 | "node_modules/@esbuild/darwin-x64": { 251 | "version": "0.17.19", 252 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 253 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 254 | "cpu": [ 255 | "x64" 256 | ], 257 | "dev": true, 258 | "optional": true, 259 | "os": [ 260 | "darwin" 261 | ], 262 | "engines": { 263 | "node": ">=12" 264 | } 265 | }, 266 | "node_modules/@esbuild/freebsd-arm64": { 267 | "version": "0.17.19", 268 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 269 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 270 | "cpu": [ 271 | "arm64" 272 | ], 273 | "dev": true, 274 | "optional": true, 275 | "os": [ 276 | "freebsd" 277 | ], 278 | "engines": { 279 | "node": ">=12" 280 | } 281 | }, 282 | "node_modules/@esbuild/freebsd-x64": { 283 | "version": "0.17.19", 284 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 285 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 286 | "cpu": [ 287 | "x64" 288 | ], 289 | "dev": true, 290 | "optional": true, 291 | "os": [ 292 | "freebsd" 293 | ], 294 | "engines": { 295 | "node": ">=12" 296 | } 297 | }, 298 | "node_modules/@esbuild/linux-arm": { 299 | "version": "0.17.19", 300 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 301 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 302 | "cpu": [ 303 | "arm" 304 | ], 305 | "dev": true, 306 | "optional": true, 307 | "os": [ 308 | "linux" 309 | ], 310 | "engines": { 311 | "node": ">=12" 312 | } 313 | }, 314 | "node_modules/@esbuild/linux-arm64": { 315 | "version": "0.17.19", 316 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 317 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 318 | "cpu": [ 319 | "arm64" 320 | ], 321 | "dev": true, 322 | "optional": true, 323 | "os": [ 324 | "linux" 325 | ], 326 | "engines": { 327 | "node": ">=12" 328 | } 329 | }, 330 | "node_modules/@esbuild/linux-ia32": { 331 | "version": "0.17.19", 332 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 333 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 334 | "cpu": [ 335 | "ia32" 336 | ], 337 | "dev": true, 338 | "optional": true, 339 | "os": [ 340 | "linux" 341 | ], 342 | "engines": { 343 | "node": ">=12" 344 | } 345 | }, 346 | "node_modules/@esbuild/linux-loong64": { 347 | "version": "0.17.19", 348 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 349 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 350 | "cpu": [ 351 | "loong64" 352 | ], 353 | "dev": true, 354 | "optional": true, 355 | "os": [ 356 | "linux" 357 | ], 358 | "engines": { 359 | "node": ">=12" 360 | } 361 | }, 362 | "node_modules/@esbuild/linux-mips64el": { 363 | "version": "0.17.19", 364 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 365 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 366 | "cpu": [ 367 | "mips64el" 368 | ], 369 | "dev": true, 370 | "optional": true, 371 | "os": [ 372 | "linux" 373 | ], 374 | "engines": { 375 | "node": ">=12" 376 | } 377 | }, 378 | "node_modules/@esbuild/linux-ppc64": { 379 | "version": "0.17.19", 380 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 381 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 382 | "cpu": [ 383 | "ppc64" 384 | ], 385 | "dev": true, 386 | "optional": true, 387 | "os": [ 388 | "linux" 389 | ], 390 | "engines": { 391 | "node": ">=12" 392 | } 393 | }, 394 | "node_modules/@esbuild/linux-riscv64": { 395 | "version": "0.17.19", 396 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 397 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 398 | "cpu": [ 399 | "riscv64" 400 | ], 401 | "dev": true, 402 | "optional": true, 403 | "os": [ 404 | "linux" 405 | ], 406 | "engines": { 407 | "node": ">=12" 408 | } 409 | }, 410 | "node_modules/@esbuild/linux-s390x": { 411 | "version": "0.17.19", 412 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 413 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 414 | "cpu": [ 415 | "s390x" 416 | ], 417 | "dev": true, 418 | "optional": true, 419 | "os": [ 420 | "linux" 421 | ], 422 | "engines": { 423 | "node": ">=12" 424 | } 425 | }, 426 | "node_modules/@esbuild/linux-x64": { 427 | "version": "0.17.19", 428 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 429 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 430 | "cpu": [ 431 | "x64" 432 | ], 433 | "dev": true, 434 | "optional": true, 435 | "os": [ 436 | "linux" 437 | ], 438 | "engines": { 439 | "node": ">=12" 440 | } 441 | }, 442 | "node_modules/@esbuild/netbsd-x64": { 443 | "version": "0.17.19", 444 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 445 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 446 | "cpu": [ 447 | "x64" 448 | ], 449 | "dev": true, 450 | "optional": true, 451 | "os": [ 452 | "netbsd" 453 | ], 454 | "engines": { 455 | "node": ">=12" 456 | } 457 | }, 458 | "node_modules/@esbuild/openbsd-x64": { 459 | "version": "0.17.19", 460 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 461 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 462 | "cpu": [ 463 | "x64" 464 | ], 465 | "dev": true, 466 | "optional": true, 467 | "os": [ 468 | "openbsd" 469 | ], 470 | "engines": { 471 | "node": ">=12" 472 | } 473 | }, 474 | "node_modules/@esbuild/sunos-x64": { 475 | "version": "0.17.19", 476 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 477 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 478 | "cpu": [ 479 | "x64" 480 | ], 481 | "dev": true, 482 | "optional": true, 483 | "os": [ 484 | "sunos" 485 | ], 486 | "engines": { 487 | "node": ">=12" 488 | } 489 | }, 490 | "node_modules/@esbuild/win32-arm64": { 491 | "version": "0.17.19", 492 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 493 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 494 | "cpu": [ 495 | "arm64" 496 | ], 497 | "dev": true, 498 | "optional": true, 499 | "os": [ 500 | "win32" 501 | ], 502 | "engines": { 503 | "node": ">=12" 504 | } 505 | }, 506 | "node_modules/@esbuild/win32-ia32": { 507 | "version": "0.17.19", 508 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 509 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 510 | "cpu": [ 511 | "ia32" 512 | ], 513 | "dev": true, 514 | "optional": true, 515 | "os": [ 516 | "win32" 517 | ], 518 | "engines": { 519 | "node": ">=12" 520 | } 521 | }, 522 | "node_modules/@esbuild/win32-x64": { 523 | "version": "0.17.19", 524 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 525 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 526 | "cpu": [ 527 | "x64" 528 | ], 529 | "dev": true, 530 | "optional": true, 531 | "os": [ 532 | "win32" 533 | ], 534 | "engines": { 535 | "node": ">=12" 536 | } 537 | }, 538 | "node_modules/@fastify/busboy": { 539 | "version": "2.1.1", 540 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 541 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 542 | "dev": true, 543 | "license": "MIT", 544 | "engines": { 545 | "node": ">=14" 546 | } 547 | }, 548 | "node_modules/@img/sharp-darwin-arm64": { 549 | "version": "0.33.5", 550 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", 551 | "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", 552 | "cpu": [ 553 | "arm64" 554 | ], 555 | "dev": true, 556 | "license": "Apache-2.0", 557 | "optional": true, 558 | "os": [ 559 | "darwin" 560 | ], 561 | "engines": { 562 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 563 | }, 564 | "funding": { 565 | "url": "https://opencollective.com/libvips" 566 | }, 567 | "optionalDependencies": { 568 | "@img/sharp-libvips-darwin-arm64": "1.0.4" 569 | } 570 | }, 571 | "node_modules/@img/sharp-darwin-x64": { 572 | "version": "0.33.5", 573 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", 574 | "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", 575 | "cpu": [ 576 | "x64" 577 | ], 578 | "dev": true, 579 | "license": "Apache-2.0", 580 | "optional": true, 581 | "os": [ 582 | "darwin" 583 | ], 584 | "engines": { 585 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 586 | }, 587 | "funding": { 588 | "url": "https://opencollective.com/libvips" 589 | }, 590 | "optionalDependencies": { 591 | "@img/sharp-libvips-darwin-x64": "1.0.4" 592 | } 593 | }, 594 | "node_modules/@img/sharp-libvips-darwin-arm64": { 595 | "version": "1.0.4", 596 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", 597 | "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", 598 | "cpu": [ 599 | "arm64" 600 | ], 601 | "dev": true, 602 | "license": "LGPL-3.0-or-later", 603 | "optional": true, 604 | "os": [ 605 | "darwin" 606 | ], 607 | "funding": { 608 | "url": "https://opencollective.com/libvips" 609 | } 610 | }, 611 | "node_modules/@img/sharp-libvips-darwin-x64": { 612 | "version": "1.0.4", 613 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", 614 | "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", 615 | "cpu": [ 616 | "x64" 617 | ], 618 | "dev": true, 619 | "license": "LGPL-3.0-or-later", 620 | "optional": true, 621 | "os": [ 622 | "darwin" 623 | ], 624 | "funding": { 625 | "url": "https://opencollective.com/libvips" 626 | } 627 | }, 628 | "node_modules/@img/sharp-libvips-linux-arm": { 629 | "version": "1.0.5", 630 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", 631 | "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", 632 | "cpu": [ 633 | "arm" 634 | ], 635 | "dev": true, 636 | "license": "LGPL-3.0-or-later", 637 | "optional": true, 638 | "os": [ 639 | "linux" 640 | ], 641 | "funding": { 642 | "url": "https://opencollective.com/libvips" 643 | } 644 | }, 645 | "node_modules/@img/sharp-libvips-linux-arm64": { 646 | "version": "1.0.4", 647 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", 648 | "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", 649 | "cpu": [ 650 | "arm64" 651 | ], 652 | "dev": true, 653 | "license": "LGPL-3.0-or-later", 654 | "optional": true, 655 | "os": [ 656 | "linux" 657 | ], 658 | "funding": { 659 | "url": "https://opencollective.com/libvips" 660 | } 661 | }, 662 | "node_modules/@img/sharp-libvips-linux-s390x": { 663 | "version": "1.0.4", 664 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", 665 | "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", 666 | "cpu": [ 667 | "s390x" 668 | ], 669 | "dev": true, 670 | "license": "LGPL-3.0-or-later", 671 | "optional": true, 672 | "os": [ 673 | "linux" 674 | ], 675 | "funding": { 676 | "url": "https://opencollective.com/libvips" 677 | } 678 | }, 679 | "node_modules/@img/sharp-libvips-linux-x64": { 680 | "version": "1.0.4", 681 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", 682 | "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", 683 | "cpu": [ 684 | "x64" 685 | ], 686 | "dev": true, 687 | "license": "LGPL-3.0-or-later", 688 | "optional": true, 689 | "os": [ 690 | "linux" 691 | ], 692 | "funding": { 693 | "url": "https://opencollective.com/libvips" 694 | } 695 | }, 696 | "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 697 | "version": "1.0.4", 698 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", 699 | "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", 700 | "cpu": [ 701 | "arm64" 702 | ], 703 | "dev": true, 704 | "license": "LGPL-3.0-or-later", 705 | "optional": true, 706 | "os": [ 707 | "linux" 708 | ], 709 | "funding": { 710 | "url": "https://opencollective.com/libvips" 711 | } 712 | }, 713 | "node_modules/@img/sharp-libvips-linuxmusl-x64": { 714 | "version": "1.0.4", 715 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", 716 | "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", 717 | "cpu": [ 718 | "x64" 719 | ], 720 | "dev": true, 721 | "license": "LGPL-3.0-or-later", 722 | "optional": true, 723 | "os": [ 724 | "linux" 725 | ], 726 | "funding": { 727 | "url": "https://opencollective.com/libvips" 728 | } 729 | }, 730 | "node_modules/@img/sharp-linux-arm": { 731 | "version": "0.33.5", 732 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", 733 | "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", 734 | "cpu": [ 735 | "arm" 736 | ], 737 | "dev": true, 738 | "license": "Apache-2.0", 739 | "optional": true, 740 | "os": [ 741 | "linux" 742 | ], 743 | "engines": { 744 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 745 | }, 746 | "funding": { 747 | "url": "https://opencollective.com/libvips" 748 | }, 749 | "optionalDependencies": { 750 | "@img/sharp-libvips-linux-arm": "1.0.5" 751 | } 752 | }, 753 | "node_modules/@img/sharp-linux-arm64": { 754 | "version": "0.33.5", 755 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", 756 | "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", 757 | "cpu": [ 758 | "arm64" 759 | ], 760 | "dev": true, 761 | "license": "Apache-2.0", 762 | "optional": true, 763 | "os": [ 764 | "linux" 765 | ], 766 | "engines": { 767 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 768 | }, 769 | "funding": { 770 | "url": "https://opencollective.com/libvips" 771 | }, 772 | "optionalDependencies": { 773 | "@img/sharp-libvips-linux-arm64": "1.0.4" 774 | } 775 | }, 776 | "node_modules/@img/sharp-linux-s390x": { 777 | "version": "0.33.5", 778 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", 779 | "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", 780 | "cpu": [ 781 | "s390x" 782 | ], 783 | "dev": true, 784 | "license": "Apache-2.0", 785 | "optional": true, 786 | "os": [ 787 | "linux" 788 | ], 789 | "engines": { 790 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 791 | }, 792 | "funding": { 793 | "url": "https://opencollective.com/libvips" 794 | }, 795 | "optionalDependencies": { 796 | "@img/sharp-libvips-linux-s390x": "1.0.4" 797 | } 798 | }, 799 | "node_modules/@img/sharp-linux-x64": { 800 | "version": "0.33.5", 801 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", 802 | "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", 803 | "cpu": [ 804 | "x64" 805 | ], 806 | "dev": true, 807 | "license": "Apache-2.0", 808 | "optional": true, 809 | "os": [ 810 | "linux" 811 | ], 812 | "engines": { 813 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 814 | }, 815 | "funding": { 816 | "url": "https://opencollective.com/libvips" 817 | }, 818 | "optionalDependencies": { 819 | "@img/sharp-libvips-linux-x64": "1.0.4" 820 | } 821 | }, 822 | "node_modules/@img/sharp-linuxmusl-arm64": { 823 | "version": "0.33.5", 824 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", 825 | "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", 826 | "cpu": [ 827 | "arm64" 828 | ], 829 | "dev": true, 830 | "license": "Apache-2.0", 831 | "optional": true, 832 | "os": [ 833 | "linux" 834 | ], 835 | "engines": { 836 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 837 | }, 838 | "funding": { 839 | "url": "https://opencollective.com/libvips" 840 | }, 841 | "optionalDependencies": { 842 | "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" 843 | } 844 | }, 845 | "node_modules/@img/sharp-linuxmusl-x64": { 846 | "version": "0.33.5", 847 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", 848 | "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", 849 | "cpu": [ 850 | "x64" 851 | ], 852 | "dev": true, 853 | "license": "Apache-2.0", 854 | "optional": true, 855 | "os": [ 856 | "linux" 857 | ], 858 | "engines": { 859 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 860 | }, 861 | "funding": { 862 | "url": "https://opencollective.com/libvips" 863 | }, 864 | "optionalDependencies": { 865 | "@img/sharp-libvips-linuxmusl-x64": "1.0.4" 866 | } 867 | }, 868 | "node_modules/@img/sharp-wasm32": { 869 | "version": "0.33.5", 870 | "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", 871 | "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", 872 | "cpu": [ 873 | "wasm32" 874 | ], 875 | "dev": true, 876 | "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 877 | "optional": true, 878 | "dependencies": { 879 | "@emnapi/runtime": "^1.2.0" 880 | }, 881 | "engines": { 882 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 883 | }, 884 | "funding": { 885 | "url": "https://opencollective.com/libvips" 886 | } 887 | }, 888 | "node_modules/@img/sharp-win32-ia32": { 889 | "version": "0.33.5", 890 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", 891 | "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", 892 | "cpu": [ 893 | "ia32" 894 | ], 895 | "dev": true, 896 | "license": "Apache-2.0 AND LGPL-3.0-or-later", 897 | "optional": true, 898 | "os": [ 899 | "win32" 900 | ], 901 | "engines": { 902 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 903 | }, 904 | "funding": { 905 | "url": "https://opencollective.com/libvips" 906 | } 907 | }, 908 | "node_modules/@img/sharp-win32-x64": { 909 | "version": "0.33.5", 910 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", 911 | "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", 912 | "cpu": [ 913 | "x64" 914 | ], 915 | "dev": true, 916 | "license": "Apache-2.0 AND LGPL-3.0-or-later", 917 | "optional": true, 918 | "os": [ 919 | "win32" 920 | ], 921 | "engines": { 922 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 923 | }, 924 | "funding": { 925 | "url": "https://opencollective.com/libvips" 926 | } 927 | }, 928 | "node_modules/@jridgewell/resolve-uri": { 929 | "version": "3.1.2", 930 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 931 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 932 | "dev": true, 933 | "license": "MIT", 934 | "engines": { 935 | "node": ">=6.0.0" 936 | } 937 | }, 938 | "node_modules/@jridgewell/sourcemap-codec": { 939 | "version": "1.5.0", 940 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 941 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 942 | "dev": true, 943 | "license": "MIT" 944 | }, 945 | "node_modules/@jridgewell/trace-mapping": { 946 | "version": "0.3.9", 947 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 948 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 949 | "dev": true, 950 | "license": "MIT", 951 | "dependencies": { 952 | "@jridgewell/resolve-uri": "^3.0.3", 953 | "@jridgewell/sourcemap-codec": "^1.4.10" 954 | } 955 | }, 956 | "node_modules/acorn": { 957 | "version": "8.14.0", 958 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 959 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 960 | "dev": true, 961 | "license": "MIT", 962 | "bin": { 963 | "acorn": "bin/acorn" 964 | }, 965 | "engines": { 966 | "node": ">=0.4.0" 967 | } 968 | }, 969 | "node_modules/acorn-walk": { 970 | "version": "8.3.2", 971 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", 972 | "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", 973 | "dev": true, 974 | "license": "MIT", 975 | "engines": { 976 | "node": ">=0.4.0" 977 | } 978 | }, 979 | "node_modules/as-table": { 980 | "version": "1.0.55", 981 | "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", 982 | "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", 983 | "dev": true, 984 | "license": "MIT", 985 | "dependencies": { 986 | "printable-characters": "^1.0.42" 987 | } 988 | }, 989 | "node_modules/blake3-wasm": { 990 | "version": "2.1.5", 991 | "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 992 | "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 993 | "dev": true 994 | }, 995 | "node_modules/boolbase": { 996 | "version": "1.0.0", 997 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 998 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" 999 | }, 1000 | "node_modules/color": { 1001 | "version": "4.2.3", 1002 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 1003 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 1004 | "dev": true, 1005 | "license": "MIT", 1006 | "optional": true, 1007 | "dependencies": { 1008 | "color-convert": "^2.0.1", 1009 | "color-string": "^1.9.0" 1010 | }, 1011 | "engines": { 1012 | "node": ">=12.5.0" 1013 | } 1014 | }, 1015 | "node_modules/color-convert": { 1016 | "version": "2.0.1", 1017 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1018 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1019 | "dev": true, 1020 | "license": "MIT", 1021 | "optional": true, 1022 | "dependencies": { 1023 | "color-name": "~1.1.4" 1024 | }, 1025 | "engines": { 1026 | "node": ">=7.0.0" 1027 | } 1028 | }, 1029 | "node_modules/color-name": { 1030 | "version": "1.1.4", 1031 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1032 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1033 | "dev": true, 1034 | "license": "MIT", 1035 | "optional": true 1036 | }, 1037 | "node_modules/color-string": { 1038 | "version": "1.9.1", 1039 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1040 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1041 | "dev": true, 1042 | "license": "MIT", 1043 | "optional": true, 1044 | "dependencies": { 1045 | "color-name": "^1.0.0", 1046 | "simple-swizzle": "^0.2.2" 1047 | } 1048 | }, 1049 | "node_modules/cookie": { 1050 | "version": "0.7.2", 1051 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 1052 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 1053 | "dev": true, 1054 | "license": "MIT", 1055 | "engines": { 1056 | "node": ">= 0.6" 1057 | } 1058 | }, 1059 | "node_modules/css-select": { 1060 | "version": "5.1.0", 1061 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 1062 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 1063 | "dependencies": { 1064 | "boolbase": "^1.0.0", 1065 | "css-what": "^6.1.0", 1066 | "domhandler": "^5.0.2", 1067 | "domutils": "^3.0.1", 1068 | "nth-check": "^2.0.1" 1069 | }, 1070 | "funding": { 1071 | "url": "https://github.com/sponsors/fb55" 1072 | } 1073 | }, 1074 | "node_modules/css-what": { 1075 | "version": "6.1.0", 1076 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 1077 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 1078 | "engines": { 1079 | "node": ">= 6" 1080 | }, 1081 | "funding": { 1082 | "url": "https://github.com/sponsors/fb55" 1083 | } 1084 | }, 1085 | "node_modules/data-uri-to-buffer": { 1086 | "version": "2.0.2", 1087 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", 1088 | "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", 1089 | "dev": true, 1090 | "license": "MIT" 1091 | }, 1092 | "node_modules/defu": { 1093 | "version": "6.1.4", 1094 | "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 1095 | "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 1096 | "dev": true, 1097 | "license": "MIT" 1098 | }, 1099 | "node_modules/detect-libc": { 1100 | "version": "2.0.3", 1101 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", 1102 | "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", 1103 | "dev": true, 1104 | "license": "Apache-2.0", 1105 | "optional": true, 1106 | "engines": { 1107 | "node": ">=8" 1108 | } 1109 | }, 1110 | "node_modules/dom-serializer": { 1111 | "version": "2.0.0", 1112 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 1113 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 1114 | "dependencies": { 1115 | "domelementtype": "^2.3.0", 1116 | "domhandler": "^5.0.2", 1117 | "entities": "^4.2.0" 1118 | }, 1119 | "funding": { 1120 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 1121 | } 1122 | }, 1123 | "node_modules/domelementtype": { 1124 | "version": "2.3.0", 1125 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 1126 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 1127 | "funding": [ 1128 | { 1129 | "type": "github", 1130 | "url": "https://github.com/sponsors/fb55" 1131 | } 1132 | ] 1133 | }, 1134 | "node_modules/domhandler": { 1135 | "version": "5.0.3", 1136 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 1137 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 1138 | "dependencies": { 1139 | "domelementtype": "^2.3.0" 1140 | }, 1141 | "engines": { 1142 | "node": ">= 4" 1143 | }, 1144 | "funding": { 1145 | "url": "https://github.com/fb55/domhandler?sponsor=1" 1146 | } 1147 | }, 1148 | "node_modules/domutils": { 1149 | "version": "3.1.0", 1150 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 1151 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 1152 | "dependencies": { 1153 | "dom-serializer": "^2.0.0", 1154 | "domelementtype": "^2.3.0", 1155 | "domhandler": "^5.0.3" 1156 | }, 1157 | "funding": { 1158 | "url": "https://github.com/fb55/domutils?sponsor=1" 1159 | } 1160 | }, 1161 | "node_modules/entities": { 1162 | "version": "4.5.0", 1163 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 1164 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 1165 | "engines": { 1166 | "node": ">=0.12" 1167 | }, 1168 | "funding": { 1169 | "url": "https://github.com/fb55/entities?sponsor=1" 1170 | } 1171 | }, 1172 | "node_modules/esbuild": { 1173 | "version": "0.17.19", 1174 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 1175 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 1176 | "dev": true, 1177 | "hasInstallScript": true, 1178 | "bin": { 1179 | "esbuild": "bin/esbuild" 1180 | }, 1181 | "engines": { 1182 | "node": ">=12" 1183 | }, 1184 | "optionalDependencies": { 1185 | "@esbuild/android-arm": "0.17.19", 1186 | "@esbuild/android-arm64": "0.17.19", 1187 | "@esbuild/android-x64": "0.17.19", 1188 | "@esbuild/darwin-arm64": "0.17.19", 1189 | "@esbuild/darwin-x64": "0.17.19", 1190 | "@esbuild/freebsd-arm64": "0.17.19", 1191 | "@esbuild/freebsd-x64": "0.17.19", 1192 | "@esbuild/linux-arm": "0.17.19", 1193 | "@esbuild/linux-arm64": "0.17.19", 1194 | "@esbuild/linux-ia32": "0.17.19", 1195 | "@esbuild/linux-loong64": "0.17.19", 1196 | "@esbuild/linux-mips64el": "0.17.19", 1197 | "@esbuild/linux-ppc64": "0.17.19", 1198 | "@esbuild/linux-riscv64": "0.17.19", 1199 | "@esbuild/linux-s390x": "0.17.19", 1200 | "@esbuild/linux-x64": "0.17.19", 1201 | "@esbuild/netbsd-x64": "0.17.19", 1202 | "@esbuild/openbsd-x64": "0.17.19", 1203 | "@esbuild/sunos-x64": "0.17.19", 1204 | "@esbuild/win32-arm64": "0.17.19", 1205 | "@esbuild/win32-ia32": "0.17.19", 1206 | "@esbuild/win32-x64": "0.17.19" 1207 | } 1208 | }, 1209 | "node_modules/escape-string-regexp": { 1210 | "version": "4.0.0", 1211 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1212 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1213 | "dev": true, 1214 | "engines": { 1215 | "node": ">=10" 1216 | }, 1217 | "funding": { 1218 | "url": "https://github.com/sponsors/sindresorhus" 1219 | } 1220 | }, 1221 | "node_modules/estree-walker": { 1222 | "version": "0.6.1", 1223 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 1224 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 1225 | "dev": true 1226 | }, 1227 | "node_modules/exit-hook": { 1228 | "version": "2.2.1", 1229 | "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 1230 | "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 1231 | "dev": true, 1232 | "license": "MIT", 1233 | "engines": { 1234 | "node": ">=6" 1235 | }, 1236 | "funding": { 1237 | "url": "https://github.com/sponsors/sindresorhus" 1238 | } 1239 | }, 1240 | "node_modules/exsolve": { 1241 | "version": "1.0.4", 1242 | "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.4.tgz", 1243 | "integrity": "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==", 1244 | "dev": true, 1245 | "license": "MIT" 1246 | }, 1247 | "node_modules/fsevents": { 1248 | "version": "2.3.3", 1249 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1250 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1251 | "dev": true, 1252 | "hasInstallScript": true, 1253 | "optional": true, 1254 | "os": [ 1255 | "darwin" 1256 | ], 1257 | "engines": { 1258 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1259 | } 1260 | }, 1261 | "node_modules/get-source": { 1262 | "version": "2.0.12", 1263 | "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", 1264 | "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", 1265 | "dev": true, 1266 | "license": "Unlicense", 1267 | "dependencies": { 1268 | "data-uri-to-buffer": "^2.0.0", 1269 | "source-map": "^0.6.1" 1270 | } 1271 | }, 1272 | "node_modules/glob-to-regexp": { 1273 | "version": "0.4.1", 1274 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 1275 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 1276 | "dev": true, 1277 | "license": "BSD-2-Clause" 1278 | }, 1279 | "node_modules/he": { 1280 | "version": "1.2.0", 1281 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 1282 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 1283 | "bin": { 1284 | "he": "bin/he" 1285 | } 1286 | }, 1287 | "node_modules/is-arrayish": { 1288 | "version": "0.3.2", 1289 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 1290 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 1291 | "dev": true, 1292 | "license": "MIT", 1293 | "optional": true 1294 | }, 1295 | "node_modules/magic-string": { 1296 | "version": "0.25.9", 1297 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", 1298 | "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", 1299 | "dev": true, 1300 | "dependencies": { 1301 | "sourcemap-codec": "^1.4.8" 1302 | } 1303 | }, 1304 | "node_modules/mime": { 1305 | "version": "3.0.0", 1306 | "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1307 | "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1308 | "dev": true, 1309 | "bin": { 1310 | "mime": "cli.js" 1311 | }, 1312 | "engines": { 1313 | "node": ">=10.0.0" 1314 | } 1315 | }, 1316 | "node_modules/miniflare": { 1317 | "version": "3.20250408.2", 1318 | "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250408.2.tgz", 1319 | "integrity": "sha512-uTs7cGWFErgJTKtBdmtctwhuoxniuCQqDT8+xaEiJdEC8d+HsaZVYfZwIX2NuSmdAiHMe7NtbdZYjFMbIXtJsQ==", 1320 | "dev": true, 1321 | "license": "MIT", 1322 | "dependencies": { 1323 | "@cspotcode/source-map-support": "0.8.1", 1324 | "acorn": "8.14.0", 1325 | "acorn-walk": "8.3.2", 1326 | "exit-hook": "2.2.1", 1327 | "glob-to-regexp": "0.4.1", 1328 | "stoppable": "1.1.0", 1329 | "undici": "^5.28.5", 1330 | "workerd": "1.20250408.0", 1331 | "ws": "8.18.0", 1332 | "youch": "3.3.4", 1333 | "zod": "3.22.3" 1334 | }, 1335 | "bin": { 1336 | "miniflare": "bootstrap.js" 1337 | }, 1338 | "engines": { 1339 | "node": ">=16.13" 1340 | } 1341 | }, 1342 | "node_modules/mustache": { 1343 | "version": "4.2.0", 1344 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 1345 | "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 1346 | "dev": true, 1347 | "license": "MIT", 1348 | "bin": { 1349 | "mustache": "bin/mustache" 1350 | } 1351 | }, 1352 | "node_modules/node-html-parser": { 1353 | "version": "6.1.13", 1354 | "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", 1355 | "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", 1356 | "dependencies": { 1357 | "css-select": "^5.1.0", 1358 | "he": "1.2.0" 1359 | } 1360 | }, 1361 | "node_modules/nth-check": { 1362 | "version": "2.1.1", 1363 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 1364 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 1365 | "dependencies": { 1366 | "boolbase": "^1.0.0" 1367 | }, 1368 | "funding": { 1369 | "url": "https://github.com/fb55/nth-check?sponsor=1" 1370 | } 1371 | }, 1372 | "node_modules/ohash": { 1373 | "version": "2.0.11", 1374 | "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", 1375 | "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", 1376 | "dev": true, 1377 | "license": "MIT" 1378 | }, 1379 | "node_modules/path-to-regexp": { 1380 | "version": "6.3.0", 1381 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 1382 | "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 1383 | "dev": true 1384 | }, 1385 | "node_modules/pathe": { 1386 | "version": "2.0.3", 1387 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 1388 | "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 1389 | "dev": true, 1390 | "license": "MIT" 1391 | }, 1392 | "node_modules/printable-characters": { 1393 | "version": "1.0.42", 1394 | "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", 1395 | "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", 1396 | "dev": true, 1397 | "license": "Unlicense" 1398 | }, 1399 | "node_modules/rollup-plugin-inject": { 1400 | "version": "3.0.2", 1401 | "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", 1402 | "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", 1403 | "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", 1404 | "dev": true, 1405 | "dependencies": { 1406 | "estree-walker": "^0.6.1", 1407 | "magic-string": "^0.25.3", 1408 | "rollup-pluginutils": "^2.8.1" 1409 | } 1410 | }, 1411 | "node_modules/rollup-plugin-node-polyfills": { 1412 | "version": "0.2.1", 1413 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", 1414 | "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", 1415 | "dev": true, 1416 | "dependencies": { 1417 | "rollup-plugin-inject": "^3.0.0" 1418 | } 1419 | }, 1420 | "node_modules/rollup-pluginutils": { 1421 | "version": "2.8.2", 1422 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 1423 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 1424 | "dev": true, 1425 | "dependencies": { 1426 | "estree-walker": "^0.6.1" 1427 | } 1428 | }, 1429 | "node_modules/semver": { 1430 | "version": "7.7.1", 1431 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 1432 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 1433 | "dev": true, 1434 | "license": "ISC", 1435 | "optional": true, 1436 | "bin": { 1437 | "semver": "bin/semver.js" 1438 | }, 1439 | "engines": { 1440 | "node": ">=10" 1441 | } 1442 | }, 1443 | "node_modules/sharp": { 1444 | "version": "0.33.5", 1445 | "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", 1446 | "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", 1447 | "dev": true, 1448 | "hasInstallScript": true, 1449 | "license": "Apache-2.0", 1450 | "optional": true, 1451 | "dependencies": { 1452 | "color": "^4.2.3", 1453 | "detect-libc": "^2.0.3", 1454 | "semver": "^7.6.3" 1455 | }, 1456 | "engines": { 1457 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1458 | }, 1459 | "funding": { 1460 | "url": "https://opencollective.com/libvips" 1461 | }, 1462 | "optionalDependencies": { 1463 | "@img/sharp-darwin-arm64": "0.33.5", 1464 | "@img/sharp-darwin-x64": "0.33.5", 1465 | "@img/sharp-libvips-darwin-arm64": "1.0.4", 1466 | "@img/sharp-libvips-darwin-x64": "1.0.4", 1467 | "@img/sharp-libvips-linux-arm": "1.0.5", 1468 | "@img/sharp-libvips-linux-arm64": "1.0.4", 1469 | "@img/sharp-libvips-linux-s390x": "1.0.4", 1470 | "@img/sharp-libvips-linux-x64": "1.0.4", 1471 | "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", 1472 | "@img/sharp-libvips-linuxmusl-x64": "1.0.4", 1473 | "@img/sharp-linux-arm": "0.33.5", 1474 | "@img/sharp-linux-arm64": "0.33.5", 1475 | "@img/sharp-linux-s390x": "0.33.5", 1476 | "@img/sharp-linux-x64": "0.33.5", 1477 | "@img/sharp-linuxmusl-arm64": "0.33.5", 1478 | "@img/sharp-linuxmusl-x64": "0.33.5", 1479 | "@img/sharp-wasm32": "0.33.5", 1480 | "@img/sharp-win32-ia32": "0.33.5", 1481 | "@img/sharp-win32-x64": "0.33.5" 1482 | } 1483 | }, 1484 | "node_modules/simple-swizzle": { 1485 | "version": "0.2.2", 1486 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 1487 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 1488 | "dev": true, 1489 | "license": "MIT", 1490 | "optional": true, 1491 | "dependencies": { 1492 | "is-arrayish": "^0.3.1" 1493 | } 1494 | }, 1495 | "node_modules/source-map": { 1496 | "version": "0.6.1", 1497 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1498 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1499 | "dev": true, 1500 | "license": "BSD-3-Clause", 1501 | "engines": { 1502 | "node": ">=0.10.0" 1503 | } 1504 | }, 1505 | "node_modules/sourcemap-codec": { 1506 | "version": "1.4.8", 1507 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 1508 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 1509 | "deprecated": "Please use @jridgewell/sourcemap-codec instead", 1510 | "dev": true 1511 | }, 1512 | "node_modules/stacktracey": { 1513 | "version": "2.1.8", 1514 | "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", 1515 | "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", 1516 | "dev": true, 1517 | "license": "Unlicense", 1518 | "dependencies": { 1519 | "as-table": "^1.0.36", 1520 | "get-source": "^2.0.12" 1521 | } 1522 | }, 1523 | "node_modules/stoppable": { 1524 | "version": "1.1.0", 1525 | "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 1526 | "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 1527 | "dev": true, 1528 | "license": "MIT", 1529 | "engines": { 1530 | "node": ">=4", 1531 | "npm": ">=6" 1532 | } 1533 | }, 1534 | "node_modules/tslib": { 1535 | "version": "2.8.1", 1536 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 1537 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 1538 | "dev": true, 1539 | "license": "0BSD", 1540 | "optional": true 1541 | }, 1542 | "node_modules/typescript": { 1543 | "version": "5.8.3", 1544 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1545 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1546 | "dev": true, 1547 | "license": "Apache-2.0", 1548 | "bin": { 1549 | "tsc": "bin/tsc", 1550 | "tsserver": "bin/tsserver" 1551 | }, 1552 | "engines": { 1553 | "node": ">=14.17" 1554 | } 1555 | }, 1556 | "node_modules/ufo": { 1557 | "version": "1.5.4", 1558 | "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", 1559 | "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", 1560 | "dev": true, 1561 | "license": "MIT" 1562 | }, 1563 | "node_modules/undici": { 1564 | "version": "5.29.0", 1565 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 1566 | "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 1567 | "dev": true, 1568 | "license": "MIT", 1569 | "dependencies": { 1570 | "@fastify/busboy": "^2.0.0" 1571 | }, 1572 | "engines": { 1573 | "node": ">=14.0" 1574 | } 1575 | }, 1576 | "node_modules/unenv": { 1577 | "version": "2.0.0-rc.14", 1578 | "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", 1579 | "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", 1580 | "dev": true, 1581 | "license": "MIT", 1582 | "dependencies": { 1583 | "defu": "^6.1.4", 1584 | "exsolve": "^1.0.1", 1585 | "ohash": "^2.0.10", 1586 | "pathe": "^2.0.3", 1587 | "ufo": "^1.5.4" 1588 | } 1589 | }, 1590 | "node_modules/workerd": { 1591 | "version": "1.20250408.0", 1592 | "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250408.0.tgz", 1593 | "integrity": "sha512-bBUX+UsvpzAqiWFNeZrlZmDGddiGZdBBbftZJz2wE6iUg/cIAJeVQYTtS/3ahaicguoLBz4nJiDo8luqM9fx1A==", 1594 | "dev": true, 1595 | "hasInstallScript": true, 1596 | "license": "Apache-2.0", 1597 | "bin": { 1598 | "workerd": "bin/workerd" 1599 | }, 1600 | "engines": { 1601 | "node": ">=16" 1602 | }, 1603 | "optionalDependencies": { 1604 | "@cloudflare/workerd-darwin-64": "1.20250408.0", 1605 | "@cloudflare/workerd-darwin-arm64": "1.20250408.0", 1606 | "@cloudflare/workerd-linux-64": "1.20250408.0", 1607 | "@cloudflare/workerd-linux-arm64": "1.20250408.0", 1608 | "@cloudflare/workerd-windows-64": "1.20250408.0" 1609 | } 1610 | }, 1611 | "node_modules/wrangler": { 1612 | "version": "3.114.9", 1613 | "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.9.tgz", 1614 | "integrity": "sha512-1e0gL+rxLF04kM9bW4sxoDGLXpJ1x53Rx1t18JuUm6F67qadKKPISyUAXuBeIQudWrCWEBXaTVnSdLHz0yBXbA==", 1615 | "dev": true, 1616 | "license": "MIT OR Apache-2.0", 1617 | "dependencies": { 1618 | "@cloudflare/kv-asset-handler": "0.3.4", 1619 | "@cloudflare/unenv-preset": "2.0.2", 1620 | "@esbuild-plugins/node-globals-polyfill": "0.2.3", 1621 | "@esbuild-plugins/node-modules-polyfill": "0.2.2", 1622 | "blake3-wasm": "2.1.5", 1623 | "esbuild": "0.17.19", 1624 | "miniflare": "3.20250408.2", 1625 | "path-to-regexp": "6.3.0", 1626 | "unenv": "2.0.0-rc.14", 1627 | "workerd": "1.20250408.0" 1628 | }, 1629 | "bin": { 1630 | "wrangler": "bin/wrangler.js", 1631 | "wrangler2": "bin/wrangler.js" 1632 | }, 1633 | "engines": { 1634 | "node": ">=16.17.0" 1635 | }, 1636 | "optionalDependencies": { 1637 | "fsevents": "~2.3.2", 1638 | "sharp": "^0.33.5" 1639 | }, 1640 | "peerDependencies": { 1641 | "@cloudflare/workers-types": "^4.20250408.0" 1642 | }, 1643 | "peerDependenciesMeta": { 1644 | "@cloudflare/workers-types": { 1645 | "optional": true 1646 | } 1647 | } 1648 | }, 1649 | "node_modules/ws": { 1650 | "version": "8.18.0", 1651 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 1652 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 1653 | "dev": true, 1654 | "license": "MIT", 1655 | "engines": { 1656 | "node": ">=10.0.0" 1657 | }, 1658 | "peerDependencies": { 1659 | "bufferutil": "^4.0.1", 1660 | "utf-8-validate": ">=5.0.2" 1661 | }, 1662 | "peerDependenciesMeta": { 1663 | "bufferutil": { 1664 | "optional": true 1665 | }, 1666 | "utf-8-validate": { 1667 | "optional": true 1668 | } 1669 | } 1670 | }, 1671 | "node_modules/youch": { 1672 | "version": "3.3.4", 1673 | "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", 1674 | "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", 1675 | "dev": true, 1676 | "license": "MIT", 1677 | "dependencies": { 1678 | "cookie": "^0.7.1", 1679 | "mustache": "^4.2.0", 1680 | "stacktracey": "^2.1.8" 1681 | } 1682 | }, 1683 | "node_modules/zod": { 1684 | "version": "3.22.3", 1685 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", 1686 | "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", 1687 | "dev": true, 1688 | "license": "MIT", 1689 | "funding": { 1690 | "url": "https://github.com/sponsors/colinhacks" 1691 | } 1692 | } 1693 | } 1694 | } 1695 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link-preview", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "wrangler dev", 7 | "deploy": "wrangler publish" 8 | }, 9 | "dependencies": { 10 | "node-html-parser": "^6.1" 11 | }, 12 | "devDependencies": { 13 | "@cloudflare/workers-types": "^4.20221111", 14 | "typescript": "^5.4", 15 | "wrangler": "^3.52" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cloudflare Workers implementation of link-preview API service. 3 | * 4 | * Workers doc: https://developers.cloudflare.com/workers/ 5 | */ 6 | 7 | import { version } from "../package.json"; 8 | import { HTMLElement, parse } from "node-html-parser"; 9 | 10 | export interface Env {} 11 | 12 | function dbg(...message: any[]) { 13 | console.info("[✌ link-preview] ", ...message); 14 | } 15 | 16 | export default { 17 | async fetch( 18 | request: Request, 19 | env: Env, 20 | ctx: ExecutionContext 21 | ): Promise { 22 | const reqUrl = new URL(request.url); 23 | const origin = request.headers.get("Origin") || "*"; 24 | 25 | if (request.method === "OPTIONS") { 26 | // Handle (accept) preflight requests from web browsers 27 | return handleOptions(request, origin); 28 | } else if (request.method === "GET") { 29 | const query = reqUrl.searchParams.get("q"); 30 | if (query) { 31 | // Primary feature: Return website's metadata for preview 32 | try { 33 | const md = await extractMetadata(query); 34 | if (md.error) { 35 | return handleError(md.status || 500, md.error, origin); 36 | } else { 37 | return new Response(JSON.stringify(md), { 38 | headers: withMonthLongCache( 39 | withCorsHeaders(origin, { 40 | "content-type": "application/json", 41 | }) 42 | ), 43 | }); 44 | } 45 | } catch (e) { 46 | return handleError(422, `[Failed to preview] ${e}`, origin); 47 | } 48 | } else { 49 | const twitterUserName = reqUrl.searchParams.get("tw-profile-icon"); 50 | if (twitterUserName) { 51 | // Hidden feature: Directly return Twitter profile image (and cache) 52 | return handleGetTwitterProfileImage(twitterUserName, origin); 53 | } else { 54 | return handleError(400, "Bad Request", origin); 55 | } 56 | } 57 | } else { 58 | return handleError(400, "Bad Request", origin); 59 | } 60 | }, 61 | }; 62 | 63 | // From: https://stackoverflow.com/a/69685872 64 | function handleOptions(request: Request, origin: string) { 65 | let headers = request.headers; 66 | if ( 67 | headers.get("Origin") !== null && 68 | headers.get("Access-Control-Request-Method") !== null && 69 | headers.get("Access-Control-Request-Headers") !== null 70 | ) { 71 | return new Response(null, { 72 | headers: withCorsHeaders(origin, { 73 | "Access-Control-Max-Age": "86400", 74 | "Access-Control-Allow-Headers": 75 | request.headers.get("Access-Control-Request-Headers") || "", 76 | }), 77 | }); 78 | } else { 79 | return new Response(null, { 80 | headers: { Allow: "GET" }, 81 | }); 82 | } 83 | } 84 | 85 | const subrequestCacheBehavior = { 86 | cacheTtlByStatus: { "200-499": 60 * 60 * 24 * 7, "500-599": 0 }, 87 | cacheEverything: true, 88 | }; 89 | 90 | async function handleGetTwitterProfileImage( 91 | twitterUserName: string, 92 | origin: string 93 | ): Promise { 94 | const url = `https://twitter.com/${twitterUserName}`; 95 | const md = await extractMetadata(url); 96 | // Profile page has profile image URL as metadata 97 | if (md.image) { 98 | const res = await fetch(md.image, { 99 | cf: { ...subrequestCacheBehavior, cacheKey: twitterUserName }, 100 | }); 101 | logSubrequest(res); 102 | const contentType = res.headers.get("content-type") || "text/plain"; 103 | if (contentType.startsWith("image/")) { 104 | // Hide origin info, creating new Response object. 105 | return new Response(res.body, { 106 | status: res.status, 107 | headers: withMonthLongCache( 108 | withCorsHeaders(origin, { "content-type": contentType }) 109 | ), 110 | }); 111 | } else { 112 | return handleError( 113 | 422, 114 | `${url} does not have user profile image!`, 115 | origin 116 | ); 117 | } 118 | } else { 119 | return handleError(404, "Not Found", origin); 120 | } 121 | } 122 | 123 | type Metadata = { 124 | title?: string; 125 | description?: string; 126 | url?: string; 127 | image?: string; 128 | charset?: string; 129 | status?: number; 130 | error?: string; 131 | }; 132 | 133 | function logSubrequest(res: Response) { 134 | dbg(`Subrequest: ${res.url} ${res.status}`); 135 | let loggedHeaders = ""; 136 | for (const [k, v] of res.headers.entries()) { 137 | loggedHeaders += `\t${k}: ${v}\n`; 138 | } 139 | dbg(`Subrequest headers:\n${loggedHeaders}`); 140 | } 141 | 142 | async function extractMetadata(query: string): Promise { 143 | const headers = { 144 | accept: "text/html,application/xhtml+xml", 145 | // When declared as Bot, some sites generously return prerendered metadata for preview (e.g. Twitter) 146 | "user-agent": `LinkPreviewBot/${version}`, 147 | "accept-language": "ja-JP", 148 | }; 149 | const res = await fetch(query, { 150 | redirect: "follow", 151 | headers: headers, 152 | cf: { ...subrequestCacheBehavior, cacheKey: query }, 153 | }); 154 | logSubrequest(res); 155 | if (res.status >= 400) { 156 | return { 157 | error: `[Error] ${query} returned status code: ${res.status}!`, 158 | status: res.status, 159 | }; 160 | } 161 | 162 | const rawBody = await res.arrayBuffer(); 163 | const utf8Body = new TextDecoder("utf-8").decode(rawBody); 164 | const parsedUtf8Body = parse(utf8Body); 165 | const detectedCharset = detectCharset(res.headers, parsedUtf8Body); 166 | dbg("Detected charset", detectedCharset); 167 | const parsed = 168 | detectedCharset === "utf-8" 169 | ? parsedUtf8Body 170 | : parse(new TextDecoder(detectedCharset).decode(rawBody)); 171 | const $ = (q: string) => parsed.querySelector(q); 172 | 173 | const title = 174 | $('meta[property="og:title"]')?.getAttribute("content") || 175 | $('meta[property="twitter:title"]')?.getAttribute("content") || 176 | $("title")?.textContent; 177 | 178 | const description = 179 | $('meta[property="og:description"]')?.getAttribute("content") || 180 | $('meta[property="twitter:description"]')?.getAttribute("content") || 181 | $('meta[name="description"]')?.getAttribute("content"); 182 | 183 | let url = query; 184 | const canonicalUrl = 185 | $('link[rel="canonical"]')?.getAttribute("href") || 186 | $('meta[property="og:url"]')?.getAttribute("content"); 187 | if (canonicalUrl) { 188 | if ( 189 | canonicalUrl?.startsWith("https://") || 190 | canonicalUrl?.startsWith("http://") 191 | ) { 192 | url = canonicalUrl; 193 | } else { 194 | // Resolve relative URL 195 | url = new URL(canonicalUrl, query).href; 196 | } 197 | } 198 | 199 | let image; 200 | const ogpImage = 201 | $('meta[property="og:image"]')?.getAttribute("content") || 202 | $('meta[property="twitter:image"]')?.getAttribute("content"); 203 | if (ogpImage) { 204 | if (ogpImage?.startsWith("https://") || ogpImage?.startsWith("http://")) { 205 | image = ogpImage; 206 | } else { 207 | // Resolve relative URL 208 | image = new URL(ogpImage, query).href; 209 | } 210 | } 211 | 212 | return { title, description, url, image, charset: detectedCharset }; 213 | } 214 | 215 | function detectCharset( 216 | headers: Headers, 217 | parsed: HTMLElement 218 | ): "utf-8" | "shift_jis" | string { 219 | const headerContentType = headers.get("content-type"); 220 | const headerCharset = headerContentType?.includes("charset=") 221 | ? headerContentType 222 | .split("charset=")[1] 223 | .toLowerCase() 224 | .replace(/^["']/, "") 225 | .replace(/["']$/, "") 226 | : undefined; 227 | 228 | let bodyCharset = parsed 229 | .querySelector("meta[charset]") 230 | ?.getAttribute("charset") 231 | ?.toLowerCase() 232 | .replace(/^["']/, "") 233 | .replace(/["']$/, ""); 234 | if (!bodyCharset) { 235 | const bodyContentType = parsed 236 | .querySelector('meta[http-equiv="Content-Type" i]') 237 | ?.getAttribute("content"); 238 | if (bodyContentType?.includes("charset=")) { 239 | bodyCharset = bodyContentType 240 | .split("charset=")[1] 241 | .toLowerCase() 242 | .replace(/^["']/, "") 243 | .replace(/["']$/, ""); 244 | } 245 | } 246 | 247 | // TODO: headerCharsetとbodyCharsetが食い違った場合、headerCharsetを優先しているが、 248 | // bodyCharsetを優先したほうが打率が高そうであれば変更するかも 249 | dbg("headerCharset", headerCharset); 250 | dbg("bodyCharset", bodyCharset); 251 | return headerCharset || bodyCharset || "utf-8"; 252 | } 253 | 254 | function help(host: string): string { 255 | return ` 256 | Hi! This is a link-preview service on Cloudflare Workers. 257 | 258 | If you are seeing this message, at least you connected to our endpoint successfully. 259 | 260 | Correct usage is: 261 | 262 | GET ${host}?q=https://cloudflare.com 263 | 264 | This should return JSON payload like this: 265 | 266 | { 267 | "title": "Cloudflare - The Web Performance & Security Company", 268 | "description": "Here at Cloudflare, we make the Internet work the way it should. Offering CDN, DNS, DDoS protection and security, find out how we can help your site.", 269 | "url": "https://www.cloudflare.com/", 270 | "image": "https://www.cloudflare.com/static/b30a57477bde900ba55c0b5f98c4e524/Cloudflare_default_OG_.png" 271 | } 272 | 273 | In short, supply whatever public URL as a query parameter "q", then send GET request. That's all! 274 | If the URL contains non-ASCII characters, url-encode them. 275 | 276 | Source code: https://github.com/ymtszw/link-preview 277 | 278 | This service is EXTREMELY easy to self-host; i.e. deploy on your own Cloudflare account. 279 | If you are going to throw many link-preview requests here, do consider it! 280 | `; 281 | } 282 | 283 | function withCorsHeaders( 284 | origin: string, 285 | otherHeaders: HeadersInit 286 | ): HeadersInit { 287 | return { 288 | ...otherHeaders, 289 | Vary: "Origin", 290 | "Access-Control-Allow-Origin": origin, 291 | "Access-Control-Allow-Methods": "GET", 292 | }; 293 | } 294 | 295 | function withMonthLongCache(otherHeaders: HeadersInit): HeadersInit { 296 | return { 297 | ...otherHeaders, 298 | "Cache-Control": 299 | "public, stale-if-error=60, stale-while-revalidate=60, max-age=" + 300 | 30 * 24 * 60 * 60, 301 | "CDN-Cache-Control": 302 | "public, stale-if-error=60, stale-while-revalidate=60, max-age=" + 303 | 7 * 24 * 60 * 60, 304 | }; 305 | } 306 | 307 | function handleError( 308 | status: number, 309 | message: string, 310 | origin: string 311 | ): Response { 312 | return new Response( 313 | JSON.stringify({ 314 | status: status, 315 | error: message, 316 | message: help(origin), 317 | }), 318 | { 319 | status: status, 320 | headers: withCorsHeaders(origin, { 321 | "content-type": "application/json", 322 | "Cache-Control": "public, max-age=" + 60 * 60, 323 | "CDN-Cache-Control": "public, max-age=" + 10 * 60, 324 | }), 325 | } 326 | ); 327 | } 328 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | "lib": [ 14 | "es2021" 15 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 16 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 17 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 18 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 19 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 20 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 21 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 22 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 23 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 24 | /* Modules */ 25 | "module": "es2022" /* Specify what module code is generated. */, 26 | // "rootDir": "./", /* Specify the root folder within your source files. */ 27 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 32 | "types": [ 33 | "@cloudflare/workers-types", 34 | ] /* Specify type package names to be included without being referenced in a source file. */, 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | "resolveJsonModule": true /* Enable importing .json files */, 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | /* JavaScript Support */ 39 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 40 | "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, 41 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 42 | /* Emit */ 43 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 44 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 45 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 46 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 47 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 48 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 49 | // "removeComments": true, /* Disable emitting comments. */ 50 | "noEmit": true /* Disable emitting files from a compilation. */, 51 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 52 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 53 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 54 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 58 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 59 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 60 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 61 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 62 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 63 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 64 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 65 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 66 | /* Interop Constraints */ 67 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 68 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 69 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 70 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 71 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 72 | /* Type Checking */ 73 | "strict": true /* Enable all strict type-checking options. */, 74 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 75 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 76 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 77 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 78 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 79 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 80 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 81 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 82 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 83 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 84 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 85 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 86 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 87 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 88 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 89 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 90 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 91 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 92 | /* Completeness */ 93 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 94 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "link-preview" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-04-05" 4 | --------------------------------------------------------------------------------