From 7a793dc417bcdcdb4826f477afe3828c5c210f54 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Apr 2026 20:21:31 +0000 Subject: [PATCH 1/6] Add first-party proxy route with CORS and target allowlist Co-authored-by: Siraj Chokshi --- AGENTS.md | 2 +- README.md | 18 + package-lock.json | 1005 ++++++++++++++++++++++++++++--- package.json | 2 +- src/routes/api/proxy/+server.ts | 101 ++++ src/utils/fetch.ts | 12 +- src/utils/proxy.ts | 165 +++++ svelte.config.js | 2 +- tests/proxy.test.ts | 136 +++++ 9 files changed, 1339 insertions(+), 104 deletions(-) create mode 100644 src/routes/api/proxy/+server.ts create mode 100644 src/utils/proxy.ts create mode 100644 tests/proxy.test.ts diff --git a/AGENTS.md b/AGENTS.md index e05f59d..b364de2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,6 @@ - The `prepare` script (`svelte-kit sync`) runs automatically during `npm install`. If you see import errors for `$app/*` modules, re-run `npm install`. - The dev server uses Vite's default port **5173**. Pass `--host 0.0.0.0` to expose it outside localhost: `npm run dev -- --host 0.0.0.0`. - Requires **Node.js >= 24** (`engines` field in `package.json`). Use `nvm use 24` if multiple versions are installed. -- Preview functionality depends on the external CORS proxy `api.codetabs.com`. If previews fail to load, this third-party service may be down. +- Preview functionality uses the internal `/api/proxy` endpoint. Configure `PROXY_ALLOWED_ORIGINS` with deployment origins (comma separated) to restrict CORS. - `svelte-preprocess` deprecation warnings about "defaults" are expected and harmless. - The `package-lock.json` uses npm; do not mix with other package managers. diff --git a/README.md b/README.md index 8f4751f..b82e658 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,24 @@ npm i npm run dev -- --open ``` +## Proxy Configuration + +Preview fetching now uses the first-party SvelteKit endpoint at `/api/proxy` (instead of `api.codetabs.com`), which requires a server adapter such as Vercel. + +To lock down CORS for your deployment domain(s), configure: + +```sh +PROXY_ALLOWED_ORIGINS=https://static-preview.vercel.app,http://localhost:5173 +``` + +Security constraints in `/api/proxy`: + +- only proxies `https://raw.githubusercontent.com/...` URLs +- only proxies GitLab `https://gitlab.com/.../-/raw/...` URLs +- rejects non-HTTPS targets, query strings, and path traversal +- allows requests only from configured origins (checks `Origin` then `Referer`) +- enforces a max upstream payload size of 5 MiB + ## Testing We use [Jest](https://jestjs.io/) for unit testing. Only URL parsing is tested to prevent regressions. diff --git a/package-lock.json b/package-lock.json index 7a5d0b5..5c8007b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "devDependencies": { "@babel/preset-env": "^7.29.2", "@eslint/js": "^8.57.1", - "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/adapter-vercel": "^6.3.3", "@sveltejs/kit": "^2.56.1", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/jest-dom": "^6.9.1", @@ -2260,6 +2260,19 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2740,6 +2753,41 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3379,51 +3427,584 @@ "openharmony" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.0.tgz", + "integrity": "sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-vercel": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-vercel/-/adapter-vercel-6.3.3.tgz", + "integrity": "sha512-jI7jT/XqRyFe9oqKvFcNPQfyNBi3pXqN1iQXa2lmeKT5Vzgr9iSOqJOD3pXf/9Q2Os6SXzqYYm6osRjHYEhkyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vercel/nft": "^1.3.2", + "esbuild": "^0.25.4" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ - "wasm32" + "x64" ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, + "os": [ + "sunos" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", @@ -3432,13 +4013,13 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "node_modules/@sveltejs/adapter-vercel/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -3449,68 +4030,49 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.0.tgz", - "integrity": "sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", - "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" + "node": ">=18" } }, - "node_modules/@sveltejs/adapter-auto": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz", - "integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==", + "node_modules/@sveltejs/adapter-vercel/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/@sveltejs/kit": { @@ -4460,6 +5022,88 @@ "win32" ] }, + "node_modules/@vercel/nft": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.5.0.tgz", + "integrity": "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^13.0.0", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@vercel/nft/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vercel/nft/node_modules/lru-cache": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.1.tgz", + "integrity": "sha512-Y71HWT4hydF1IAG/2OPync4dgQ/J2iWye7eg6CuzJHI+E97tvqFPlADzxiNnjH6WSljg8ecfXMr9k6bfFuqA5w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@vercel/nft/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -4473,6 +5117,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4632,6 +5286,13 @@ "dequal": "^2.0.3" } }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true, + "license": "MIT" + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -4817,6 +5478,16 @@ "node": ">=6.0.0" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -4978,6 +5649,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", @@ -5111,6 +5792,16 @@ "dev": true, "license": "MIT" }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5791,6 +6482,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5932,6 +6630,13 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -8166,6 +8871,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -8250,6 +8968,64 @@ "license": "MIT", "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8264,6 +9040,22 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9734,6 +10526,33 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index ffa0103..6644cef 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@babel/preset-env": "^7.29.2", "@eslint/js": "^8.57.1", - "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/adapter-vercel": "^6.3.3", "@sveltejs/kit": "^2.56.1", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/jest-dom": "^6.9.1", diff --git a/src/routes/api/proxy/+server.ts b/src/routes/api/proxy/+server.ts new file mode 100644 index 0000000..6047d85 --- /dev/null +++ b/src/routes/api/proxy/+server.ts @@ -0,0 +1,101 @@ +import { env } from '$env/dynamic/private' +import { error } from '@sveltejs/kit' +import { + buildCorsHeaders, + getAllowedProxyOrigins, + getRequestOrigin, + isAllowedOrigin, + MAX_PROXY_RESPONSE_BYTES, + parseProxyTarget, + ProxyRequestError, +} from '../../../utils/proxy' + +export const prerender = false + +function enforceProxyPolicy(request: Request, requestUrl: URL) { + const allowedOrigins = getAllowedProxyOrigins( + env.PROXY_ALLOWED_ORIGINS, + requestUrl.origin, + ) + const requestOrigin = getRequestOrigin(request.headers) + + if (!isAllowedOrigin(requestOrigin, allowedOrigins)) { + throw error(403, 'Origin not allowed') + } + + return buildCorsHeaders(request.headers.get('origin'), allowedOrigins) +} + +export function OPTIONS({ request, url }) { + const corsHeaders = enforceProxyPolicy(request, url) + + return new Response(null, { + status: 204, + headers: corsHeaders, + }) +} + +export async function GET({ request, url, fetch }) { + const corsHeaders = enforceProxyPolicy(request, url) + + let target: URL + + try { + target = parseProxyTarget(url.searchParams.get('url')) + } catch (err) { + if (err instanceof ProxyRequestError) { + throw error(err.status, err.message) + } + + throw error(400, 'Invalid url param') + } + + let upstream: Response + + try { + upstream = await fetch(target.toString(), { + redirect: 'follow', + headers: { + Accept: request.headers.get('accept') ?? '*/*', + }, + }) + } catch { + throw error(502, `Could not load ${target.toString()}`) + } + + const contentLength = Number(upstream.headers.get('content-length')) + if ( + Number.isFinite(contentLength) && + contentLength > MAX_PROXY_RESPONSE_BYTES + ) { + throw error(413, 'Upstream payload too large') + } + + const payload = await upstream.arrayBuffer() + if (payload.byteLength > MAX_PROXY_RESPONSE_BYTES) { + throw error(413, 'Upstream payload too large') + } + + const headers = new Headers(corsHeaders) + headers.set( + 'Content-Type', + upstream.headers.get('content-type') ?? 'text/plain; charset=utf-8', + ) + headers.set('X-Content-Type-Options', 'nosniff') + headers.set('Cache-Control', upstream.headers.get('cache-control') ?? 'public, max-age=300') + + const etag = upstream.headers.get('etag') + if (etag) { + headers.set('ETag', etag) + } + + const lastModified = upstream.headers.get('last-modified') + if (lastModified) { + headers.set('Last-Modified', lastModified) + } + + return new Response(payload, { + status: upstream.status, + headers, + }) +} diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index ec470b6..5d76787 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -1,19 +1,15 @@ import { PreviewError, errorType } from './errors' export async function proxyFetch(url: string) { - const res = await fetch( - `https://api.codetabs.com/v1/proxy/?quest=${url}`, - undefined, - ) - - if (!res.ok) - throw new PreviewError(`Could not load ${url}`, errorType.NETWORK_ERROR) - + const res = await fetch(`/api/proxy?url=${encodeURIComponent(url)}`) const raw = await res.text() if (res.status === 404 || raw.includes('Not Found')) // Manually check for this title since Gitlab returns a 200 for non-existent files throw new PreviewError(`Could not load ${url}`, errorType.NOT_FOUND) + if (!res.ok) + throw new PreviewError(`Could not load ${url}`, errorType.NETWORK_ERROR) + return raw } diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts new file mode 100644 index 0000000..3acf78e --- /dev/null +++ b/src/utils/proxy.ts @@ -0,0 +1,165 @@ +const RAW_GITHUB_HOST = 'raw.githubusercontent.com' +const GITLAB_HOST = 'gitlab.com' + +const DEFAULT_ALLOWED_ORIGINS = [ + 'https://static-preview.vercel.app', + 'http://localhost:5173', +] + +export const MAX_PROXY_RESPONSE_BYTES = 5 * 1024 * 1024 // 5 MiB + +export class ProxyRequestError extends Error { + constructor( + message: string, + public status: number, + ) { + super(message) + } +} + +function normalizeOrigin(origin: string | null | undefined): string | undefined { + if (!origin) return undefined + + try { + return new URL(origin).origin + } catch { + return undefined + } +} + +function hasUnsafePathSegment(pathSegments: string[]): boolean { + return pathSegments.some((segment) => { + let decodedSegment: string + + try { + decodedSegment = decodeURIComponent(segment) + } catch { + return true + } + + return decodedSegment === '' || decodedSegment === '.' || decodedSegment === '..' + }) +} + +function splitPath(pathname: string): string[] { + return pathname.split('/').filter(Boolean) +} + +function isAllowedGithubRawPath(pathname: string): boolean { + const pathSegments = splitPath(pathname) + + // /:owner/:repo/:branch/:file... + if (pathSegments.length < 4) return false + if (hasUnsafePathSegment(pathSegments)) return false + + return true +} + +function isAllowedGitlabRawPath(pathname: string): boolean { + const pathSegments = splitPath(pathname) + + if (hasUnsafePathSegment(pathSegments)) return false + + const rawPathSegment = pathSegments.findIndex( + (segment, idx) => segment === '-' && pathSegments[idx + 1] === 'raw', + ) + + // /:owner/(optional subgroups...)/:repo/-/raw/:branch/:file... + if (rawPathSegment < 2) return false + if (pathSegments.length <= rawPathSegment + 2) return false + + return true +} + +export function isAllowedProxyTarget(target: URL): boolean { + const normalizedHostname = target.hostname.toLowerCase() + + if (target.protocol !== 'https:') return false + if (target.username || target.password) return false + if (target.port) return false + if (target.search.length > 0) return false + + if (normalizedHostname === RAW_GITHUB_HOST) { + return isAllowedGithubRawPath(target.pathname) + } + + if (normalizedHostname === GITLAB_HOST) { + return isAllowedGitlabRawPath(target.pathname) + } + + return false +} + +export function parseProxyTarget(rawTarget: string | null): URL { + if (!rawTarget) { + throw new ProxyRequestError('Missing url param', 400) + } + + let target: URL + + try { + target = new URL(rawTarget) + } catch { + throw new ProxyRequestError('Invalid url param', 400) + } + + target.hash = '' + + if (!isAllowedProxyTarget(target)) { + throw new ProxyRequestError('Target url is not allowed', 403) + } + + return target +} + +export function getAllowedProxyOrigins( + configuredOrigins: string | undefined, + requestOrigin: string, +): string[] { + const allowedOrigins = configuredOrigins + ?.split(',') + .map((origin) => origin.trim()) + .filter(Boolean) + .map((origin) => normalizeOrigin(origin)) + .filter((origin): origin is string => Boolean(origin)) + + if (allowedOrigins && allowedOrigins.length > 0) { + return [...new Set(allowedOrigins)] + } + + return [...new Set([...DEFAULT_ALLOWED_ORIGINS, requestOrigin])] +} + +export function getRequestOrigin(headers: Headers): string | undefined { + const originHeader = normalizeOrigin(headers.get('origin')) + if (originHeader) return originHeader + + return normalizeOrigin(headers.get('referer')) +} + +export function isAllowedOrigin( + requestOrigin: string | undefined, + allowedOrigins: string[], +): boolean { + if (!requestOrigin) return false + return allowedOrigins.includes(requestOrigin) +} + +export function buildCorsHeaders( + originHeader: string | null, + allowedOrigins: string[], +): Headers { + const headers = new Headers({ + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + Vary: 'Origin', + }) + + const requestOrigin = normalizeOrigin(originHeader) + if (requestOrigin && allowedOrigins.includes(requestOrigin)) { + headers.set('Access-Control-Allow-Origin', requestOrigin) + } + + return headers +} diff --git a/svelte.config.js b/svelte.config.js index bbe7936..8a3b094 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from '@sveltejs/adapter-auto' +import adapter from '@sveltejs/adapter-vercel' import preprocess from 'svelte-preprocess' /** @type {import('@sveltejs/kit').Config} */ diff --git a/tests/proxy.test.ts b/tests/proxy.test.ts new file mode 100644 index 0000000..761a4e5 --- /dev/null +++ b/tests/proxy.test.ts @@ -0,0 +1,136 @@ +import { + buildCorsHeaders, + getAllowedProxyOrigins, + getRequestOrigin, + isAllowedOrigin, + isAllowedProxyTarget, + parseProxyTarget, + ProxyRequestError, +} from '../src/utils/proxy' + +function expectProxyError(fn: () => void, status: number) { + try { + fn() + throw new Error('Expected proxy policy error') + } catch (err) { + expect(err).toBeInstanceOf(ProxyRequestError) + expect((err as ProxyRequestError).status).toBe(status) + } +} + +describe('[Proxy] target validation', () => { + it('allows raw GitHub URLs', () => { + const target = new URL( + 'https://raw.githubusercontent.com/acme/site/main/index.html', + ) + + expect(isAllowedProxyTarget(target)).toBe(true) + }) + + it('allows GitLab raw URLs with subgroup path', () => { + const target = new URL( + 'https://gitlab.com/acme/subgroup/site/-/raw/main/index.html', + ) + + expect(isAllowedProxyTarget(target)).toBe(true) + }) + + it('rejects non-https URLs', () => { + expectProxyError( + () => + parseProxyTarget( + 'http://raw.githubusercontent.com/acme/site/main/index.html', + ), + 403, + ) + }) + + it('rejects non-allowlisted hosts', () => { + expectProxyError( + () => parseProxyTarget('https://github.com/acme/site/blob/main/index.html'), + 403, + ) + }) + + it('rejects non-raw GitLab URLs', () => { + expectProxyError( + () => parseProxyTarget('https://gitlab.com/acme/site/-/blob/main/index.html'), + 403, + ) + }) + + it('rejects path traversal attempts', () => { + expectProxyError( + () => + parseProxyTarget( + 'https://raw.githubusercontent.com/acme/site/main/%2e%2e/index.html', + ), + 403, + ) + }) +}) + +describe('[Proxy] origin and CORS policy', () => { + it('returns configured origins when provided', () => { + const allowedOrigins = getAllowedProxyOrigins( + 'https://preview.example.com, https://preview.example.com, invalid-origin', + 'https://ignored.example.com', + ) + + expect(allowedOrigins).toStrictEqual(['https://preview.example.com']) + }) + + it('falls back to default + request origin when env var missing', () => { + const allowedOrigins = getAllowedProxyOrigins( + undefined, + 'https://preview.example.com', + ) + + expect(allowedOrigins).toContain('https://preview.example.com') + expect(allowedOrigins).toContain('https://static-preview.vercel.app') + expect(allowedOrigins).toContain('http://localhost:5173') + }) + + it('prefers Origin header over Referer', () => { + const headers = new Headers({ + origin: 'https://preview.example.com', + referer: 'https://other.example.com/path', + }) + + expect(getRequestOrigin(headers)).toBe('https://preview.example.com') + }) + + it('falls back to Referer when Origin header is missing', () => { + const headers = new Headers({ + referer: 'https://preview.example.com/path', + }) + + expect(getRequestOrigin(headers)).toBe('https://preview.example.com') + }) + + it('matches request origins against allowlist', () => { + expect( + isAllowedOrigin('https://preview.example.com', [ + 'https://preview.example.com', + ]), + ).toBe(true) + + expect(isAllowedOrigin(undefined, ['https://preview.example.com'])).toBe( + false, + ) + }) + + it('only sets ACAO for allowed origins', () => { + const allowedCorsHeaders = buildCorsHeaders('https://preview.example.com', [ + 'https://preview.example.com', + ]) + expect(allowedCorsHeaders.get('Access-Control-Allow-Origin')).toBe( + 'https://preview.example.com', + ) + + const blockedCorsHeaders = buildCorsHeaders('https://evil.example.com', [ + 'https://preview.example.com', + ]) + expect(blockedCorsHeaders.get('Access-Control-Allow-Origin')).toBeNull() + }) +}) From 2b883825a06067ebe32bb6efca1bfa4b4504b916 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Apr 2026 20:25:40 +0000 Subject: [PATCH 2/6] Refine proxy checks and ignore Vercel output Co-authored-by: Siraj Chokshi --- .gitignore | 1 + src/routes/api/proxy/+server.ts | 8 +++++--- src/utils/proxy.ts | 8 ++++++-- tests/proxy.test.ts | 6 ++++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index f4401a3..8f5041b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules .env .env.* !.env.example +/.vercel diff --git a/src/routes/api/proxy/+server.ts b/src/routes/api/proxy/+server.ts index 6047d85..699fe1a 100644 --- a/src/routes/api/proxy/+server.ts +++ b/src/routes/api/proxy/+server.ts @@ -1,4 +1,3 @@ -import { env } from '$env/dynamic/private' import { error } from '@sveltejs/kit' import { buildCorsHeaders, @@ -14,7 +13,7 @@ export const prerender = false function enforceProxyPolicy(request: Request, requestUrl: URL) { const allowedOrigins = getAllowedProxyOrigins( - env.PROXY_ALLOWED_ORIGINS, + process.env.PROXY_ALLOWED_ORIGINS, requestUrl.origin, ) const requestOrigin = getRequestOrigin(request.headers) @@ -82,7 +81,10 @@ export async function GET({ request, url, fetch }) { upstream.headers.get('content-type') ?? 'text/plain; charset=utf-8', ) headers.set('X-Content-Type-Options', 'nosniff') - headers.set('Cache-Control', upstream.headers.get('cache-control') ?? 'public, max-age=300') + headers.set( + 'Cache-Control', + upstream.headers.get('cache-control') ?? 'public, max-age=300', + ) const etag = upstream.headers.get('etag') if (etag) { diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 3acf78e..6886919 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -17,7 +17,9 @@ export class ProxyRequestError extends Error { } } -function normalizeOrigin(origin: string | null | undefined): string | undefined { +function normalizeOrigin( + origin: string | null | undefined, +): string | undefined { if (!origin) return undefined try { @@ -37,7 +39,9 @@ function hasUnsafePathSegment(pathSegments: string[]): boolean { return true } - return decodedSegment === '' || decodedSegment === '.' || decodedSegment === '..' + return ( + decodedSegment === '' || decodedSegment === '.' || decodedSegment === '..' + ) }) } diff --git a/tests/proxy.test.ts b/tests/proxy.test.ts index 761a4e5..4326ee3 100644 --- a/tests/proxy.test.ts +++ b/tests/proxy.test.ts @@ -47,14 +47,16 @@ describe('[Proxy] target validation', () => { it('rejects non-allowlisted hosts', () => { expectProxyError( - () => parseProxyTarget('https://github.com/acme/site/blob/main/index.html'), + () => + parseProxyTarget('https://github.com/acme/site/blob/main/index.html'), 403, ) }) it('rejects non-raw GitLab URLs', () => { expectProxyError( - () => parseProxyTarget('https://gitlab.com/acme/site/-/blob/main/index.html'), + () => + parseProxyTarget('https://gitlab.com/acme/site/-/blob/main/index.html'), 403, ) }) From 384cffa08a2cee881bc499d6b1f5233737601a0f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Apr 2026 20:45:41 +0000 Subject: [PATCH 3/6] Fix SSR logger import and ignore Vercel build output in lint Co-authored-by: Siraj Chokshi --- eslint.config.js | 1 + src/utils/logger.ts | 36 +++++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index f182248..21475cb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,6 +15,7 @@ export default [ '.DS_Store', 'node_modules/**', 'build/**', + '.vercel/**', '.svelte-kit/**', 'package/**', '.env', diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 167b982..f3c51a2 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,27 +1,37 @@ import logbench from 'logbench' -const logger = logbench({ - logFn: (message) => - console.log( - `%c${message}`, - ` +// Check if we're in browser environment +const isBrowser = typeof window !== 'undefined' + +const logger = isBrowser + ? logbench({ + logFn: (message) => + console.log( + `%c${message}`, + ` font-size: 12px; background: black; color: white; padding: 5px; `, - ), - warnFn: (message) => - console.log( - `%c${message}`, - ` + ), + warnFn: (message) => + console.log( + `%c${message}`, + ` font-size: 12px; background: #200e00; color: #feeada; padding: 5px; `, - ), - isProduction: process.env.NODE_ENV === 'production', -}) + ), + isProduction: process.env.NODE_ENV === 'production', + }) + : { + log: () => {}, + warn: () => {}, + time: () => {}, + timeEnd: () => {}, + } export default logger From c99837da742dc19a05a2ceaf0fdaab84f8ba8b43 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Apr 2026 21:06:27 +0000 Subject: [PATCH 4/6] Tighten GitLab raw path validation for file segment Co-authored-by: Siraj Chokshi --- src/utils/proxy.ts | 2 +- tests/proxy.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 6886919..279bdfe 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -70,7 +70,7 @@ function isAllowedGitlabRawPath(pathname: string): boolean { // /:owner/(optional subgroups...)/:repo/-/raw/:branch/:file... if (rawPathSegment < 2) return false - if (pathSegments.length <= rawPathSegment + 2) return false + if (pathSegments.length <= rawPathSegment + 3) return false return true } diff --git a/tests/proxy.test.ts b/tests/proxy.test.ts index 4326ee3..a62b6e4 100644 --- a/tests/proxy.test.ts +++ b/tests/proxy.test.ts @@ -61,6 +61,13 @@ describe('[Proxy] target validation', () => { ) }) + it('rejects GitLab raw URLs with no file segment', () => { + expectProxyError( + () => parseProxyTarget('https://gitlab.com/acme/site/-/raw/main'), + 403, + ) + }) + it('rejects path traversal attempts', () => { expectProxyError( () => From aa0bc5205ba4792c244e1b5e35176a694954eec2 Mon Sep 17 00:00:00 2001 From: Siraj Chokshi <19193347+SirajChokshi@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:06:13 -0700 Subject: [PATCH 5/6] Fix: don't fall back to permissive defaults when all configured origins are invalid (#29) When PROXY_ALLOWED_ORIGINS is set but all entries fail normalizeOrigin (e.g. missing protocol), the function now returns the empty valid set instead of silently falling back to DEFAULT_ALLOWED_ORIGINS which includes localhost. Only fall back to defaults when the env var is truly undefined. Co-authored-by: Cursor Agent --- src/utils/proxy.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 279bdfe..61c0c61 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -120,14 +120,14 @@ export function getAllowedProxyOrigins( configuredOrigins: string | undefined, requestOrigin: string, ): string[] { - const allowedOrigins = configuredOrigins - ?.split(',') - .map((origin) => origin.trim()) - .filter(Boolean) - .map((origin) => normalizeOrigin(origin)) - .filter((origin): origin is string => Boolean(origin)) - - if (allowedOrigins && allowedOrigins.length > 0) { + if (configuredOrigins !== undefined) { + const allowedOrigins = configuredOrigins + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean) + .map((origin) => normalizeOrigin(origin)) + .filter((origin): origin is string => Boolean(origin)) + return [...new Set(allowedOrigins)] } From 8176d833ca58a1187dd80cd1d0962379a18f3b10 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Apr 2026 22:54:56 +0000 Subject: [PATCH 6/6] Harden proxy responses against same-origin HTML execution Co-authored-by: Siraj Chokshi --- src/routes/api/proxy/+server.ts | 14 +++++++++++++- src/utils/proxy.ts | 31 +++++++++++++++++++++++++++++++ tests/proxy.test.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/routes/api/proxy/+server.ts b/src/routes/api/proxy/+server.ts index 699fe1a..0ea3e8d 100644 --- a/src/routes/api/proxy/+server.ts +++ b/src/routes/api/proxy/+server.ts @@ -4,9 +4,11 @@ import { getAllowedProxyOrigins, getRequestOrigin, isAllowedOrigin, + isNavigationProxyRequest, MAX_PROXY_RESPONSE_BYTES, parseProxyTarget, ProxyRequestError, + sanitizeProxyContentType, } from '../../../utils/proxy' export const prerender = false @@ -37,6 +39,10 @@ export function OPTIONS({ request, url }) { export async function GET({ request, url, fetch }) { const corsHeaders = enforceProxyPolicy(request, url) + if (isNavigationProxyRequest(request.headers.get('sec-fetch-dest'))) { + throw error(403, 'Direct navigation to proxy endpoint is not allowed') + } + let target: URL try { @@ -78,9 +84,15 @@ export async function GET({ request, url, fetch }) { const headers = new Headers(corsHeaders) headers.set( 'Content-Type', - upstream.headers.get('content-type') ?? 'text/plain; charset=utf-8', + sanitizeProxyContentType(upstream.headers.get('content-type')), ) headers.set('X-Content-Type-Options', 'nosniff') + headers.set('Content-Disposition', 'attachment') + headers.set( + 'Content-Security-Policy', + "sandbox; default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'", + ) + headers.set('X-Frame-Options', 'DENY') headers.set( 'Cache-Control', upstream.headers.get('cache-control') ?? 'public, max-age=300', diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 279bdfe..c6074bb 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -5,6 +5,14 @@ const DEFAULT_ALLOWED_ORIGINS = [ 'https://static-preview.vercel.app', 'http://localhost:5173', ] +const NAVIGATION_DESTINATIONS = new Set([ + 'document', + 'iframe', + 'frame', + 'object', + 'embed', +]) +const SAFE_PROXY_CONTENT_TYPE = 'text/plain; charset=utf-8' export const MAX_PROXY_RESPONSE_BYTES = 5 * 1024 * 1024 // 5 MiB @@ -167,3 +175,26 @@ export function buildCorsHeaders( return headers } + +export function isNavigationProxyRequest(secFetchDest: string | null): boolean { + if (!secFetchDest) return false + return NAVIGATION_DESTINATIONS.has(secFetchDest.toLowerCase()) +} + +export function sanitizeProxyContentType( + upstreamContentType: string | null, +): string { + if (!upstreamContentType) { + return SAFE_PROXY_CONTENT_TYPE + } + + const normalizedContentType = upstreamContentType.toLowerCase() + if ( + normalizedContentType.startsWith('text/html') || + normalizedContentType.startsWith('application/xhtml+xml') + ) { + return SAFE_PROXY_CONTENT_TYPE + } + + return upstreamContentType +} diff --git a/tests/proxy.test.ts b/tests/proxy.test.ts index a62b6e4..2305edd 100644 --- a/tests/proxy.test.ts +++ b/tests/proxy.test.ts @@ -3,9 +3,11 @@ import { getAllowedProxyOrigins, getRequestOrigin, isAllowedOrigin, + isNavigationProxyRequest, isAllowedProxyTarget, parseProxyTarget, ProxyRequestError, + sanitizeProxyContentType, } from '../src/utils/proxy' function expectProxyError(fn: () => void, status: number) { @@ -143,3 +145,29 @@ describe('[Proxy] origin and CORS policy', () => { expect(blockedCorsHeaders.get('Access-Control-Allow-Origin')).toBeNull() }) }) + +describe('[Proxy] response hardening', () => { + it('blocks navigation-style requests', () => { + expect(isNavigationProxyRequest('document')).toBe(true) + expect(isNavigationProxyRequest('iframe')).toBe(true) + expect(isNavigationProxyRequest('empty')).toBe(false) + expect(isNavigationProxyRequest(null)).toBe(false) + }) + + it('downgrades HTML content types to plain text', () => { + expect(sanitizeProxyContentType('text/html; charset=utf-8')).toBe( + 'text/plain; charset=utf-8', + ) + expect(sanitizeProxyContentType('application/xhtml+xml')).toBe( + 'text/plain; charset=utf-8', + ) + }) + + it('keeps non-HTML content types unchanged', () => { + expect(sanitizeProxyContentType('text/css')).toBe('text/css') + expect(sanitizeProxyContentType('application/javascript')).toBe( + 'application/javascript', + ) + expect(sanitizeProxyContentType(null)).toBe('text/plain; charset=utf-8') + }) +})