@claxedo/workspace-relay 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Claxedo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,6 +5,11 @@ cloud-hosted browsers and workspace-runtime hosts, whether the host is a cloud
5
5
  VM or a user laptop. One package, one process, one config surface — there is no
6
6
  separate `dev-relay` codepath.
7
7
 
8
+ Runtime requirement: the packaged `workspace-relay` bin and the default server
9
+ bootstrap run on **Bun** (they use `Bun.*` APIs via the Bun adapter). Node is
10
+ not supported for the standalone process today; the Cloudflare Worker adapter
11
+ is the other supported runtime.
12
+
8
13
  Production v1 is deliberately single-instance for user-hosted traffic. The
9
14
  relay keeps host presence and host-tunnel sockets in process-local maps, and a
10
15
  `hostId` has exactly one active tunnel at a time. Deploy one active relay
@@ -13,6 +18,74 @@ owner for sticky host tunnels, split-brain prevention, and failover. A relay
13
18
  process, VM, or region failure drops existing user-hosted HTTP, WebSocket, SSE,
14
19
  and PTY sessions until workspace runtimes reconnect.
15
20
 
21
+ ## Quickstart
22
+
23
+ ```sh
24
+ bun add @claxedo/workspace-relay jose
25
+ ```
26
+
27
+ The smallest runnable relay: mint an EdDSA key pair to stand in for the
28
+ control plane's signing key, point `resolveTarget` at a cloud-VM-style
29
+ workspace host, boot the Bun adapter, then mint a Runtime Access Token the
30
+ way `claxedo-control-plane` would and use it to reach the target through the
31
+ relay.
32
+
33
+ ```ts
34
+ // relay.ts
35
+ import { generateKeyPair } from "jose"
36
+ import { createWorkspaceRelayBun, mintRuntimeAccessToken } from "@claxedo/workspace-relay"
37
+
38
+ const runtimeAccessKey = await generateKeyPair("EdDSA", { extractable: true })
39
+ const relayHostKey = await generateKeyPair("EdDSA", { extractable: true })
40
+
41
+ // Stand-in workspace host: whatever the relay forwards accepted traffic to.
42
+ const host = Bun.serve({
43
+ port: 0,
44
+ fetch: (request) => new Response(`host saw ${new URL(request.url).pathname}`),
45
+ })
46
+
47
+ const handler = createWorkspaceRelayBun({
48
+ runtimeAccessKey: runtimeAccessKey.publicKey,
49
+ relayHostSigningKey: relayHostKey.privateKey,
50
+ relayHostAlgorithm: "EdDSA",
51
+ resolveTarget: (claims) => ({
52
+ workspaceId: claims.workspace_id,
53
+ hostId: claims.host_id,
54
+ baseUrl: String(host.url).replace(/\/$/, ""),
55
+ access: "cloud",
56
+ backing: "cloud-vm",
57
+ }),
58
+ })
59
+
60
+ const relay = Bun.serve({
61
+ port: 7777,
62
+ fetch: handler.fetch,
63
+ websocket: handler.websocket,
64
+ })
65
+
66
+ const token = await mintRuntimeAccessToken(
67
+ { subject: "user_1", orgId: "org_1", workspaceId: "ws_1", hostId: "host_1", role: "editor" },
68
+ runtimeAccessKey.privateKey,
69
+ "EdDSA",
70
+ )
71
+
72
+ console.log(`relay listening on ${relay.url}`)
73
+ console.log(`curl -H "Authorization: Bearer ${token}" ${relay.url}workspaces/ws_1/hello`)
74
+ ```
75
+
76
+ ```sh
77
+ bun run relay.ts
78
+ ```
79
+
80
+ Run the printed `curl` command in another terminal — the relay verifies the
81
+ Runtime Access Token, calls `resolveTarget`, mints a Relay Host Token, and
82
+ forwards the request; you'll see `host saw /hello` come back. This mirrors
83
+ what [`src/main.ts`](src/main.ts) assembles for production, minus the
84
+ env-driven resolver client, JWKS, and graceful drain — see
85
+ [docs/architecture.md](docs/architecture.md) for how those pieces fit
86
+ together, and the Configuration table below for the env vars that replace
87
+ the hardcoded values above in a real deployment.
88
+
16
89
  ## Deployment Security
17
90
 
18
91
  The relay is a public edge service for workspace traffic, but it is not the
@@ -125,8 +198,17 @@ production fail-closed gate at `src/main.ts`.
125
198
  | `CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM` | Private PEM the relay uses to mint relay-host tokens. |
126
199
  | `CLAXEDO_RELAY_METRICS_TOKEN` | Optional bearer token for `/metrics`. Without it, `/metrics` requires a trusted loopback remote-address resolver. |
127
200
  | `CLAXEDO_RELAY_DRAIN_TIMEOUT_MS` | Graceful shutdown wait before force-closing sockets. Defaults to 30000. |
201
+ | `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. |
128
202
  | `NODE_ENV` | `production` / `development` / `test`. Switches the production fail-closed gate. |
129
203
 
204
+ Cloudflare Worker tracing knobs (Durable Object deployment):
205
+
206
+ | Env var | Purpose |
207
+ | --- | --- |
208
+ | `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). |
209
+ | `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. |
210
+ | `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). |
211
+
130
212
  Token verification is pluggable today via the
131
213
  `RelayKey | RelayKeyResolver` pair on `verifyRuntimeAccessToken` and
132
214
  friends, **and** via the unified
package/dist/auth.d.ts CHANGED
@@ -35,6 +35,7 @@ export type RelayHostTokenClaims = {
35
35
  org_id: string;
36
36
  workspace_id: string;
37
37
  host_id: string;
38
+ role: RelayRole;
38
39
  exp: number;
39
40
  iat: number;
40
41
  jti: string;
@@ -53,6 +53,7 @@ export type WorkspaceRelayDurableObjectRoomOptions = WorkspaceRelayOptions & {
53
53
  runtimeAccessTokenActiveCheckIntervalMs?: number;
54
54
  workspaceTargetActiveCheckIntervalMs?: number;
55
55
  traceSampleRate?: number;
56
+ traceForceHeaderSecret?: string;
56
57
  traceLog?: (event: WorkspaceRelayTraceLogEvent) => void;
57
58
  now?: () => number;
58
59
  };
@@ -98,6 +99,7 @@ export type WorkspaceRelayDurableObjectDrainController = {
98
99
  export declare function workspaceRelayDurableObjectRoomName(workspaceId: string): string;
99
100
  export declare function workspaceRelayDurableObjectWorkspaceId(request: Request): string | undefined;
100
101
  export declare function setWorkspaceRelayAppOrigins(raw: string | undefined): void;
102
+ export declare function setWorkspaceRelayAllowedOrigins(raw: string | undefined): void;
101
103
  export declare function createWorkspaceRelayDurableObjectGateway(options?: WorkspaceRelayDurableObjectGatewayOptions): {
102
104
  fetch(request: Request, env?: WorkspaceRelayDurableObjectEnv): Promise<Response>;
103
105
  };
@@ -110,6 +112,9 @@ export declare function createWorkspaceRelayDurableObjectRoom(options: Workspace
110
112
  hostTunnelCount: number;
111
113
  clientCount: number;
112
114
  hostIds: string[];
115
+ hostWorkspaceIds: {
116
+ [k: string]: string[];
117
+ };
113
118
  };
114
119
  drain: WorkspaceRelayDurableObjectDrainController;
115
120
  };
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_RELAY_APP_ORIGINS: string[];
2
+ export declare function parseAllowedOrigins(raw: string | undefined): string[] | undefined;
3
+ export declare function createOriginMatcher(patterns: string[]): (origin: string) => boolean;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./auth";
2
2
  export * from "./bun";
3
3
  export * from "./cloudflare";
4
+ export * from "./cors-origins";
4
5
  export * from "./directory";
5
6
  export * from "./server";