{"_id":"@claxedo/workspace-relay","_rev":"7-d6cc096a65b8805cce1caa84144a6b20","name":"@claxedo/workspace-relay","dist-tags":{"latest":"0.8.0"},"versions":{"0.1.0":{"name":"@claxedo/workspace-relay","version":"0.1.0","_id":"@claxedo/workspace-relay@0.1.0","maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"bin":{"workspace-relay":"dist/main.mjs"},"dist":{"shasum":"6591e9da6d4f3763d78fcd37bb34c0b341a0522b","tarball":"https://registry.npmjs.org/@claxedo/workspace-relay/-/workspace-relay-0.1.0.tgz","fileCount":12,"integrity":"sha512-pcsXCHjqqIq15rk72RiTQkztgcTN9VBCGCX2wcV/oalLPV1tsDrKkOeipZ2VjSJxj9cyj2A2CUnl4dtTrXslbQ==","signatures":[{"sig":"MEUCIQCGoNtr+bYsJmVGwAyphBlQNTnSPnfnQ35DEAjGr34trgIgHAfFL0g3sagkwhnChuwe35lvKKAhLL8AHj4sQTXWiQE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":157522},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.ts","gitHead":"25db7e7a68de7a628bc53847b2db00dced45a5d5","scripts":{"dev":"bun run --hot src/main.ts","test":"bun test src","build":"tsx scripts/build.ts","start":"bun run src/main.ts","typecheck":"tsc --noEmit -p tsconfig.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"kyashrathore","email":"kanusdlp@gmail.com"},"_npmVersion":"11.12.1","description":"The Claxedo workspace relay — a single canonical bidirectional tunnel between cloud-hosted browsers and workspace-runtime hosts (whether the host is a cloud VM or a user laptop). One package, one process, one config surface — there is no separate `dev-rel","directories":{},"_nodeVersion":"25.9.0","dependencies":{"hono":"4.10.7","jose":"6.0.11","@claxedo/workspace-relay-protocol":"^0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","esbuild":"^0.25.0","@types/bun":"catalog:","typescript":"catalog:","@tsconfig/node-lts":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/workspace-relay_0.1.0_1779196791000_0.8404182546729388","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@claxedo/workspace-relay","version":"0.5.0","_id":"@claxedo/workspace-relay@0.5.0","maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"bin":{"workspace-relay":"dist/main.mjs"},"dist":{"shasum":"3c3d19f738c2de39fb66b0500594e30fc1d2bb3a","tarball":"https://registry.npmjs.org/@claxedo/workspace-relay/-/workspace-relay-0.5.0.tgz","fileCount":14,"integrity":"sha512-4RNWeqJGA54c0ylDlQynJDN9gTi4eBCSe792l6JTfgBXI6uoyKTfSpkFCcZlNIYQXQIqV4ADZz573+IjO+rJFw==","signatures":[{"sig":"MEYCIQDJlC0lt8NsMxymZ91bMRDNYNRD3m1j1hnh8pi6f7TnDAIhAPiPku64EXPwrQfTnc/A6t3Vy50W2NfPXEoaczBcNDqq","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":283078},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"bun":"./dist/index.mjs","types":"./dist/index.d.ts","import":"./dist/index.mjs","default":"./dist/index.mjs","development":"./dist/index.mjs"}},"gitHead":"76982dadead99e05f3b952bf3475da3c5dd27ef6","scripts":{"dev":"bun run --hot src/main.ts","test":"bun test src","build":"tsx scripts/build.ts","start":"bun run src/main.ts","typecheck":"tsc --noEmit -p tsconfig.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"kyashrathore","email":"kanusdlp@gmail.com"},"_npmVersion":"10.9.8","description":"The Claxedo workspace relay — the single canonical tunnel process between cloud-hosted browsers and workspace-runtime hosts, whether the host is a cloud VM or a user laptop. One package, one process, one config surface — there is no separate `dev-relay` c","directories":{},"_nodeVersion":"22.22.3","dependencies":{"hono":"4.10.7","jose":"6.0.11","@claxedo/workspace-relay-protocol":"0.5.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.22.3","esbuild":"0.25.12","@types/bun":"1.3.13","typescript":"5.8.2","@tsconfig/node-lts":"22.0.4"},"_npmOperationalInternal":{"tmp":"tmp/workspace-relay_0.5.0_1782924822202_0.39924623863636666","host":"s3://npm-registry-packages-npm-production"}},"0.5.1":{"name":"@claxedo/workspace-relay","version":"0.5.1","_id":"@claxedo/workspace-relay@0.5.1","maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"bin":{"workspace-relay":"dist/main.mjs"},"dist":{"shasum":"944354588e4d2fc61fdd49132fc22d9b6a26937f","tarball":"https://registry.npmjs.org/@claxedo/workspace-relay/-/workspace-relay-0.5.1.tgz","fileCount":14,"integrity":"sha512-a3U5M3GdjOyYE4c4GFooYvFld2mqk2JkUmJbz/sHiXJq7MsmyzOn+00uEaYcpCE5Vzxchp/fYdK/s9sF3jsQ1Q==","signatures":[{"sig":"MEYCIQDjAMhol2okspUDOziCPoqPEf7Xbo/tn4wxsc0M8XIjcQIhAJz053VhCt+MLnxDzGhRS9j9AHztDILFZAXRdNHXZ+CF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":283078},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"bun":"./dist/index.mjs","types":"./dist/index.d.ts","import":"./dist/index.mjs","default":"./dist/index.mjs","development":"./dist/index.mjs"}},"gitHead":"76982dadead99e05f3b952bf3475da3c5dd27ef6","scripts":{"dev":"bun run --hot src/main.ts","test":"bun test src","build":"tsx scripts/build.ts","start":"bun run src/main.ts","typecheck":"tsc --noEmit -p tsconfig.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"kyashrathore","email":"kanusdlp@gmail.com"},"_npmVersion":"10.9.8","description":"The Claxedo workspace relay — the single canonical tunnel process between cloud-hosted browsers and workspace-runtime hosts, whether the host is a cloud VM or a user laptop. One package, one process, one config surface — there is no separate `dev-relay` c","directories":{},"_nodeVersion":"22.22.3","dependencies":{"hono":"4.10.7","jose":"6.0.11","@claxedo/workspace-relay-protocol":"0.5.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.22.3","esbuild":"0.25.12","@types/bun":"1.3.13","typescript":"5.8.2","@tsconfig/node-lts":"22.0.4"},"_npmOperationalInternal":{"tmp":"tmp/workspace-relay_0.5.1_1782925031665_0.597659533878478","host":"s3://npm-registry-packages-npm-production"}},"0.5.2":{"name":"@claxedo/workspace-relay","version":"0.5.2","keywords":["claxedo","relay","tunnel","workspaces"],"license":"MIT","_id":"@claxedo/workspace-relay@0.5.2","maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"homepage":"https://github.com/kyashrathore/Claxedo#readme","bugs":{"url":"https://github.com/kyashrathore/Claxedo/issues"},"bin":{"workspace-relay":"dist/main.mjs"},"dist":{"shasum":"8b1e90021605240424b296bfeeba85163a1f41ba","tarball":"https://registry.npmjs.org/@claxedo/workspace-relay/-/workspace-relay-0.5.2.tgz","fileCount":17,"integrity":"sha512-iU6dQpYMzC0ciJxf4HsQjKcy2Z41uGREUKL3NPgf8hyEEd/ayps5zQX1izBf/q+NjV2pJ4JbfGrv3AL/Bwkg4Q==","signatures":[{"sig":"MEQCIDl3d1hcL5Tk1gDxVHc4RKjWS+JTntt0aHon+w/PVO9iAiBhgwsY/Tci2owYH6v7tQRbD7Hf6YhJxh9tIBccsHWpqg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1874578},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"bun":"./dist/index.mjs","types":"./dist/index.d.ts","import":"./dist/index.mjs","default":"./dist/index.mjs","development":"./dist/index.mjs"}},"gitHead":"689fe2da7c48b9a1eb894146840413577e25e3e1","scripts":{"dev":"bun run --hot src/main.ts","test":"bun test src","build":"tsx scripts/build.ts","start":"bun run src/main.ts","typecheck":"tsc --noEmit -p tsconfig.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"kyashrathore","email":"kanusdlp@gmail.com"},"repository":{"url":"git+https://github.com/kyashrathore/Claxedo.git","type":"git","directory":"packages/workspace-relay"},"_npmVersion":"10.9.8","description":"Relay/tunnel server that routes authenticated traffic to workspace runtimes: JWT workspace tokens, header-stripping trust boundary, Bun and Cloudflare adapters","directories":{},"_nodeVersion":"22.22.3","dependencies":{"hono":"4.10.7","jose":"6.0.11","@sentry/bun":"10.64.0","@claxedo/workspace-relay-protocol":"0.5.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.22.3","esbuild":"0.25.12","@types/bun":"1.3.13","typescript":"catalog:","@daytona/sdk":"0.192.0","@tsconfig/node-lts":"22.0.4"},"_npmOperationalInternal":{"tmp":"tmp/workspace-relay_0.5.2_1784389260693_0.5208899546550159","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@claxedo/workspace-relay","version":"0.6.0","keywords":["claxedo","relay","tunnel","workspaces"],"license":"MIT","_id":"@claxedo/workspace-relay@0.6.0","maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"homepage":"https://github.com/kyashrathore/Claxedo#readme","bugs":{"url":"https://github.com/kyashrathore/Claxedo/issues"},"bin":{"workspace-relay":"dist/main.mjs"},"dist":{"shasum":"1d33d6267e3739d57f777075c56e25df38879e20","tarball":"https://registry.npmjs.org/@claxedo/workspace-relay/-/workspace-relay-0.6.0.tgz","fileCount":17,"integrity":"sha512-BRDsgiwewWlsyqeiTui4inD9RsAPLeLQCRL35rdfER8s8f+hvkN4CW+frrz8yrwj8ophWnJd4Wld4J0DG4YAaw==","signatures":[{"sig":"MEYCIQC589F7JEXxuLt1dd+yFgA8rYIjaH8qJUm2qqfP2Z8rdwIhAPBzkH14p5HnyM/e5nP7EXSr9oQIXE3hVAHoqDa2zxab","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1874578},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"bun":"./dist/index.mjs","types":"./dist/index.d.ts","import":"./dist/index.mjs","default":"./dist/index.mjs","development":"./dist/index.mjs"}},"gitHead":"3437f51de5ae445f0e88add1b95f9428ddcce4c6","scripts":{"dev":"bun run --hot src/main.ts","test":"bun test src","build":"tsx scripts/build.ts","start":"bun run src/main.ts","typecheck":"tsc --noEmit -p tsconfig.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"kyashrathore","email":"kanusdlp@gmail.com"},"repository":{"url":"git+https://github.com/kyashrathore/Claxedo.git","type":"git","directory":"packages/workspace-relay"},"_npmVersion":"10.9.8","description":"Relay/tunnel server that routes authenticated traffic to workspace runtimes: JWT workspace tokens, header-stripping trust boundary, Bun and Cloudflare adapters","directories":{},"_nodeVersion":"22.22.3","dependencies":{"hono":"4.10.7","jose":"6.0.11","@sentry/bun":"10.64.0","@claxedo/workspace-relay-protocol":"0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.22.3","esbuild":"0.25.12","@types/bun":"1.3.13","typescript":"catalog:","@daytona/sdk":"0.192.0","@tsconfig/node-lts":"22.0.4"},"_npmOperationalInternal":{"tmp":"tmp/workspace-relay_0.6.0_1784556792428_0.4058231448095353","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@claxedo/workspace-relay","version":"0.7.0","keywords":["claxedo","relay","tunnel","workspaces"],"license":"MIT","_id":"@claxedo/workspace-relay@0.7.0","maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"homepage":"https://github.com/kyashrathore/Claxedo#readme","bugs":{"url":"https://github.com/kyashrathore/Claxedo/issues"},"bin":{"workspace-relay":"dist/main.mjs"},"dist":{"shasum":"5160209cb52d73516e351fbc1b786e7fd8e2e373","tarball":"https://registry.npmjs.org/@claxedo/workspace-relay/-/workspace-relay-0.7.0.tgz","fileCount":18,"integrity":"sha512-v+6hgCTcsy58JP+XVCA3rGnMmqldjiuRppFVcUuSqlBG5ScqqAehRQn4OPQVp36SkEorajSTX2TvN7GQsyRedA==","signatures":[{"sig":"MEUCICdtC0gKnCq87zIocFZzZSXyjR9QvjVC+lpWPuenet5ZAiEAxc8tWahbvhX4pLZ75vmgWe3cLlMiz/TR2p6iu1tTRzY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@claxedo%2fworkspace-relay@0.7.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":958417},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.ts","exports":{".":{"bun":"./dist/index.mjs","types":"./dist/index.d.ts","import":"./dist/index.mjs","default":"./dist/index.mjs","development":"./dist/index.mjs"}},"gitHead":"eff8f2bf1d4c99bd1afdc0f269ff4f5d427fc872","scripts":{"dev":"bun run --hot src/main.ts","test":"mkdir -p .artifacts/unit && bun test src bench --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml","build":"tsx scripts/build.ts","start":"bun run src/main.ts","typecheck":"tsc --noEmit -p tsconfig.json","bench:gate":"bun bench/local-dry-run.ts","bench:gate:cf":"bun bench/cf-dev-smoke.ts","prepublishOnly":"npm run build","typecheck:bench":"tsc --noEmit -p bench/tsconfig.json"},"_npmUser":{"name":"kyashrathore","email":"kanusdlp@gmail.com"},"repository":{"url":"git+https://github.com/kyashrathore/Claxedo.git","type":"git","directory":"packages/workspace-relay"},"_npmVersion":"10.9.8","description":"Relay/tunnel server that routes authenticated traffic to workspace runtimes: JWT workspace tokens, header-stripping trust boundary, Bun and Cloudflare adapters","directories":{},"_nodeVersion":"22.23.1","dependencies":{"hono":"4.10.7","jose":"6.0.11","posthog-node":"4.18.0","@claxedo/workspace-relay-protocol":"0.7.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.22.3","esbuild":"0.25.12","wrangler":"4.114.0","miniflare":"4.20260722.0","@types/bun":"1.3.13","typescript":"catalog:","@daytona/sdk":"0.192.0","@tsconfig/node-lts":"22.0.4","@claxedo/sandbox-manager":"0.7.0","@cloudflare/workers-types":"catalog:"},"_npmOperationalInternal":{"tmp":"tmp/workspace-relay_0.7.0_1785430369296_0.11062500075448978","host":"s3://npm-registry-packages-npm-production"}},"0.8.0":{"type":"module","description":"Relay/tunnel server that routes authenticated traffic to workspace runtimes: JWT workspace tokens, header-stripping trust boundary, Bun and Cloudflare adapters","license":"MIT","repository":{"type":"git","url":"git+https://github.com/kyashrathore/Claxedo.git","directory":"packages/workspace-relay"},"keywords":["claxedo","relay","tunnel","workspaces"],"main":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"development":"./src/index.ts","bun":"./src/index.ts","types":"./dist/index.d.ts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./bun":{"development":"./src/bun.ts","bun":"./src/bun.ts","types":"./dist/bun.d.ts","import":"./dist/bun.mjs","default":"./dist/bun.mjs"}},"bin":{"workspace-relay":"dist/main.mjs"},"publishConfig":{"access":"public"},"name":"@claxedo/workspace-relay","version":"0.8.0","scripts":{"start":"bun run src/main.ts","dev":"bun run --hot src/main.ts","build":"tsx scripts/build.ts","prepublishOnly":"npm run build","typecheck":"tsc --noEmit -p tsconfig.json","typecheck:bench":"tsc --noEmit -p bench/tsconfig.json","test":"mkdir -p .artifacts/unit && node ../../scripts/test-deadline.mjs bun test src bench --path-ignore-patterns='**/relay-workerd-*' --path-ignore-patterns='**/bun.test.ts' --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml && node ../../scripts/test-deadline.mjs bun test src/bun.test.ts --timeout 120000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-bun-adapter.xml && node ../../scripts/test-deadline.mjs bun test src/relay-workerd-backpressure.test.ts src/relay-workerd-binary.test.ts --timeout 240000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-workerd.xml","bench:gate":"bun bench/local-dry-run.ts","bench:gate:cf":"bun bench/cf-dev-smoke.ts"},"dependencies":{"@claxedo/helpers":"0.1.0","@claxedo/workspace-relay-protocol":"0.8.0","hono":"4.10.7","jose":"6.0.11","posthog-node":"4.18.0"},"devDependencies":{"@claxedo/sandbox-manager":"0.9.0","@daytona/sdk":"0.192.0","@tsconfig/node-lts":"22.0.4","@types/bun":"1.3.13","esbuild":"0.25.12","miniflare":"4.20260722.0","tsx":"4.22.3","typescript":"catalog:","wrangler":"4.114.0","@cloudflare/workers-types":"catalog:"},"_id":"@claxedo/workspace-relay@0.8.0","bugs":{"url":"https://github.com/kyashrathore/Claxedo/issues"},"homepage":"https://github.com/kyashrathore/Claxedo#readme","_nodeVersion":"26.8.1","_npmVersion":"11.19.0","dist":{"integrity":"sha512-CIi3Eynt+ViG/pYjctM3NpzyNMOsS+6Ft5j73zyKHuN1bmUfg46HX3KdEixH8Mw5IafegDVjXvqAhRY5sTqsnA==","shasum":"7713f300b6ed201cadb8c96c5320769cb83f9dd7","tarball":"https://registry.npmjs.org/@claxedo/workspace-relay/-/workspace-relay-0.8.0.tgz","fileCount":22,"unpackedSize":1070309,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHZwwr9DdALPwo54M5y79CT+SPcETY25OItCF1UG84jzAiAqWEKxUaE3FINDfNjS/evYHczlRAT50lluZ2YFsq9zoA=="}]},"_npmUser":{"name":"kyashrathore","email":"kanusdlp@gmail.com"},"directories":{},"maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/workspace-relay_0.8.0_1789461672532_0.4717763837251805"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T13:19:50.943Z","modified":"2026-09-15T08:41:12.914Z","0.1.0":"2026-05-19T13:19:51.141Z","0.5.0":"2026-07-01T16:53:42.331Z","0.5.1":"2026-07-01T16:57:11.800Z","0.5.2":"2026-07-18T15:41:00.846Z","0.6.0":"2026-07-20T14:13:12.615Z","0.7.0":"2026-07-30T16:52:49.539Z","0.8.0":"2026-09-15T08:41:12.679Z"},"bugs":{"url":"https://github.com/kyashrathore/Claxedo/issues"},"license":"MIT","homepage":"https://github.com/kyashrathore/Claxedo#readme","keywords":["claxedo","relay","tunnel","workspaces"],"repository":{"type":"git","url":"git+https://github.com/kyashrathore/Claxedo.git","directory":"packages/workspace-relay"},"description":"Relay/tunnel server that routes authenticated traffic to workspace runtimes: JWT workspace tokens, header-stripping trust boundary, Bun and Cloudflare adapters","maintainers":[{"name":"kyashrathore","email":"kanusdlp@gmail.com"}],"readme":"# `@claxedo/workspace-relay`\n\nThe Claxedo workspace relay — the single canonical tunnel process between\ncloud-hosted browsers and workspace-runtime hosts, whether the host is a cloud\nVM or a user laptop. One package, one process, one config surface — there is no\nseparate `dev-relay` codepath.\n\nRuntime requirement: the packaged `workspace-relay` bin and the default server\nbootstrap run on **Bun** (they use `Bun.*` APIs via the Bun adapter). Node is\nnot supported for the standalone process today; the Cloudflare Worker adapter\nis the other supported runtime.\n\nProduction v1 is deliberately single-instance for user-hosted traffic. The\nrelay keeps host presence and host-tunnel sockets in process-local maps, and a\n`hostId` has exactly one active tunnel at a time. Deploy one active relay\nprocess per user-hosted relay fleet; horizontal scaling needs a future routing\nowner for sticky host tunnels, split-brain prevention, and failover. A relay\nprocess, VM, or region failure drops existing user-hosted HTTP, WebSocket, SSE,\nand PTY sessions until workspace runtimes reconnect.\n\n## Quickstart\n\n```sh\nbun add @claxedo/workspace-relay jose\n```\n\nThe smallest runnable relay: mint an EdDSA key pair to stand in for the\ncontrol plane's signing key, point `resolveTarget` at a cloud-VM-style\nworkspace host, boot the Bun adapter, then mint a Runtime Access Token the\nway `claxedo-control-plane` would and use it to reach the target through the\nrelay.\n\n```ts\n// relay.ts\nimport { generateKeyPair } from \"jose\"\nimport { createWorkspaceRelayBun, mintRuntimeAccessToken } from \"@claxedo/workspace-relay\"\n\nconst runtimeAccessKey = await generateKeyPair(\"EdDSA\", { extractable: true })\nconst relayHostKey = await generateKeyPair(\"EdDSA\", { extractable: true })\n\n// Stand-in workspace host: whatever the relay forwards accepted traffic to.\nconst host = Bun.serve({\n  port: 0,\n  fetch: (request) => new Response(`host saw ${new URL(request.url).pathname}`),\n})\n\nconst handler = createWorkspaceRelayBun({\n  runtimeAccessKey: runtimeAccessKey.publicKey,\n  relayHostSigningKey: relayHostKey.privateKey,\n  relayHostAlgorithm: \"EdDSA\",\n  resolveTarget: (claims) => ({\n    workspaceId: claims.workspace_id,\n    hostId: claims.host_id,\n    baseUrl: String(host.url).replace(/\\/$/, \"\"),\n    access: \"cloud\",\n    backing: \"cloud-vm\",\n  }),\n})\n\nconst relay = Bun.serve({\n  port: 7777,\n  fetch: handler.fetch,\n  websocket: handler.websocket,\n})\n\nconst token = await mintRuntimeAccessToken(\n  {\n    principalKind: \"user\",\n    actorId: \"user_1\",\n    actorKind: \"human\",\n    orgId: \"org_1\",\n    workspaceId: \"ws_1\",\n    hostId: \"host_1\",\n    role: \"editor\",\n  },\n  runtimeAccessKey.privateKey,\n  \"EdDSA\",\n)\n\nconsole.log(`relay listening on ${relay.url}`)\nconsole.log(`curl -H \"Authorization: Bearer ${token}\" ${relay.url}workspaces/ws_1/hello`)\n```\n\n```sh\nbun run relay.ts\n```\n\nRun the printed `curl` command in another terminal — the relay verifies the\nRuntime Access Token, calls `resolveTarget`, mints a Relay Host Token, and\nforwards the request; you'll see `host saw /hello` come back. This mirrors\nwhat [`src/main.ts`](src/main.ts) assembles for production, minus the\nenv-driven resolver client, JWKS, and graceful drain — see\n[docs/architecture.md](docs/architecture.md) for how those pieces fit\ntogether, and the Configuration table below for the env vars that replace\nthe hardcoded values above in a real deployment.\n\n## Deployment Security\n\nThe relay is a public edge service for workspace traffic, but it is not the\nidentity provider or policy database. It verifies short-lived runtime access\ntokens, calls the configured target resolver/revocation callback, and forwards\naccepted traffic to the selected host with a freshly minted relay-host token.\n\n| Token | Default TTL | Issuer | Audience | Purpose |\n| --- | ---: | --- | --- | --- |\n| Runtime Access Token (RAT) | 30 minutes | `claxedo-control-plane` | `workspace-relay` | User/browser authorization to reach one workspace and host through the relay. |\n| Host Tunnel Token (HTT) | 5 minutes | `claxedo-control-plane` | `workspace-relay-host-tunnel` | Workspace runtime authorization to register a user-hosted tunnel. |\n| Relay Host Token (RHT) | 60 seconds | `workspace-relay` | `workspace-host-service` | Per-request relay-to-host authorization minted after RAT validation. |\n\nRuntime access tokens and host tunnel tokens bind issuer, audience, subject,\nworkspace id, host id, expiry, issue time, and JTI. Runtime access tokens also\nbind role. Relay-host tokens additionally bind the deployment pair:\n`cloud/cloud-vm` or `user-hosted/local-worktree`.\n\n### Revocation And Active Checks\n\n`isRuntimeAccessTokenActive` is the revocation/target freshness hook. Production\ndeployments should implement it by calling the control plane or an equivalent\nauthority on every new HTTP request and WebSocket upgrade. A false result\nrejects before forwarding to the host.\n\nLong-lived relayed sockets are authorized at establishment time. Already-open\nSSE, PTY, and WebSocket streams may live until normal close, reconnect, relay\ndrain, host disconnect, or process restart. If immediate stream revocation is a\nrequirement, the control plane must also close the session/runtime channel.\n\n### Host Tunnel Serving-Generation Fence\n\nA Host Tunnel Token minted by a connect-enrolled host carries `enrollment_id`\nand `generation`. A relay built by `src/main.ts` or `src/worker.ts` — that is,\nany relay with a resolver — asks\n`GET <CLAXEDO_RELAY_RESOLVER_URL>/host-generation?enrollmentId=` on every\nadmission and re-checks established tunnels every 30 s (both adapters). The\nverdicts, in order:\n\n| Control plane answer | Admission | Established tunnel |\n| --- | --- | --- |\n| Same generation, not revoked | admitted | kept |\n| Higher generation | `403 host_generation_superseded` | closed `1008` |\n| Lower generation | `403 host_generation_unknown` | closed `1008` |\n| Enrollment revoked or paused | `403 host_enrollment_revoked` | closed `1008` |\n| `404 relay_resolver_enrollment_not_found` | `403 host_enrollment_unknown` | closed `1008` |\n| Any other status, a bare 404 (route missing), malformed body, or the 5 s deadline | `503 host_generation_lookup_unavailable` (retry) | survives two consecutive failures, closed `1012` on the third |\n\nA token without a generation is admitted without asking the control plane. It\nnever displaces a socket that carries a generation: for one host+workspace\nidentity (Bun) or one room (Cloudflare), an incumbent with a generation yields\nonly to an equal or higher generation, and an incumbent without one yields to\nany later socket. Every refusal is audited as `host_tunnel.denied` with the\ncode as `reason` (Bun; the Cloudflare room has no audit hook).\n\nA `host.registration.update` frame is re-admitted the same way, with the\nsocket's own claims as the first incumbent: an update whose token carries no\ngeneration, or a lower one than the socket holds, is refused (closed `1008\nHost tunnel registration update superseded`); one that passes is then checked\nagainst the control plane exactly as a connect is and closed with the table's\nestablished-tunnel code on refusal (`1012` for an unavailable lookup, without\nthe outage grace). An accepted update replaces the socket's claims and\nidentities, and a socket that became fenced starts the 30 s re-check (or arms\nthe hibernation alarm).\n\nA relay composed directly from `createWorkspaceRelayBun` /\n`createWorkspaceRelayDurableObjectRoom` without `resolveHostGeneration` — the\ndesktop and self-hosted composition — admits tokens without a generation\nnewest-wins and refuses any token that carries one with\n`403 host_generation_unverifiable` (an update: closed `1008`). A generation is\na fence the relay cannot verify without the resolver, and the refusal is not\nretryable because the resolver is a property of the composition.\n\n### Forwarding Boundary\n\nThe relay strips client-supplied `x-forwarded-for`, `x-forwarded-host`,\n`x-forwarded-proto`, `x-real-ip`, `x-claxedo-internal-*`, and `x-supervisor-*`\nheaders. It replaces `Authorization` with an RHT, sets `x-workspace-id`, and\nadds `x-forwarded-by: workspace-relay`.\n\nFor user-hosted targets, `Cookie` is stripped before forwarding. User-hosted\nworkspace processes may run near a developer's local browser cookie jar, so\nbrowser cookies must not be passed through to the local host service. Cloud VM\ntargets may receive cookies when the caller intentionally sends them.\n\n### CORS\n\nThe relay owns CORS responses for browser-facing workspace requests. Do not\nforward upstream CORS headers as the source of truth. Add allowed product\norigins in the relay/server CORS configuration and keep wildcard origins out of\ncredentialed deployments.\n\n### Metrics\n\n`GET /metrics` is privileged operational data. In production, set\n`CLAXEDO_RELAY_METRICS_TOKEN` and scrape with `Authorization: Bearer <token>`.\nWhen no metrics token is configured, the endpoint only allows callers that the\nadapter identifies as loopback; if no remote-address resolver exists, it fails\nclosed.\n\n### User-Hosted Topology\n\nProduction v1 supports one active relay instance for user-hosted traffic, or a\nload balancer with strict stickiness that keeps each `hostId` on the relay\nprocess that owns its tunnel socket. Non-sticky horizontal scaling needs an\nexternal directory and tunnel routing owner before it is safe to advertise as\ndurable.\n\n## Why one package, no parallel impl\n\nIn the fa9cabf9 design pass the working assumption was clarified:\n\n> \"I think this creates a parallel implementation problem — why is there\n> no devmode in the prod relay?\"\n\nSo this package is the relay. Local-dev configuration is selected by\nenvironment variables, not by a sibling `dev-relay.ts` module. If a\nfuture cycle re-introduces a separate `dev-relay.*` file in\n`packages/claxedo-server/`, that should be flagged as a regression.\n\n## Public surface\n\nRe-exported from [`src/index.ts`](src/index.ts):\n\n| Concern | Module | Notable exports |\n| --- | --- | --- |\n| Token issuance / verification | [`src/auth.ts`](src/auth.ts) | `mintRuntimeAccessToken`, `verifyRuntimeAccessToken`, `mintRelayHostToken`, `verifyRelayHostToken`, `mintHostTunnelToken`, `verifyHostTunnelToken`, types `RelayRole`, `RelayAccess`, `RelayBacking`, `RelayJwtAlgorithm`, `RuntimeAccessTokenClaims`, `RelayKey`, `RelayKeyResolver`, error class `WorkspaceRelayAuthError` |\n| Hono HTTP surface | [`src/server.ts`](src/server.ts) | `createWorkspaceRelay`, `authorizeWorkspaceRelayRequest`, types `WorkspaceRelayOptions`, `WorkspaceRelayTarget`, `RuntimeAccessTokenActiveResult`, `WorkspaceRelayAuditEvent`, `WorkspaceRelayMetricsSources`, `RelayHostPublicKey` |\n| Active-host directory | [`src/directory.ts`](src/directory.ts) | `createWorkspaceRelayDirectory`, `disposeWorkspaceRelayDirectory`, types `WorkspaceRelayDirectory`, `HostTunnelPresence` |\n| Bun-specific server bootstrap | [`src/bun.ts`](src/bun.ts) | `createWorkspaceRelayBun`, type `WorkspaceRelayBunOptions`, `WorkspaceRelayBunDrainController`, metrics `getFragmentationStats`, `getSlowConsumerStats` |\n\nWire types live in the sibling package\n[`@claxedo/workspace-relay-protocol`](../workspace-relay-protocol/)\n(`TUNNEL_PROTOCOL_VERSION`, `TunnelMessage`, `isTunnelMessage`,\n`makeTunnelPong`). Keep that split — it lets non-Node consumers\nimplement the tunnel protocol without pulling Hono and Jose.\n\n## Configuration\n\nAll knobs are environment variables. The relay refuses to boot in\nproduction if `CLAXEDO_RELAY_RESOLVER_TOKEN` is missing — the\nproduction fail-closed gate at `src/main.ts`.\n\n| Env var | Purpose |\n| --- | --- |\n| `CLAXEDO_RELAY_BIND_HOST`, `CLAXEDO_RELAY_BIND_PORT` | Listening socket. Loopback by default. |\n| `CLAXEDO_RELAY_PUBLIC_URL` | Externally-resolvable URL the relay advertises in tokens. |\n| `CLAXEDO_RELAY_RESOLVER_URL` | Control-plane resolver base (`https://<control-plane>/internal/relay`). The relay derives `/target`, `/revocation`, and `/host-generation` from it. Required by the Bun process; the Worker also accepts `CLAXEDO_CENTRAL_URL` and appends `/internal/relay`. |\n| `CLAXEDO_RELAY_RESOLVER_TOKEN` | Bearer token the relay sends to the resolver. **Required in production.** |\n| `CLAXEDO_RELAY_HOST_GENERATION_URL` | Optional absolute URL of the host-generation lookup. Unset (the normal case) derives `<CLAXEDO_RELAY_RESOLVER_URL>/host-generation`; set it only when the lookup lives at a different origin than the rest of the resolver. There is no way to turn the fence off on a resolver-backed relay. |\n| `CLAXEDO_RELAY_HOST_GENERATION_CACHE_TTL_MS` | Cache TTL for host-generation answers. Defaults to 10000. A superseded tunnel closes within the re-check interval (30 s) plus this TTL. |\n| `CLAXEDO_RELAY_TARGET_CACHE_TTL_MS`, `CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS` | Cache TTLs for `/target` (default 30000 on Bun, 5000 on the Worker) and `/revocation` (default 10000) answers. |\n| `CLAXEDO_RELAY_JWKS_URL` | Optional remote JWKS the relay uses to verify runtime-access tokens. |\n| `CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM` | Inline public key (alternative to JWKS). |\n| `CLAXEDO_RELAY_HOST_VERIFY_PEM` | Public PEM the relay uses to verify host-tunnel tokens. |\n| `CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM` | Private PEM the relay uses to mint relay-host tokens. |\n| `CLAXEDO_RELAY_METRICS_TOKEN` | Optional bearer token for `/metrics`. Without it, `/metrics` requires a trusted loopback remote-address resolver. |\n| `CLAXEDO_RELAY_DRAIN_TIMEOUT_MS` | Graceful shutdown wait before force-closing sockets. Defaults to 30000. |\n| `CLAXEDO_RELAY_ALLOWED_ORIGINS` | Comma-separated browser-origin allowlist for CORS. **Replaces** the built-in default list (Claxedo/OpenCode app origins plus `http://localhost:*` dev hosts) — self-hosted deployments set their own origins here. Grammar: exact origin, `https://*.example.com`, `http://localhost:*`. Works on both the Bun process and the Cloudflare Worker. |\n| `NODE_ENV` | `production` / `development` / `test`. Switches the production fail-closed gate. |\n\nCloudflare Worker tracing knobs (Durable Object deployment):\n\n| Env var | Purpose |\n| --- | --- |\n| `CLAXEDO_RELAY_TRACE_SAMPLE_RATE` | 0–1 sampling rate for request traces. Only sampled requests emit `Server-Timing` phase headers and structured trace logs; unsampled responses carry just the `x-claxedo-trace-id` correlation header. Defaults to 0 (off). |\n| `CLAXEDO_RELAY_TRACE_FORCE_SECRET` | Optional operator secret. When set, a request with `x-claxedo-relay-trace: <secret>` forces sampling for that request (targeted debugging). Unset (default) means the force header is ignored — clients can never force tracing or timing emission. |\n| `CLAXEDO_APP_ORIGINS` | Cloudflare-only, **additive** origin entries layered on top of the base allowlist (kept for existing deploys; prefer `CLAXEDO_RELAY_ALLOWED_ORIGINS` for full control). |\n\nToken verification is pluggable today via the\n`RelayKey | RelayKeyResolver` pair on `verifyRuntimeAccessToken` and\nfriends, **and** via the unified\n[`TokenVerifier`](../workspace-relay-protocol/src/token-verifier.ts)\ninterface in `@claxedo/workspace-relay-protocol`. A custom verifier is only the\ncrypto/introspection authority; the relay still validates its output into\n`RuntimeAccessTokenClaims`, binds the URL workspace id to the claims, applies\nrevocation, and allowlists roles. Missing, unknown, or malformed roles deny.\n\nTo swap the relay's auth backend in a self-hosted deployment:\n\n```ts\nimport { createStaticTokenVerifier } from \"@claxedo/workspace-relay-protocol\"\n\nconst verifier = createStaticTokenVerifier({\n  tokens: {\n    \"tok-tenant-1\": { subject: \"u1\", scopes: [\"workspace:write\"], claims: {} },\n  },\n})\n// Pass `verifier` as `WorkspaceRelayOptions.tokenVerifier`.\n```\n\nThe `TokenVerifier` interface intentionally contains only `verify(token)`.\nJWKS fetching, audience binding, and replay caches belong to the\nimplementation. Two reference implementations ship in the protocol\npackage: `createHttpTokenVerifier` for remote verifiers\n(token introspection, custom OIDC), and `createStaticTokenVerifier`\nfor tests and self-hosted single-tenant setups.\n\n`createHttpTokenVerifier` is a reference implementation. Its HTTPS endpoint is\nthe crypto authority and must enforce issuer, audience, expiry, key selection,\nand replay policy before returning claims. Treat the endpoint as trusted\noperator configuration, not as tenant/user input.\n\nLong-lived relayed sockets are authorized at establishment. Revocation is\nchecked for new HTTP requests and WebSocket upgrades; already-established\nWebSocket/SSE/PTY streams may live until their normal close, reconnect, relay\ndrain, or process restart.\n\n## Tunnel Lifecycle\n\nHost-tunnel reconnects replace the old socket deterministically. Replacement\ncleans the old socket's pending HTTP responses, child WebSocket channels,\nheartbeat timer, and buffered work before installing the new socket. Stale close\nevents identity-check the current owner before deleting presence, so an old\nsocket cannot mark a replacement offline.\n\nRelay drain sets `/health` unhealthy, rejects new workspace requests and tunnel\nregistrations with `503 relay_draining`, closes active host tunnels so runtimes\nreconnect promptly, waits for pending work up to the configured timeout, then\nstops the server. Truly uncaught exceptions and unhandled rejections are fatal:\nthe relay marks itself draining, stops accepting work, disposes timers, and\nexits for supervisor restart.\n\n## External Directory Design\n\nThe current `WorkspaceRelayDirectory` is an in-memory implementation of the\ndirectory contract:\n\n```ts\ntype WorkspaceRelayDirectory = {\n  registerHostTunnel(input: { hostId: string; workspaceIds: string[] }): HostTunnelPresence\n  recordPong(hostId: string): HostTunnelPresence | undefined\n  disconnectHost(hostId: string): void\n  activeHost(input: { hostId: string; workspaceId: string }): HostTunnelPresence | undefined\n  sweep(): void\n  dispose(): void\n  size(): number\n}\n```\n\nA Redis, Durable Object, or equivalent implementation should keep the same\nsemantics:\n\n- one active owner for a `hostId`;\n- TTL extension on heartbeat pong;\n- immediate removal on disconnect;\n- workspace membership checks before user-hosted forwarding;\n- split-brain prevention when a replacement tunnel connects;\n- observability for active host count and stale owner cleanup.\n\nThe missing piece for true multi-instance user-hosted relay is not just durable\npresence storage. HTTP/WebSocket/SSE/PTY traffic must also route to the process\nor durable object that owns the live tunnel socket for that `hostId`.\n\n## Routing\n\nThe relay has **no** `/w/{workspaceId}/*` URL prefix of its own. The\ngateway pattern lives in `claxedo-server`:\n\n- The user's browser calls `/api/claxedo/...`.\n- `claxedo-server` mounts `workspaceRuntimeProxy` middleware\n  (`packages/claxedo-server/src/workspace/runtime-dispatch/internals.ts:205`) on its top-level Hono\n  app at `packages/claxedo-server/src/deployments/local/server.ts:93`.\n- The proxy resolves the active workspace target via\n  `internal-relay.ts` (control-plane auth — see \"Seam: internal-relay\n  vs workspace-relay\" below), strips its own prefix, and forwards\n  to the relay's tunnel endpoint.\n\nIf you are adding a new HTTP-level workspace surface, attach it to\n`claxedo-server`'s gateway, not to `workspace-relay`. The relay is\nintentionally narrow — it only handles tunnel traffic and the auth\nchecks that gate it.\n\n## Seam: `internal-relay` vs `workspace-relay`\n\n| | `claxedo-server/src/deployments/shared-routes/internal-relay.ts` | `@claxedo/workspace-relay` |\n| --- | --- | --- |\n| Layer | Control-plane auth | Tunnel transport |\n| Trust | Authenticates the relay process itself (loopback or bearer) | Authenticates the user's runtime-access token |\n| Routes | `GET /internal/relay/target`, `GET /internal/relay/revocation` | WS upgrade + tunnel framing |\n| Owners | `claxedo-server` (depends on the workspace authority, the identity provider, audit log) | `workspace-relay` (no authority or identity-provider dependency) |\n| Lives in | `packages/claxedo-server/` (server-side only) | `packages/workspace-relay/` (own process) |\n\nThis split is deliberate: the relay never reads the workspace authority or the\nidentity provider directly. It calls back to `claxedo-server` over HTTP for resolver\ndecisions. The decision data crosses the seam as\n`RuntimeAccessTokenActiveResult` (defined in `server.ts`).\n\n## Development\n\n```sh\nbun --cwd packages/workspace-relay dev   # hot-reload main.ts\nbun --cwd packages/workspace-relay test  # run all *.test.ts\nbun --cwd packages/workspace-relay typecheck\n```\n\nThe TS error baseline for this package is **0** — keep it that way.\n\n## Boundary rule\n\nConsumers outside this package import `@claxedo/workspace-relay` only —\nnot `@claxedo/workspace-relay/src/...`. Direct deep-source imports\nbreak the package boundary. The check is\n\n```sh\ngrep -rn \"workspace-relay/src/\" packages/claxedo-{server,app}/src\n```\n\n— required to return zero hits.\n","readmeFilename":"README.md"}