diff --git a/package.json b/package.json index 0c37c09..eecc9d4 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ } }, "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.lib.json", + "build": "pnpm run build:hosted-browser-module && rm -rf dist && tsc -p tsconfig.lib.json", + "build:hosted-browser-module": "tsx scripts/build-hosted-browser-module.ts", "check": "pnpm run typecheck && pnpm run lint:check && pnpm run format:check", "typecheck": "tsc -p tsconfig.json && pnpm --recursive --filter '!captun' --if-present run typecheck", "format": "oxfmt", @@ -75,7 +76,7 @@ "lint": "oxlint . --threads 1 --deny-warnings", "lint:check": "pnpm run lint", "lint:fix": "oxlint . --fix", - "deploy": "wrangler deploy", + "deploy": "pnpm run build:hosted-browser-module && wrangler deploy", "dev": "wrangler dev", "test": "vitest run", "test:unit": "vitest run test/worker.test.ts", @@ -94,7 +95,10 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20260515.1", + "@codemirror/lang-javascript": "6.2.4", + "@modelcontextprotocol/sdk": "1.29.0", "@types/node": "^25.8.0", + "codemirror": "6.0.1", "esbuild": "^0.28.0", "miniflare": "^4.20260515.0", "oxfmt": "^0.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8807cb6..c828247 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,9 +27,18 @@ importers: '@cloudflare/workers-types': specifier: ^4.20260515.1 version: 4.20260518.1 + '@codemirror/lang-javascript': + specifier: 6.2.4 + version: 6.2.4 + '@modelcontextprotocol/sdk': + specifier: 1.29.0 + version: 1.29.0(zod@4.4.3) '@types/node': specifier: ^25.8.0 version: 25.8.0 + codemirror: + specifier: 6.0.1 + version: 6.0.1 esbuild: specifier: ^0.28.0 version: 0.28.0 @@ -160,6 +169,30 @@ packages: '@cloudflare/workers-types@4.20260518.1': resolution: {integrity: sha512-xXzGrbRi8RHRBNQFgXYkzrB4DgF0RXvmp8E1vCxoBmINpeitM/ZjVDd1CNC+N3uXjgcNjacoz4OgTa0rxgig1A==} + '@codemirror/autocomplete@6.20.2': + resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} + + '@codemirror/commands@6.10.3': + resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/language@6.12.3': + resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + + '@codemirror/lint@6.9.6': + resolution: {integrity: sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==} + + '@codemirror/search@6.7.0': + resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} + + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + + '@codemirror/view@6.43.0': + resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -492,6 +525,12 @@ packages: '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -773,6 +812,31 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -1242,6 +1306,21 @@ packages: resolution: {integrity: sha512-kMwLlxUbduttIgaPdSkmEarFpP+mSY8FEm+QWMBRJwxOHWkri+cxd8KZHO9EMrB9vgUuz+5WEaCawaL5wGVoXg==} engines: {node: '>=18.0.0'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1249,9 +1328,25 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + bun-types@1.3.14: resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + capnweb@0.8.0: resolution: {integrity: sha512-BK/TuXUiyfLSKsmjojn70yN7oYG/JJzoURZ3tckjg5Zj2KcygPm0A5jyOlswK7SYB4f0Gh9tt+RZ132b80iLfA==} @@ -1266,27 +1361,97 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + codemirror@6.0.1: + resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -1297,19 +1462,50 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-wrap-ansi@0.2.0: resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} @@ -1322,15 +1518,84 @@ packages: picomatch: optional: true + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hono@4.12.23: + resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -1408,11 +1673,34 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + miniflare@4.20260515.0: resolution: {integrity: sha512-2j0oQWizk1Eu4Cm8tDX7Z+Nsjd0nebIj1TQcQ+Oy1QKeo0Ay9+bdn8wfLAtOj9znDCybDCUlnS1+nYvKXEdfNg==} engines: {node: '>=22.0.0'} hasBin: true + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@3.0.0: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -1422,9 +1710,28 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -1443,9 +1750,20 @@ packages: oxlint-tsgolint: optional: true + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1456,19 +1774,47 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + postcss@8.5.14: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + radash@12.1.1: resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==} engines: {node: '>=14.18.0'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + rolldown@1.0.1: resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1477,10 +1823,45 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1495,9 +1876,16 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -1525,6 +1913,10 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + trpc-cli@0.14.1: resolution: {integrity: sha512-yXycwKWAu322fJGQqdh9fbEPTLjmqnwrpHiLnLNM+kPEwDxXlkH3gbO9iOTmN5CNOBo+8Uk+Tuae+XS8uANz/w==} engines: {node: '>=18'} @@ -1562,6 +1954,10 @@ packages: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -1577,9 +1973,17 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + urlpattern-polyfill@10.1.0: resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite@8.0.13: resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1664,6 +2068,14 @@ packages: jsdom: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1684,6 +2096,9 @@ packages: '@cloudflare/workers-types': optional: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -1714,6 +2129,11 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -1744,6 +2164,62 @@ snapshots: '@cloudflare/workers-types@4.20260518.1': {} + '@codemirror/autocomplete@6.20.2': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.3': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/language@6.12.3': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.6': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + crelt: 1.0.6 + + '@codemirror/search@6.7.0': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + crelt: 1.0.6 + + '@codemirror/state@6.6.0': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.43.0': + dependencies: + '@codemirror/state': 6.6.0 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -1927,6 +2403,10 @@ snapshots: '@fastify/busboy@3.2.0': {} + '@hono/node-server@1.19.14(hono@4.12.23)': + dependencies: + hono: 4.12.23 + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -2151,6 +2631,46 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.2': {} + + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.23) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.23 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -2528,14 +3048,56 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + assertion-error@2.0.1: {} blake3-wasm@2.1.5: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + bun-types@1.3.14: dependencies: '@types/node': 25.8.0 + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + capnweb@0.8.0: {} chai@6.2.2: {} @@ -2544,18 +3106,75 @@ snapshots: cli-width@4.1.0: {} + codemirror@6.0.1: + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/search': 6.7.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + commander@14.0.3: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + crelt@1.0.6: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + detect-libc@2.1.2: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -2614,18 +3233,70 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 + escape-html@1.0.3: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 + etag@1.8.1: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + expect-type@1.3.0: {} + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: dependencies: fast-string-truncated-width: 3.0.3 + fast-uri@3.1.2: {} + fast-wrap-ansi@0.2.0: dependencies: fast-string-width: 3.0.2 @@ -2634,13 +3305,82 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hono@4.12.23: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.2.3: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + kleur@4.1.5: {} lightningcss-android-arm64@1.32.0: @@ -2696,6 +3436,18 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + miniflare@4.20260515.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -2708,12 +3460,28 @@ snapshots: - bufferutil - utf-8-validate + ms@2.1.3: {} + mute-stream@3.0.0: {} nanoid@3.3.12: {} + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + obug@2.1.1: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + openapi-types@12.1.3: {} oxfmt@0.35.0: @@ -2762,22 +3530,50 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.66.0 '@oxlint/binding-win32-x64-msvc': 1.66.0 + parseurl@1.3.3: {} + + path-key@3.1.1: {} + path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} + pathe@2.0.3: {} picocolors@1.1.1: {} picomatch@4.0.4: {} + pkce-challenge@5.0.1: {} + postcss@8.5.14: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + radash@12.1.1: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + require-from-string@2.0.2: {} + rolldown@1.0.1: dependencies: '@oxc-project/types': 0.130.0 @@ -2799,10 +3595,47 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.1 '@rolldown/binding-win32-x64-msvc': 1.0.1 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + safer-buffer@2.1.2: {} semver@7.8.0: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -2834,6 +3667,40 @@ snapshots: '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -2842,8 +3709,12 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.1.0: {} + style-mod@4.1.3: {} + supports-color@10.2.2: {} tagged-tag@1.0.0: {} @@ -2861,6 +3732,8 @@ snapshots: tinyrainbow@3.1.0: {} + toidentifier@1.0.1: {} + trpc-cli@0.14.1(@orpc/server@1.14.3(ws@8.20.1))(zod@4.4.3): dependencies: commander: 14.0.3 @@ -2880,6 +3753,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript@6.0.3: {} undici-types@7.24.6: {} @@ -2890,8 +3769,12 @@ snapshots: dependencies: pathe: 2.0.3 + unpipe@1.0.0: {} + urlpattern-polyfill@10.1.0: {} + vary@1.1.2: {} + vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.2): dependencies: lightningcss: 1.32.0 @@ -2932,6 +3815,12 @@ snapshots: transitivePeerDependencies: - msw + w3c-keyname@2.2.8: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -2962,6 +3851,8 @@ snapshots: - bufferutil - utf-8-validate + wrappy@1.0.2: {} + ws@8.18.0: {} ws@8.20.1: {} @@ -2979,4 +3870,8 @@ snapshots: cookie: 1.1.1 youch-core: 0.3.3 + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod@4.4.3: {} diff --git a/scripts/build-hosted-browser-module.ts b/scripts/build-hosted-browser-module.ts new file mode 100644 index 0000000..8a65ef0 --- /dev/null +++ b/scripts/build-hosted-browser-module.ts @@ -0,0 +1,59 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { build } from "esbuild"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const packageJson = JSON.parse(await readFile(resolve(repoRoot, "package.json"), "utf8")) as { + dependencies: { capnweb: string }; +}; +const capnwebVersion = packageJson.dependencies.capnweb.replace(/^[^\d]*/, ""); + +const result = await build({ + absWorkingDir: repoRoot, + bundle: true, + entryPoints: ["src/index.ts"], + external: [`https://esm.sh/capnweb@${capnwebVersion}`], + format: "esm", + keepNames: false, + minify: false, + platform: "browser", + plugins: [ + { + name: "browser-capnweb-import", + setup(build) { + build.onResolve({ filter: /^capnweb$/ }, () => ({ + external: true, + path: `https://esm.sh/capnweb@${capnwebVersion}`, + })); + }, + }, + ], + sourcemap: false, + target: "es2022", + write: false, +}); + +const output = result.outputFiles[0]?.text; +if (!output) throw new Error("esbuild did not produce a browser module"); + +await writeFile( + resolve(repoRoot, "src/hosted/browser-module.generated.ts"), + [ + "// Generated by scripts/build-hosted-browser-module.ts.", + "// Do not edit by hand.", + "export const WWW_BROWSER_MODULE =", + ` ${jsStringLiteral(output)};`, + "", + ].join("\n"), +); + +function jsStringLiteral(value: string) { + return `'${value + .replace(/\\/g, "\\\\") + .replace(/'/g, "\\'") + .replace(/\r/g, "\\r") + .replace(/\n/g, "\\n") + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029")}'`; +} diff --git a/src/hosted/browser-module.generated.ts b/src/hosted/browser-module.generated.ts new file mode 100644 index 0000000..8ddb4f1 --- /dev/null +++ b/src/hosted/browser-module.generated.ts @@ -0,0 +1,4 @@ +// Generated by scripts/build-hosted-browser-module.ts. +// Do not edit by hand. +export const WWW_BROWSER_MODULE = + '// src/index.ts\nimport { newWebSocketRpcSession as newWebSocketRpcSession2, RpcTarget } from "https://esm.sh/capnweb@0.8.0";\n\n// src/routing.ts\nvar HOSTED_CAPTUN_HOSTNAME = "captun.sh";\nvar HOSTED_CAPTUN_GATEWAY = "https://captun.sh";\nvar GATEWAY_CONNECT_QUERY_PARAM = "captun-connect";\nvar TUNNEL_NAME_QUERY_PARAM = "captun-name";\nvar CONNECT_TOKEN_QUERY_PARAM = "captun-token";\nvar TUNNEL_CONNECT_DIAGNOSTIC_HEADER = "x-captun-connect-diagnostic";\n\n// src/server-core.ts\nimport { newWebSocketRpcSession } from "https://esm.sh/capnweb@0.8.0";\nfunction fetcherStubFromRemoteCapability(remote, options) {\n remote.onRpcBroken(() => options.onDisconnect?.());\n return {\n fetch: (request) => remote.fetch(request),\n ready: (tunnel) => remote.ready(tunnel),\n [Symbol.dispose]: () => remote[Symbol.dispose]()\n };\n}\nfunction acceptFetcherCapabilityFromSocket(socket, options = {}) {\n const remote = newWebSocketRpcSession(socket);\n return fetcherStubFromRemoteCapability(remote, options);\n}\n\n// src/token.ts\nfunction randomConnectToken() {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");\n}\n\n// src/index.ts\nvar CaptunTunnelConnectError = class extends Error {\n response;\n constructor(message, response) {\n super(message);\n this.name = "CaptunTunnelConnectError";\n this.response = response;\n }\n};\nvar TUNNEL_READY_TIMEOUT_MS = 5e3;\nvar WEBSOCKET_REJECTION_PROBE_TIMEOUT_MS = 500;\nasync function createCaptunTunnel(options) {\n const connect = gatewayConnectRequest(options);\n const ready = Promise.withResolvers();\n const socket = createWebSocket(connect.url);\n const fetcher = new TunnelTargetFetcher({\n fetch: options.fetch,\n ready: (tunnel) => ready.resolve(tunnel)\n });\n const session = newWebSocketRpcSession2(socket, fetcher);\n try {\n await waitUntilOpen(socket, connect.url);\n const tunnel = await waitUntilReady(ready.promise);\n return {\n url: tunnel.url,\n token: tunnel.token || connect.token,\n [Symbol.dispose]: () => session[Symbol.dispose]()\n };\n } catch (error) {\n session[Symbol.dispose]();\n throw error;\n }\n}\nfunction gatewayConnectRequest(options) {\n const name = options.name || randomTunnelName();\n const url = new URL(options.gateway || HOSTED_CAPTUN_GATEWAY);\n const token = options.token || (isHostedCaptunGateway(url) ? randomConnectToken() : void 0);\n url.searchParams.set(GATEWAY_CONNECT_QUERY_PARAM, "1");\n url.searchParams.set(TUNNEL_NAME_QUERY_PARAM, name);\n if (token) url.searchParams.set(CONNECT_TOKEN_QUERY_PARAM, token);\n return { url: url.toString(), name, token };\n}\nfunction isHostedCaptunGateway(url) {\n return url.hostname === HOSTED_CAPTUN_HOSTNAME;\n}\nfunction randomTunnelName() {\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");\n}\nvar TunnelTargetFetcher = class extends RpcTarget {\n fetcher;\n onReady;\n constructor(options) {\n super();\n this.fetcher = { fetch: options.fetch };\n this.onReady = options.ready;\n }\n fetch(request) {\n return this.fetcher.fetch(request);\n }\n ready(tunnel) {\n this.onReady(tunnel);\n }\n};\nfunction createWebSocket(url) {\n const connectUrl = new URL(url);\n connectUrl.protocol = connectUrl.protocol === "https:" ? "wss:" : "ws:";\n return new WebSocket(connectUrl.href);\n}\nasync function waitUntilOpen(socket, connectUrl) {\n if (socket.readyState === WebSocket.OPEN) return;\n if (socket.readyState !== WebSocket.CONNECTING) {\n throw new Error("WebSocket closed before opening");\n }\n const listeners = new AbortController();\n await new Promise((resolve, reject) => {\n const settle = (callback) => {\n listeners.abort();\n callback();\n };\n socket.addEventListener("open", () => settle(resolve), { signal: listeners.signal });\n socket.addEventListener(\n "error",\n () => settle(() => {\n void webSocketConnectionFailedError(connectUrl).then(reject);\n }),\n { signal: listeners.signal }\n );\n socket.addEventListener(\n "close",\n (event) => {\n listeners.abort();\n void webSocketConnectionFailedError(connectUrl).then((error) => {\n reject(\n error.response ? error : new Error(`WebSocket closed before opening: ${event.code} ${event.reason}`)\n );\n });\n },\n { signal: listeners.signal }\n );\n });\n}\nasync function webSocketConnectionFailedError(connectUrl) {\n const response = await readWebSocketRejection(connectUrl);\n if (!response) return new CaptunTunnelConnectError("WebSocket connection failed", void 0);\n return new CaptunTunnelConnectError(\n `WebSocket connection failed: ${response.status} ${response.statusText}: ${response.body}`.trim(),\n response\n );\n}\nasync function readWebSocketRejection(connectUrl) {\n const abort = new AbortController();\n const timeout = setTimeout(() => abort.abort(), WEBSOCKET_REJECTION_PROBE_TIMEOUT_MS);\n try {\n const response = await fetch(connectUrl, {\n headers: { [TUNNEL_CONNECT_DIAGNOSTIC_HEADER]: "1" },\n signal: abort.signal\n });\n if (response.ok) return void 0;\n return {\n status: response.status,\n statusText: response.statusText || "Rejected",\n body: (await response.text()).trim()\n };\n } catch {\n return void 0;\n } finally {\n clearTimeout(timeout);\n }\n}\nasync function waitUntilReady(promise) {\n let timeout;\n try {\n return await Promise.race([\n promise,\n new Promise((_, reject) => {\n timeout = setTimeout(\n () => reject(new Error("Timed out waiting for tunnel gateway ready message")),\n TUNNEL_READY_TIMEOUT_MS\n );\n })\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\nfunction acceptFetcherCapability(options = {}) {\n const pair = new WebSocketPair();\n const clientSocket = pair[0];\n const serverSocket = pair[1];\n serverSocket.accept();\n const fetcher = acceptFetcherCapabilityFromSocket(serverSocket, options);\n return {\n fetcher,\n response: new Response(null, { status: 101, webSocket: clientSocket })\n };\n}\nexport {\n CaptunTunnelConnectError,\n acceptFetcherCapability,\n acceptFetcherCapabilityFromSocket,\n createCaptunTunnel\n};\n'; diff --git a/src/hosted/site.ts b/src/hosted/site.ts index d9e2df7..2c3bc52 100644 --- a/src/hosted/site.ts +++ b/src/hosted/site.ts @@ -1,7 +1,28 @@ -import { HOSTED_CAPTUN_HOSTNAME, RESERVED_TUNNEL_NAMES } from "../routing.js"; +import { HOSTED_CAPTUN_HOSTNAME, isLoopbackHostname, RESERVED_TUNNEL_NAMES } from "../routing.js"; +import { WWW_BROWSER_MODULE } from "./browser-module.generated.js"; + +declare const createCaptunTunnel: typeof import("../index.js").createCaptunTunnel; + +declare function runtimeImport(specifier: string): Promise; + +type BrowserMcpTransport = { + close: () => Promise; + handleRequest: (request: Request) => Promise; +}; + +declare global { + interface Window { + chatMessages?: string[]; + mcpTransports?: Map; + } +} export function hostedCaptunResponse(request: Request): Response | undefined { const url = new URL(request.url); + if (isLoopbackHostname(url.hostname) && isWwwCaptunPath(url.pathname)) { + return wwwCaptunResponse(url); + } + if (url.hostname === HOSTED_CAPTUN_HOSTNAME) { return Response.redirect( `https://www.${HOSTED_CAPTUN_HOSTNAME}${url.pathname}${url.search}`, @@ -22,6 +43,10 @@ export function hostedCaptunResponse(request: Request): Response | undefined { } } +function isWwwCaptunPath(pathname: string) { + return pathname === "/" || pathname === "/captun.browser.js" || pathname === "/favicon.svg"; +} + function wwwCaptunResponse(url: URL): Response { if (url.pathname === "/captun.browser.js") { return new Response(WWW_BROWSER_MODULE, { @@ -49,7 +74,321 @@ function wwwCaptunResponse(url: URL): Response { }); } -const WWW_LANDING_PAGE = ` +const FROM_CODE_SOURCE = js` +import { createCaptunTunnel } from "captun"; + +const tunnel = await createCaptunTunnel({ + fetch: (request) => { + const url = new URL(request.url); + return Response.json({ method: request.method, path: url.pathname }); + }, +}); + +console.log(tunnel.url); +`; + +const HELLO_WORLD_DEMO_SOURCE = functionBody(helloWorldDemoSource); +const CHAT_ROOM_DEMO_SOURCE = functionBody(chatRoomDemoSource); +const MCP_SERVER_DEMO_SOURCE = functionBody(mcpServerDemoSource); +const LANDING_PAGE_SCRIPT_SOURCE = functionBody(landingPageScriptSource); + +function functionBody(fn: () => unknown) { + const source = fn.toString(); + return stripCommonIndent(source.slice(source.indexOf("{") + 1, source.lastIndexOf("}"))) + .replaceAll("/* @__PURE__ */ ", "") + .replaceAll(/\n *\/\/ prettier-ignore\n/g, "\n") + .replaceAll("runtimeImport(", "import(") + .replaceAll("setTimeout(resolve, 3e3)", "setTimeout(resolve, 3000)") + .replaceAll(" || (await createMcpTransport())", " || await createMcpTransport()"); +} + +function helloWorldDemoSource() { + createCaptunTunnel({ + fetch: () => new Response("hello world from this browser tab\n"), + }); +} + +function chatRoomDemoSource() { + createCaptunTunnel({ + fetch: async (request) => { + // your "server" is this browser tab! + window.chatMessages ||= []; + if (request.method === "POST") { + window.chatMessages.push(await request.text()); + return Response.json({ ok: true }); + } + + const messages = window.chatMessages.join("\n").replace(/[^\w\s-,.'"!?():]/g, ""); + + const html = ` + +
${messages}
+
+ +
+ `; + return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }); + }, + }); +} + +async function mcpServerDemoSource() { + const { z } = await runtimeImport("https://esm.sh/zod@3.25.76"); + const { McpServer } = await runtimeImport< + typeof import("@modelcontextprotocol/sdk/server/mcp.js") + >("https://esm.sh/@modelcontextprotocol/sdk@1.29.0/server/mcp.js?deps=zod@3.25.76"); + const { WebStandardStreamableHTTPServerTransport } = await runtimeImport< + typeof import("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js") + >( + "https://esm.sh/@modelcontextprotocol/sdk@1.29.0/server/webStandardStreamableHttp.js?deps=zod@3.25.76", + ); + + for (const transport of window.mcpTransports?.values() || []) { + await transport.close?.(); + } + + window.mcpTransports = new Map(); + + createCaptunTunnel({ + fetch: async (request) => { + if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 })); + + const sessionId = request.headers.get("mcp-session-id"); + const transport = window.mcpTransports!.get(sessionId) || (await createMcpTransport()); + const response = await transport.handleRequest(request); + const responseSessionId = response.headers.get("mcp-session-id"); + if (responseSessionId) window.mcpTransports!.set(responseSessionId, transport); + return withCors(response); + }, + }); + + async function createMcpTransport() { + const mcpServer = new McpServer({ + name: "browser-tab-mcp", + version: "1.0.0", + }); + + mcpServer.registerTool( + "ask_question", + { + description: "Ask a question in the browser tab that owns this MCP server.", + inputSchema: { + question: z.string().describe("Question to ask in the browser prompt."), + }, + }, + async ({ question }: any) => { + await new Promise((resolve) => setTimeout(resolve, 3000)); + const answer = window.prompt(question) || ""; + return { + content: [{ text: answer, type: "text" }], + structuredContent: { answer }, + }; + }, + ); + + const transport = new WebStandardStreamableHTTPServerTransport({ + enableJsonResponse: true, + onsessionclosed: (sessionId) => { + window.mcpTransports?.delete(sessionId); + }, + sessionIdGenerator: () => crypto.randomUUID(), + }); + await mcpServer.connect(transport); + return transport; + } + function withCors(response: Response) { + response.headers.set("Access-Control-Allow-Origin", "*"); + response.headers.set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + // prettier-ignore + response.headers.set("Access-Control-Allow-Headers", "accept, authorization, content-type, mcp-protocol-version, mcp-session-id"); + response.headers.set("Access-Control-Expose-Headers", "mcp-session-id"); + return response; + } +} + +function landingPageScriptSource() { + const fromCodeSource = document.querySelector("#from-code-source")!; + const fromCodeEditorHost = document.querySelector("#from-code-editor")!; + const description = document.querySelector("#demo-description")!; + const source = document.querySelector("#demo-source")!; + const editorHost = document.querySelector("#demo-editor")!; + const snippetButtons = Array.from( + document.querySelectorAll("[data-demo-snippet]"), + ); + const button = document.querySelector("#demo-create")!; + const reload = document.querySelector("#demo-reload")!; + const status = document.querySelector("#demo-status")!; + const urlRow = document.querySelector("#demo-url")!; + const link = document.querySelector("#demo-link")!; + const frame = document.querySelector("#demo-frame")!; + const error = document.querySelector("#demo-error")!; + let tunnel: Awaited>; + let activeFetch: Fetcher["fetch"]; + let editor: any; + void enhanceEditor(); + const captunBrowser = runtimeImport("/captun.browser.js"); + const snippets: Record = { + hello: { + description: "Return a tiny text response from this browser tab.", + source: source.value, + }, + chat: { + description: "Run the chat room currently on captun.sh from this browser tab.", + source: document.querySelector("#demo-chat-source")!.value, + }, + mcp: { + description: "Run an MCP server from this browser tab. Use the shown URL in MCP Inspector.", + source: document.querySelector("#demo-mcp-source")!.value, + }, + }; + let activeSnippet = "hello"; + + function currentSource() { + return editor ? editor.state.doc.toString() : source.value; + } + + async function evaluateDemo() { + let capturedFetch; + const createCaptunTunnel = (options: { fetch: (request: Request) => Promise }) => { + capturedFetch = options.fetch; + return Promise.resolve({ url: tunnel ? tunnel.url : "https://pending.captun.sh" }); + }; + await new Function( + "createCaptunTunnel", + "return (async () => {\n" + currentSource() + "\n})()", + )(createCaptunTunnel); + if (typeof capturedFetch !== "function") { + throw new Error("Call createCaptunTunnel({ fetch }) in the editor."); + } + activeFetch = capturedFetch; + } + + function switchSnippet(name: string) { + const snippet = snippets[name]; + if (!snippet) return; + activeSnippet = name; + description.innerText = snippet.description; + for (const snippetButton of snippetButtons) { + snippetButton.setAttribute( + "aria-pressed", + String(snippetButton.dataset.demoSnippet === name), + ); + } + setSource(snippet.source); + } + + function setSource(nextSource: string) { + if (!editor) { + source.value = nextSource; + return; + } + editor.dispatch({ + changes: { from: 0, to: editor.state.doc.length, insert: nextSource }, + }); + } + + function showTunnelTarget(tunnelUrl: string) { + link.href = tunnelUrl; + link.textContent = tunnelUrl; + if (activeSnippet === "mcp") { + frame.removeAttribute("src"); + frame.srcdoc = previewHtml(tunnelUrl); + return; + } + + frame.removeAttribute("srcdoc"); + frame.src = tunnelUrl; + } + + for (const snippetButton of snippetButtons) { + snippetButton.addEventListener("click", () => + switchSnippet(snippetButton.dataset.demoSnippet!), + ); + } + reload.addEventListener("click", () => { + if (tunnel) showTunnelTarget(tunnel.url); + }); + + button.addEventListener("click", async () => { + const startedAt = performance.now(); + button.disabled = true; + reload.disabled = true; + status.textContent = "connecting"; + error.textContent = ""; + + try { + if (tunnel) tunnel[Symbol.dispose](); + await closeMcpTransports(); + await evaluateDemo(); + const { createCaptunTunnel } = await captunBrowser; + const options = { fetch: (request: Request) => activeFetch(request) }; + tunnel = await createCaptunTunnel(options); + urlRow.classList.remove("hidden"); + showTunnelTarget(tunnel.url); + status.textContent = "connected in " + Math.round(performance.now() - startedAt) + "ms"; + reload.disabled = false; + } catch (caught: any) { + status.textContent = "failed"; + error.textContent = caught && caught.stack ? caught.stack : String(caught); + } finally { + button.disabled = false; + } + }); + + async function closeMcpTransports() { + for (const transport of window.mcpTransports?.values() || []) { + await transport.close(); + } + window.mcpTransports = new Map(); + } + + function previewHtml(url: string) { + return ( + "
MCP server listening at\n" +
+      url +
+      "\n\nUse ask_question to prompt this browser tab and return the answer.
" + ); + } + + async function enhanceEditor() { + try { + const [{ EditorView, basicSetup }, { javascript }] = await Promise.all([ + runtimeImport("https://esm.sh/codemirror@6.0.1"), + runtimeImport( + "https://esm.sh/@codemirror/lang-javascript@6.2.4", + ), + ]); + new EditorView({ + doc: fromCodeSource.value, + extensions: [basicSetup, javascript(), EditorView.editable.of(false)], + parent: fromCodeEditorHost, + }); + editor = new EditorView({ + doc: source.value, + extensions: [basicSetup, javascript()], + parent: editorHost, + }); + fromCodeSource.classList.add("enhanced"); + fromCodeEditorHost.classList.add("enhanced"); + source.classList.add("enhanced"); + editorHost.classList.add("enhanced"); + } catch (caught) { + console.warn("CodeMirror failed to load; using textarea editor.", caught); + } + } +} + +const WWW_LANDING_PAGE = landingPageHtml(); + +function landingPageHtml() { + return ` @@ -72,6 +411,11 @@ const WWW_LANDING_PAGE = ` #from-code-editor .cm-editor, #demo-editor .cm-editor { background: #f4f4f4; font: 16px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } #from-code-editor .cm-scroller { min-height: 210px; } #demo-editor .cm-scroller { min-height: 300px; } + .snippet-tabs { display: flex; align-items: stretch; margin: 12px 0 0; border: 1px solid #ddd; border-bottom: 0; background: #eee; } + .snippet-tab { margin: 0; padding: 7px 10px; color: #111; background: #fff; border: 0; border-right: 1px solid #ddd; } + .snippet-tab[aria-pressed="true"] { color: #fff; background: #111; } + .snippet-tabs + #demo-source { border-top: 0; } + .snippet-tabs + #demo-source.enhanced + #demo-editor.enhanced { border-top: 0; } button { margin: 12px 0; padding: 9px 12px; font: inherit; color: #fff; background: #111; border: 1px solid #111; cursor: pointer; } button:disabled { opacity: 0.55; cursor: wait; } .status-group { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; white-space: nowrap; } @@ -95,47 +439,21 @@ const WWW_LANDING_PAGE = `

From code

You don't need to run a local server. Just a fetch function:

- +

Try it in this tab

This works in any environment supported by capnweb, so you can run a "server" basically anywhere, even the browser.

-

Edit the fetch function, create a tunnel, then the iframe below will load the public URL.

- +

Return a tiny text response from this browser tab.

+
+ + + +
+
+ +
@@ -151,211 +469,42 @@ console.log(tunnel.url);
npx captun deploy

Source: github.com/iterate/captun

- + `; - -const WWW_FAVICON = ` - - - -`; - -const WWW_BROWSER_MODULE = `import { newWebSocketRpcSession, RpcTarget } from "https://esm.sh/capnweb@0.8.0"; - -export async function createCaptunTunnel(options) { - const connect = gatewayConnectUrl(options); - const socket = new WebSocket(connect.url); - const readyPromise = waitForReady(); - const tunnelTargetFetcher = new TunnelTargetFetcher(options.fetch, readyPromise.ready); - const session = newWebSocketRpcSession(socket, tunnelTargetFetcher); - await waitUntilOpen(socket); - const tunnel = await readyPromise.promise; - return { - url: tunnel.url, - token: tunnel.token || connect.token, - close: () => disposeSession(session), - }; -} - -class TunnelTargetFetcher extends RpcTarget { - constructor(fetcher, ready) { - super(); - this.fetcher = fetcher; - this.readyCallback = ready; - } - - fetch(request) { - return this.fetcher(request); - } - - ready(tunnel) { - return this.readyCallback(tunnel); - } } -function gatewayConnectUrl(options) { - const url = new URL(options.gateway || "https://captun.sh"); - const token = options.token || (url.hostname === "captun.sh" ? randomConnectToken() : undefined); - url.protocol = url.protocol === "http:" ? "ws:" : "wss:"; - url.searchParams.set("captun-connect", "1"); - url.searchParams.set("captun-name", options.name || randomTunnelName()); - if (token) url.searchParams.set("captun-token", token); - return { url, token }; +function htmlText(value: string) { + return value.replace(/&/g, "&").replace(//g, ">"); } -function waitUntilOpen(socket) { - if (socket.readyState === WebSocket.OPEN) return Promise.resolve(); - if (socket.readyState !== WebSocket.CONNECTING) { - return Promise.reject(new Error("WebSocket closed before opening")); - } - - return new Promise((resolve, reject) => { - const listeners = new AbortController(); - const settle = (callback) => { - listeners.abort(); - callback(); - }; - socket.addEventListener("open", () => settle(resolve), { signal: listeners.signal }); - socket.addEventListener("error", () => settle(() => reject(new Error("WebSocket connection failed"))), { signal: listeners.signal }); - socket.addEventListener("close", (event) => settle(() => reject(new Error("WebSocket closed before opening: " + event.code + " " + event.reason))), { signal: listeners.signal }); - }); +function htmlScript(value: string) { + return value.replace(/<\/script/gi, "<\\/script"); } -function waitForReady() { - let timer; - let resolveReady; - let rejectReady; - const promise = new Promise((resolve, reject) => { - resolveReady = resolve; - rejectReady = reject; - timer = setTimeout(() => reject(new Error("Timed out waiting for tunnel gateway")), 5000); - }); - return { - promise, - ready: (tunnel) => { - clearTimeout(timer); - resolveReady(tunnel); - }, - reject: rejectReady, - }; -} +function stripCommonIndent(source: string) { + const lines = source + .replace(/^\n/, "") + .replace(/\n\s*$/, "") + .split("\n"); + const indents = lines + .filter((line) => line.trim()) + .map((line) => { + const match = line.match(/^[ \t]*/); + return match ? match[0].length : 0; + }); + if (!indents.length) return ""; -function randomTunnelName() { - const bytes = new Uint8Array(8); - crypto.getRandomValues(bytes); - return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); + const indent = Math.min(...indents); + return lines.map((line) => (line.trim() ? line.slice(indent) : "")).join("\n"); } -function randomConnectToken() { - const bytes = new Uint8Array(16); - crypto.getRandomValues(bytes); - return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); +function js(strings: TemplateStringsArray, ...values: string[]) { + return stripCommonIndent(String.raw(strings, ...values)).trim(); } -function disposeSession(session) { - const disposeSymbol = Symbol.dispose; - if (disposeSymbol && typeof session[disposeSymbol] === "function") session[disposeSymbol](); -} -`; +const WWW_FAVICON = ` + + + +`; diff --git a/src/hosted/worker.ts b/src/hosted/worker.ts index e32c3ea..5a44108 100644 --- a/src/hosted/worker.ts +++ b/src/hosted/worker.ts @@ -12,6 +12,7 @@ import { getTunnelNameFromUrl, getTunnelUrl, HOSTED_CAPTUN_HOSTNAME, + isLoopbackHostname, isValidTunnelName, RESERVED_TUNNEL_NAMES, TUNNEL_CONNECT_DIAGNOSTIC_HEADER, @@ -72,8 +73,9 @@ export default { const hostedResponse = hostedCaptunResponse(request); if (hostedResponse) return hostedResponse; + const customHostname = customHostnameForRequest(request, env); const tunnelName = getTunnelNameFromUrl({ - customHostname: env.CUSTOM_HOSTNAME, + customHostname, url: request.url, }); if (!tunnelName) return new Response("Missing tunnel name\n", { status: 404 }); @@ -91,10 +93,10 @@ export default { if (rateLimited) return rateLimited; const shard = captunServerShard(env, tunnelName); - const forwarded = new Request(request.url, request); + const forwarded = createForwardedRequest(request, customHostname); const tunnelUrl = getTunnelUrl({ reqUrl: request.url, - customHostname: env.CUSTOM_HOSTNAME, + customHostname, tunnelName, }); const response = await shard.forward( @@ -117,10 +119,11 @@ async function connectTunnel(request: Request, env: HostedCaptunEnv) { return new Response("Missing tunnel name\n", { status: 404 }); } + const customHostname = customHostnameForRequest(request, env); const shard = captunServerShard(env, tunnelName); const tunnelUrl = getTunnelUrl({ reqUrl: request.url, - customHostname: env.CUSTOM_HOSTNAME, + customHostname, tunnelName, }); const connectRequest = createTunnelConnectRequest({ request, tunnelName, tunnelUrl }); @@ -160,6 +163,20 @@ function connectToken(request: Request) { return new URL(request.url).searchParams.get(CONNECT_TOKEN_QUERY_PARAM); } +function customHostnameForRequest(request: Request, env: HostedCaptunEnv) { + const url = new URL(request.url); + if (isLoopbackHostname(url.hostname)) return undefined; + return env.CUSTOM_HOSTNAME; +} + +function createForwardedRequest(request: Request, customHostname: string | undefined) { + const url = new URL(request.url); + if (!customHostname) { + url.pathname = url.pathname.match(/^\/[^/]+(\/.*)?$/)?.[1] || "/"; + } + return new Request(url, request); +} + function stripSetCookieHeadersOutsideTunnel(response: Response, tunnelHostname: string) { const setCookies = setCookieHeaders(response.headers); if (setCookies.length === 0) return response; diff --git a/src/routing.ts b/src/routing.ts index f03a8ab..197b00c 100644 --- a/src/routing.ts +++ b/src/routing.ts @@ -117,6 +117,15 @@ export function isValidTunnelName(name: string): boolean { return true; } +export function isLoopbackHostname(hostname: string): boolean { + return ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "0.0.0.0" || + hostname === "[::1]" + ); +} + /** Maps a tunnel name to a stable Durable Object shard name. */ export function captunShardName(tunnelName: string, shardCount: number) { if (!Number.isFinite(shardCount) || shardCount <= 1) return "tunnel-shard-0"; diff --git a/test/hosted-worker.test.ts b/test/hosted-worker.test.ts index e13ddcc..93733b4 100644 --- a/test/hosted-worker.test.ts +++ b/test/hosted-worker.test.ts @@ -83,6 +83,10 @@ test("Hosted Captun serves the browser demo module on www", async () => { expect(module).toEqual(expect.stringContaining("captun-connect")); expect(module).toEqual(expect.stringContaining("captun-token")); expect(module).toEqual(expect.stringContaining("randomConnectToken")); + expect(module).toEqual( + expect.stringContaining("[Symbol.dispose]: () => session[Symbol.dispose]()"), + ); + expect(module).not.toContain("close: () => disposeSession(session)"); expect(module).not.toEqual(expect.stringContaining("__captun-connect")); }); @@ -100,27 +104,116 @@ test("Hosted Captun landing page includes an in-browser tunnel demo", async () = html.indexOf( 'This works in any environment supported by capnweb', ), - ).toBeLessThan( - html.indexOf("Edit the fetch function, create a tunnel, then the iframe below will load"), - ); + ).toBeLessThan(html.indexOf('id="demo-description"')); expect(html).toEqual( expect.stringContaining( 'This works in any environment supported by capnweb', ), ); - expect(html).toEqual(expect.stringContaining('// your "server" is this browser tab!')); - expect(html).toEqual(expect.stringContaining("window.chatMessages")); - expect(textareaValue(html, "demo-source")).toEqual( + expect(html).toEqual( + expect.stringContaining( + '

Return a tiny text response from this browser tab.

', + ), + ); + expect(html).toEqual(expect.stringContaining('class="snippet-tabs"')); + expect(html).toEqual( + expect.stringContaining( + '', + ), + ); + expect(html).toEqual( + expect.stringContaining( + '', + ), + ); + expect(html).toEqual( + expect.stringContaining( + '', + ), + ); + const demoSource = textareaValue(html, "demo-source"); + expect(demoSource).toEqual( + `createCaptunTunnel({ + fetch: () => new Response("hello world from this browser tab\\n") +});`, + ); + expect(demoSource).not.toContain("@modelcontextprotocol"); + + const chatSource = textareaValue(html, "demo-chat-source"); + expect(chatSource).toEqual(expect.stringContaining("window.chatMessages ||= [];")); + expect(chatSource).toEqual(expect.stringContaining('if (request.method === "POST")')); + expect(chatSource).toEqual(expect.stringContaining("function send(form)")); + expect(chatSource).toEqual(expect.stringContaining("")); + + const mcpSource = textareaValue(html, "demo-mcp-source"); + expect(mcpSource.indexOf('import("https://esm.sh/zod@3.25.76")')).toBeLessThan( + mcpSource.indexOf( + 'import("https://esm.sh/@modelcontextprotocol/sdk@1.29.0/server/mcp.js?deps=zod@3.25.76")', + ), + ); + expect(mcpSource).toEqual( + expect.stringContaining( + 'import("https://esm.sh/@modelcontextprotocol/sdk@1.29.0/server/mcp.js?deps=zod@3.25.76")', + ), + ); + expect(mcpSource).toEqual( + expect.stringContaining( + 'import(\n "https://esm.sh/@modelcontextprotocol/sdk@1.29.0/server/webStandardStreamableHttp.js?deps=zod@3.25.76"', + ), + ); + expect(mcpSource).toEqual(expect.stringContaining('import("https://esm.sh/zod@3.25.76")')); + expect(mcpSource).toEqual(expect.stringContaining("new McpServer")); + expect(mcpSource).toEqual(expect.stringContaining("WebStandardStreamableHTTPServerTransport")); + expect(mcpSource).toEqual(expect.stringContaining('"ask_question"')); + expect(mcpSource).toEqual( + expect.stringContaining("await new Promise((resolve) => setTimeout(resolve, 3000));"), + ); + expect(mcpSource).toEqual( + expect.stringContaining('const answer = window.prompt(question) || "";'), + ); + expect(mcpSource).toEqual(expect.stringContaining("window.mcpTransports = new Map();")); + expect(mcpSource).toEqual( + expect.stringContaining( + "const transport = window.mcpTransports.get(sessionId) || await createMcpTransport();", + ), + ); + expect(mcpSource).toEqual( + expect.stringContaining("if (responseSessionId) window.mcpTransports.set"), + ); + expect(mcpSource).not.toContain('"alert"'); + expect(mcpSource).not.toContain("alert("); + expect(mcpSource).not.toContain("my_funky_search"); + expect(mcpSource).toEqual(expect.stringContaining("transport.handleRequest(request)")); + expect(mcpSource).toEqual(expect.stringContaining("onsessionclosed: (sessionId) => {")); + expect(mcpSource).toEqual(expect.stringContaining("window.mcpTransports?.delete(sessionId);")); + expect(mcpSource).toEqual( + expect.stringContaining( + 'if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }));', + ), + ); + expect(mcpSource).toEqual(expect.stringContaining("return withCors(response);")); + expect(mcpSource).toEqual(expect.stringContaining("function withCors(response)")); + expect(mcpSource).not.toContain('request.method === "DELETE"'); + expect(mcpSource).toEqual( + expect.stringContaining('response.headers.set("Access-Control-Allow-Origin", "*")'), + ); + expect(mcpSource).toEqual( expect.stringContaining( - "window.chatMessages.join(\"\\n\").replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/'/g, ''').replace(/`/g, '`')", + 'response.headers.set("Access-Control-Allow-Headers", "accept, authorization, content-type, mcp-protocol-version, mcp-session-id")', ), ); - expect(html).toEqual(expect.stringContaining("document.cookie")); - expect(html).toEqual(expect.stringContaining("username ||= ")); - expect(html).toEqual(expect.stringContaining("function send(form)")); - expect(html).toEqual(expect.stringContaining('onsubmit="send(this); return false"')); - expect(html).toEqual(expect.stringContaining("")); - expect(html).toEqual(expect.stringContaining("Response.json({ ok: true })")); + expect(mcpSource).toEqual( + expect.stringContaining( + 'response.headers.set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")', + ), + ); + expect(mcpSource).toEqual( + expect.stringContaining( + 'response.headers.set("Access-Control-Expose-Headers", "mcp-session-id")', + ), + ); + expect(html).not.toContain("function browserDemoFetch(fetcher)"); + expect(html).toEqual(expect.stringContaining("capturedFetch = options.fetch;")); expect(html).toEqual(expect.stringContaining('