Compare commits

...
Author SHA1 Message Date
Jesse Merhi dfde546167 fix: prefer edge-forwarded client IP 2026-06-29 13:52:56 +10:00
Jesse Merhi 526f2a2d04 fix: trust forwarded IPs from ClawHub edge 2026-06-28 00:07:47 +10:00
10 changed files with 243 additions and 31 deletions
+51
View File
@@ -31,6 +31,7 @@
"@tanstack/react-router": "1.170.16",
"@tanstack/react-start": "1.168.26",
"@vercel/analytics": "2.0.1",
"@vercel/functions": "3.7.3",
"@vercel/speed-insights": "2.0.0",
"class-variance-authority": "0.7.1",
"clawhub-schema": "workspace:0.0.2",
@@ -932,6 +933,14 @@
"@vercel/analytics": ["@vercel/analytics@2.0.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g=="],
"@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="],
"@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="],
"@vercel/functions": ["@vercel/functions@3.7.3", "", { "dependencies": { "@vercel/oidc": "3.7.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*", "ws": ">=8" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity", "ws"] }, "sha512-Bn7VPthKb6jBiGM3W2JUXDgDl/2trIanvnnxQ9dCVpDlDLvwn/+uk1TM9bSQJuZ4dKbOfusstpbM0PZeuNStsw=="],
"@vercel/oidc": ["@vercel/oidc@3.7.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-FWmULATInXyxER8FJjtSEUdoaoeqWv9G9T4y3vraYMJNAx/ox354COtDmhiBb/XaeDjFuiwTY9ss44NRBaGjLQ=="],
"@vercel/speed-insights": ["@vercel/speed-insights@2.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
@@ -1054,6 +1063,8 @@
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crossws": ["crossws@0.4.5", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA=="],
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
@@ -1204,6 +1215,8 @@
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
@@ -1236,6 +1249,8 @@
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
@@ -1286,6 +1301,8 @@
"httpxy": ["httpxy@0.5.3", "", {}, "sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -1312,10 +1329,14 @@
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"isbot": ["isbot@5.1.41", "", {}, "sha512-9WFV/Vhh0FEj6CQ7MoHweEL9/vLKPjeoD2I2htbAjX7kbW7VJs3OCpWOVyd+JraNTWVU6/DRx2MZy2KaUNXHcg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
@@ -1434,6 +1455,8 @@
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
@@ -1498,6 +1521,8 @@
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
@@ -1524,6 +1549,8 @@
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="],
"oauth4webapi": ["oauth4webapi@3.8.6", "", {}, "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ=="],
@@ -1550,6 +1577,8 @@
"ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
"os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="],
"oxc-parser": ["oxc-parser@0.120.0", "", { "dependencies": { "@oxc-project/types": "^0.120.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.120.0", "@oxc-parser/binding-android-arm64": "0.120.0", "@oxc-parser/binding-darwin-arm64": "0.120.0", "@oxc-parser/binding-darwin-x64": "0.120.0", "@oxc-parser/binding-freebsd-x64": "0.120.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.120.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.120.0", "@oxc-parser/binding-linux-arm64-gnu": "0.120.0", "@oxc-parser/binding-linux-arm64-musl": "0.120.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-musl": "0.120.0", "@oxc-parser/binding-linux-s390x-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-musl": "0.120.0", "@oxc-parser/binding-openharmony-arm64": "0.120.0", "@oxc-parser/binding-wasm32-wasi": "0.120.0", "@oxc-parser/binding-win32-arm64-msvc": "0.120.0", "@oxc-parser/binding-win32-ia32-msvc": "0.120.0", "@oxc-parser/binding-win32-x64-msvc": "0.120.0" } }, "sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w=="],
"oxfmt": ["oxfmt@0.56.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.56.0", "@oxfmt/binding-android-arm64": "0.56.0", "@oxfmt/binding-darwin-arm64": "0.56.0", "@oxfmt/binding-darwin-x64": "0.56.0", "@oxfmt/binding-freebsd-x64": "0.56.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.56.0", "@oxfmt/binding-linux-arm-musleabihf": "0.56.0", "@oxfmt/binding-linux-arm64-gnu": "0.56.0", "@oxfmt/binding-linux-arm64-musl": "0.56.0", "@oxfmt/binding-linux-ppc64-gnu": "0.56.0", "@oxfmt/binding-linux-riscv64-gnu": "0.56.0", "@oxfmt/binding-linux-riscv64-musl": "0.56.0", "@oxfmt/binding-linux-s390x-gnu": "0.56.0", "@oxfmt/binding-linux-x64-gnu": "0.56.0", "@oxfmt/binding-linux-x64-musl": "0.56.0", "@oxfmt/binding-openharmony-arm64": "0.56.0", "@oxfmt/binding-win32-arm64-msvc": "0.56.0", "@oxfmt/binding-win32-ia32-msvc": "0.56.0", "@oxfmt/binding-win32-x64-msvc": "0.56.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-9Dv0wV3zKiyvhjD7bRKaInKmHQ1sPx3RGOjQkGFJbbdQ16576yf8qhMSO9Q9cvHcs+1NpBsRTkuDDYFFPTJ6gw=="],
@@ -1570,6 +1599,8 @@
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
@@ -1696,6 +1727,10 @@
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="],
"shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="],
@@ -1744,6 +1779,8 @@
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
@@ -1868,12 +1905,18 @@
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="],
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="],
@@ -1938,6 +1981,10 @@
"@types/ws/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
"@vercel/oidc/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
@@ -1962,6 +2009,10 @@
"engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+56 -4
View File
@@ -66,8 +66,10 @@ function componentRateLimitCalls(runMutation: ReturnType<typeof vi.fn>) {
describe("getClientIp", () => {
let prev: string | undefined;
let prevEdgeSecret: string | undefined;
beforeEach(() => {
prev = process.env.TRUST_FORWARDED_IPS;
prevEdgeSecret = process.env.CLAWHUB_EDGE_SECRET;
});
afterEach(() => {
if (prev === undefined) {
@@ -75,6 +77,11 @@ describe("getClientIp", () => {
} else {
process.env.TRUST_FORWARDED_IPS = prev;
}
if (prevEdgeSecret === undefined) {
delete process.env.CLAWHUB_EDGE_SECRET;
} else {
process.env.CLAWHUB_EDGE_SECRET = prevEdgeSecret;
}
});
it("returns null when cf-connecting-ip is missing (CF-only default)", () => {
@@ -107,34 +114,75 @@ describe("getClientIp", () => {
expect(getClientIp(request)).toBeNull();
});
it("returns first ip from cf-connecting-ip when trusted mode is enabled", () => {
it("ignores forwarded headers when trusted mode lacks the edge secret", () => {
const request = new Request("https://example.com", {
headers: {
"x-forwarded-for": "203.0.113.9",
},
});
process.env.TRUST_FORWARDED_IPS = "true";
delete process.env.CLAWHUB_EDGE_SECRET;
expect(getClientIp(request)).toBeNull();
});
it("ignores forwarded headers when the edge secret header is missing", () => {
const request = new Request("https://example.com", {
headers: {
"x-forwarded-for": "203.0.113.9",
},
});
process.env.TRUST_FORWARDED_IPS = "true";
process.env.CLAWHUB_EDGE_SECRET = "edge-secret";
expect(getClientIp(request)).toBeNull();
});
it("ignores forwarded headers when the edge secret header is wrong", () => {
const request = new Request("https://example.com", {
headers: {
"x-clawhub-edge-secret": "wrong",
"x-forwarded-for": "203.0.113.9",
},
});
process.env.TRUST_FORWARDED_IPS = "true";
process.env.CLAWHUB_EDGE_SECRET = "edge-secret";
expect(getClientIp(request)).toBeNull();
});
it("falls back to cf-connecting-ip when trusted edge mode has no forwarded header", () => {
const request = new Request("https://example.com", {
headers: {
"x-clawhub-edge-secret": "edge-secret",
"cf-connecting-ip": "203.0.113.1, 198.51.100.2",
},
});
process.env.TRUST_FORWARDED_IPS = "true";
process.env.CLAWHUB_EDGE_SECRET = "edge-secret";
expect(getClientIp(request)).toBe("203.0.113.1");
});
it("uses forwarded headers when opt-in enabled", () => {
it("uses forwarded headers when trusted edge mode is enabled", () => {
const request = new Request("https://example.com", {
headers: {
"x-clawhub-edge-secret": "edge-secret",
"x-forwarded-for": "203.0.113.9, 198.51.100.2",
},
});
process.env.TRUST_FORWARDED_IPS = "true";
process.env.CLAWHUB_EDGE_SECRET = "edge-secret";
expect(getClientIp(request)).toBe("203.0.113.9");
});
it("prefers x-forwarded-for over x-real-ip when trusted mode is enabled", () => {
it("prefers x-forwarded-for over edge connection IP headers", () => {
const request = new Request("https://example.com", {
headers: {
"x-clawhub-edge-secret": "edge-secret",
"cf-connecting-ip": "192.0.2.44",
"x-forwarded-for": "203.0.113.9, 198.51.100.2",
"x-real-ip": "198.51.100.77",
},
});
process.env.TRUST_FORWARDED_IPS = "true";
process.env.CLAWHUB_EDGE_SECRET = "edge-secret";
expect(getClientIp(request)).toBe("203.0.113.9");
});
});
@@ -557,6 +605,7 @@ describe("applyRateLimit headers", () => {
it("scopes known-ip anonymous buckets by rate limit kind", async () => {
vi.stubEnv("TRUST_FORWARDED_IPS", "true");
vi.stubEnv("CLAWHUB_EDGE_SECRET", "edge-secret");
vi.spyOn(Date, "now").mockReturnValue(4_550_000);
const readCtx = makeRateLimitCtx({
ip: {
@@ -575,7 +624,10 @@ describe("applyRateLimit headers", () => {
},
});
const request = new Request("https://example.com/api/v1/packages/demo/download", {
headers: { "cf-connecting-ip": "203.0.113.1" },
headers: {
"x-clawhub-edge-secret": "edge-secret",
"cf-connecting-ip": "203.0.113.1",
},
});
await applyRateLimit(readCtx, request, "read");
+15 -12
View File
@@ -17,6 +17,7 @@ const RATE_LIMIT_WINDOW_MS = 60_000;
const HTTP_RATE_LIMIT_SHARDS = 16;
const HTTP_RATE_LIMIT_MIN_SHARD_CAPACITY = 10;
const HTTP_RATE_LIMIT_KEY_TTL_MS = 24 * 60 * 60 * 1000;
const EDGE_SECRET_HEADER = "x-clawhub-edge-secret";
type RateLimitResult = {
allowed: boolean;
@@ -184,25 +185,23 @@ function getAuthenticatedRateLimit(kind: RateLimitKind, user: Pick<Doc<"users">,
}
export function getClientIp(request: Request): string | null {
if (!shouldTrustClientIpHeaders()) return null;
const cfHeader = request.headers.get("cf-connecting-ip");
if (cfHeader) return splitFirstIp(cfHeader);
if (!shouldTrustClientIpHeaders(request)) return null;
const forwarded =
request.headers.get("x-forwarded-for") ??
request.headers.get("x-real-ip") ??
request.headers.get("fly-client-ip");
request.headers.get("fly-client-ip") ??
request.headers.get("cf-connecting-ip");
return splitFirstIp(forwarded);
}
function getClientIpSource(request: Request) {
if (!shouldTrustClientIpHeaders()) return "none";
if (request.headers.get("cf-connecting-ip")) return "cf-connecting-ip";
if (!shouldTrustClientIpHeaders(request)) return "none";
if (request.headers.get("x-forwarded-for")) return "x-forwarded-for";
if (request.headers.get("x-real-ip")) return "x-real-ip";
if (request.headers.get("fly-client-ip")) return "fly-client-ip";
if (request.headers.get("cf-connecting-ip")) return "cf-connecting-ip";
return "none";
}
@@ -311,13 +310,17 @@ function splitFirstIp(header: string | null) {
return trimmed || null;
}
function shouldTrustClientIpHeaders() {
function shouldTrustClientIpHeaders(request: Request) {
const value = (process.env.TRUST_FORWARDED_IPS ?? "").trim().toLowerCase();
// Direct Convex HTTP endpoints can be reached without ClawHub's edge. Trust
// client IP headers only when the deployment is explicitly behind that edge.
if (!value) return false;
if (value === "1" || value === "true" || value === "yes") return true;
return false;
if (!(value === "1" || value === "true" || value === "yes")) return false;
// Direct Convex HTTP endpoints are public. Only trust forwarded IP headers
// when ClawHub's edge has stripped caller-supplied headers and added this
// deployment secret.
const edgeSecret = process.env.CLAWHUB_EDGE_SECRET?.trim();
if (!edgeSecret) return false;
return request.headers.get(EDGE_SECRET_HEADER) === edgeSecret;
}
function isRateLimitWriteConflict(error: unknown) {
+7 -3
View File
@@ -76,9 +76,13 @@ Client guidance:
IP source:
- Uses trusted client IP headers, including `cf-connecting-ip`, only when the
deployment explicitly enables trusted forwarded headers.
- ClawHub uses trusted forwarding headers to identify client IPs at the edge.
- Uses trusted client IP headers only when the deployment explicitly enables
trusted forwarded headers and the request was marked by ClawHub's edge trust
secret.
- Prefers the edge-stamped `x-forwarded-for` value over connection IP headers
so Vercel rewrites are attributed to the original client, not Vercel egress.
- ClawHub uses trusted forwarding headers to identify client IPs at the edge;
direct Convex-origin requests without the edge secret use fallback buckets.
- If no trusted client IP is available, anonymous requests use fallback buckets
scoped only by rate-limit kind. These fallback buckets do not include
caller-supplied paths, slugs, package names, versions, query strings, or other
+59
View File
@@ -0,0 +1,59 @@
/* @vitest-environment node */
import { afterEach, describe, expect, it, vi } from "vitest";
import middleware, { config } from "./middleware";
function getForwardedHeader(response: Response, name: string) {
return response.headers.get(`x-middleware-request-${name}`);
}
describe("Vercel API middleware", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("adds an edge trust secret and Vercel-derived client IP for Convex rewrites", () => {
vi.stubEnv("CLAWHUB_EDGE_SECRET", "edge-secret");
const response = middleware(
new Request("https://clawhub.ai/api/v1/packages/demo", {
headers: {
"x-clawhub-edge-secret": "caller-secret",
"cf-connecting-ip": "192.0.2.10",
"x-forwarded-for": "192.0.2.11",
"x-real-ip": "203.0.113.9",
"fly-client-ip": "192.0.2.12",
},
}),
);
expect(response.headers.get("x-middleware-next")).toBe("1");
expect(getForwardedHeader(response, "x-clawhub-edge-secret")).toBe("edge-secret");
expect(getForwardedHeader(response, "x-forwarded-for")).toBe("203.0.113.9");
expect(getForwardedHeader(response, "x-real-ip")).toBe("203.0.113.9");
expect(getForwardedHeader(response, "cf-connecting-ip")).toBeNull();
expect(getForwardedHeader(response, "fly-client-ip")).toBeNull();
});
it("strips caller-supplied trust and IP headers when edge trust is not configured", () => {
const response = middleware(
new Request("https://clawhub.ai/api/v1/packages/demo", {
headers: {
"x-clawhub-edge-secret": "caller-secret",
"cf-connecting-ip": "192.0.2.10",
"x-forwarded-for": "192.0.2.11",
"x-real-ip": "203.0.113.9",
"fly-client-ip": "192.0.2.12",
},
}),
);
expect(getForwardedHeader(response, "x-clawhub-edge-secret")).toBeNull();
expect(getForwardedHeader(response, "cf-connecting-ip")).toBeNull();
expect(getForwardedHeader(response, "x-forwarded-for")).toBeNull();
expect(getForwardedHeader(response, "x-real-ip")).toBeNull();
expect(getForwardedHeader(response, "fly-client-ip")).toBeNull();
});
it("runs only on Convex-backed API and feed routes", () => {
expect(config.matcher).toEqual(["/api/:path*", "/v1/feeds/plugins", "/v1/feeds/skills"]);
});
});
+29
View File
@@ -0,0 +1,29 @@
import { ipAddress, next } from "@vercel/functions";
const EDGE_SECRET_HEADER = "x-clawhub-edge-secret";
const CLIENT_IP_HEADERS = [
"cf-connecting-ip",
"x-forwarded-for",
"x-real-ip",
"fly-client-ip",
] as const;
export default function middleware(request: Request): Response {
const headers = new Headers(request.headers);
for (const header of CLIENT_IP_HEADERS) headers.delete(header);
headers.delete(EDGE_SECRET_HEADER);
const edgeSecret = process.env.CLAWHUB_EDGE_SECRET?.trim();
const clientIp = ipAddress(request);
if (edgeSecret && clientIp) {
headers.set(EDGE_SECRET_HEADER, edgeSecret);
headers.set("x-forwarded-for", clientIp);
headers.set("x-real-ip", clientIp);
}
return next({ request: { headers } });
}
export const config = {
matcher: ["/api/:path*", "/v1/feeds/plugins", "/v1/feeds/skills"],
};
+3 -2
View File
@@ -44,8 +44,8 @@
"format": "oxfmt --write",
"format:check": "oxfmt --check",
"lint": "bun run lint:oxlint",
"lint:fix": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/clawhub/src ./packages/clawhub-admin/src ./packages/schema/src --fix && bun run format",
"lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/clawhub/src ./packages/clawhub-admin/src ./packages/schema/src",
"lint:fix": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./middleware.ts ./src ./convex ./packages/clawhub/src ./packages/clawhub-admin/src ./packages/schema/src --fix && bun run format",
"lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./middleware.ts ./src ./convex ./packages/clawhub/src ./packages/clawhub-admin/src ./packages/schema/src",
"llms:check": "bun scripts/generate-llms-txt.ts --check",
"llms:generate": "bun scripts/generate-llms-txt.ts",
"preinstall": "bunx --bun only-allow@1.2.2 bun",
@@ -98,6 +98,7 @@
"@tanstack/react-router": "1.170.16",
"@tanstack/react-start": "1.168.26",
"@vercel/analytics": "2.0.1",
"@vercel/functions": "3.7.3",
"@vercel/speed-insights": "2.0.0",
"class-variance-authority": "0.7.1",
"clawhub-schema": "workspace:0.0.2",
+12 -4
View File
@@ -130,11 +130,17 @@ Ensure Convex env is set (auth + embeddings):
- `GITHUB_APP_PRIVATE_KEY`
- Optional fallback: `GITHUB_TOKEN` (used when GitHub App auth is unavailable,
and for arbitrary public repository lookups such as trusted-publisher setup)
- `CLAWHUB_EDGE_SECRET` and `TRUST_FORWARDED_IPS=true` when the matching Vercel
deployment also has the same `CLAWHUB_EDGE_SECRET`.
Do not set `TRUST_FORWARDED_IPS=true` while the Convex `*.convex.site` HTTP
origin remains publicly reachable. That flag makes rate limits and download
metrics trust forwarded client IP headers, so it is only safe behind a
header-sanitizing edge that prevents direct origin requests.
`TRUST_FORWARDED_IPS=true` is only effective when Convex also has
`CLAWHUB_EDGE_SECRET`. The Vercel middleware strips caller-supplied client IP and
edge trust headers before adding its own secret plus `x-forwarded-for` /
`x-real-ip` for `/api/*` and hosted feed rewrites. Convex prefers that
edge-stamped `x-forwarded-for` value over connection IP headers, because the
connection into Convex may be Vercel rather than the original client. Direct
`*.convex.site` calls without the secret remain in the missing-IP fallback
bucket.
## 2) Deploy web app (Vercel)
@@ -163,6 +169,8 @@ This repo currently uses `vercel.json` rewrites:
For self-host:
- update `vercel.json` to your deployment's Convex site URL.
- set the same `CLAWHUB_EDGE_SECRET` in Vercel and Convex before enabling
`TRUST_FORWARDED_IPS=true` in Convex.
## 4) Registry discovery
+10 -6
View File
@@ -9,12 +9,16 @@ telemetry writes, delete/undelete mutations, or search queries.
Client IP headers are not trustworthy on direct Convex HTTP endpoints. Treat
`cf-connecting-ip`, `x-forwarded-for`, `x-real-ip`, and `fly-client-ip` as
trusted only when the deployment explicitly enables trusted forwarded headers.
Do not enable that opt-in while the Convex `*.convex.site` HTTP origin remains
publicly reachable. Without the opt-in, anonymous traffic must use conservative
missing-IP fallback buckets scoped only by rate-limit kind. Missing-IP buckets
must not include user-controlled paths, dynamic path segments, query parameters,
package names, skill slugs, or artifact versions.
trusted only when the deployment explicitly enables trusted forwarded headers
and the request carries ClawHub's edge trust secret. The Vercel middleware owns
that boundary: it strips caller-supplied client IP and edge trust headers before
adding `x-clawhub-edge-secret`, `x-forwarded-for`, and `x-real-ip` for Convex
rewrites. Convex prefers the edge-stamped `x-forwarded-for` over connection IP
headers so Vercel rewrites are limited by the original client IP, not the Vercel
egress IP. Direct `*.convex.site` callers without the secret must continue to
use conservative missing-IP fallback buckets scoped only by rate-limit kind.
Missing-IP buckets must not include user-controlled paths, dynamic path
segments, query parameters, package names, skill slugs, or artifact versions.
Artifact-specific download scoping is only safe after the caller has an
authenticated identity or a trusted client IP.
+1
View File
@@ -1,5 +1,6 @@
{
"include": [
"middleware.ts",
"src",
"convex",
"packages/clawhub/src",