@claxedo/workspace-relay 0.1.0 → 0.5.1
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/README.md +143 -9
- package/dist/auth.d.ts +16 -8
- package/dist/bun.d.ts +40 -8
- package/dist/cloudflare.d.ts +116 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +2153 -212
- package/dist/main.d.ts +35 -18
- package/dist/main.mjs +984 -215
- package/dist/server.d.ts +83 -15
- package/dist/worker.d.ts +50 -0
- package/package.json +18 -8
package/README.md
CHANGED
|
@@ -1,9 +1,82 @@
|
|
|
1
1
|
# `@claxedo/workspace-relay`
|
|
2
2
|
|
|
3
|
-
The Claxedo workspace relay —
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The Claxedo workspace relay — the single canonical tunnel process between
|
|
4
|
+
cloud-hosted browsers and workspace-runtime hosts, whether the host is a cloud
|
|
5
|
+
VM or a user laptop. One package, one process, one config surface — there is no
|
|
6
|
+
separate `dev-relay` codepath.
|
|
7
|
+
|
|
8
|
+
Production v1 is deliberately single-instance for user-hosted traffic. The
|
|
9
|
+
relay keeps host presence and host-tunnel sockets in process-local maps, and a
|
|
10
|
+
`hostId` has exactly one active tunnel at a time. Deploy one active relay
|
|
11
|
+
process per user-hosted relay fleet; horizontal scaling needs a future routing
|
|
12
|
+
owner for sticky host tunnels, split-brain prevention, and failover. A relay
|
|
13
|
+
process, VM, or region failure drops existing user-hosted HTTP, WebSocket, SSE,
|
|
14
|
+
and PTY sessions until workspace runtimes reconnect.
|
|
15
|
+
|
|
16
|
+
## Deployment Security
|
|
17
|
+
|
|
18
|
+
The relay is a public edge service for workspace traffic, but it is not the
|
|
19
|
+
identity provider or policy database. It verifies short-lived runtime access
|
|
20
|
+
tokens, calls the configured target resolver/revocation callback, and forwards
|
|
21
|
+
accepted traffic to the selected host with a freshly minted relay-host token.
|
|
22
|
+
|
|
23
|
+
| Token | Default TTL | Issuer | Audience | Purpose |
|
|
24
|
+
| --- | ---: | --- | --- | --- |
|
|
25
|
+
| Runtime Access Token (RAT) | 30 minutes | `claxedo-control-plane` | `workspace-relay` | User/browser authorization to reach one workspace and host through the relay. |
|
|
26
|
+
| Host Tunnel Token (HTT) | 5 minutes | `claxedo-control-plane` | `workspace-relay-host-tunnel` | Workspace runtime authorization to register a user-hosted tunnel. |
|
|
27
|
+
| Relay Host Token (RHT) | 60 seconds | `workspace-relay` | `workspace-host-service` | Per-request relay-to-host authorization minted after RAT validation. |
|
|
28
|
+
|
|
29
|
+
Runtime access tokens and host tunnel tokens bind issuer, audience, subject,
|
|
30
|
+
workspace id, host id, expiry, issue time, and JTI. Runtime access tokens also
|
|
31
|
+
bind role. Relay-host tokens additionally bind the deployment pair:
|
|
32
|
+
`cloud/cloud-vm` or `user-hosted/local-worktree`.
|
|
33
|
+
|
|
34
|
+
### Revocation And Active Checks
|
|
35
|
+
|
|
36
|
+
`isRuntimeAccessTokenActive` is the revocation/target freshness hook. Production
|
|
37
|
+
deployments should implement it by calling the control plane or an equivalent
|
|
38
|
+
authority on every new HTTP request and WebSocket upgrade. A false result
|
|
39
|
+
rejects before forwarding to the host.
|
|
40
|
+
|
|
41
|
+
Long-lived relayed sockets are authorized at establishment time. Already-open
|
|
42
|
+
SSE, PTY, and WebSocket streams may live until normal close, reconnect, relay
|
|
43
|
+
drain, host disconnect, or process restart. If immediate stream revocation is a
|
|
44
|
+
requirement, the control plane must also close the session/runtime channel.
|
|
45
|
+
|
|
46
|
+
### Forwarding Boundary
|
|
47
|
+
|
|
48
|
+
The relay strips client-supplied `x-forwarded-for`, `x-forwarded-host`,
|
|
49
|
+
`x-forwarded-proto`, `x-real-ip`, `x-claxedo-internal-*`, and `x-supervisor-*`
|
|
50
|
+
headers. It replaces `Authorization` with an RHT, sets `x-workspace-id`, and
|
|
51
|
+
adds `x-forwarded-by: workspace-relay`.
|
|
52
|
+
|
|
53
|
+
For user-hosted targets, `Cookie` is stripped before forwarding. User-hosted
|
|
54
|
+
workspace processes may run near a developer's local browser cookie jar, so
|
|
55
|
+
browser cookies must not be passed through to the local host service. Cloud VM
|
|
56
|
+
targets may receive cookies when the caller intentionally sends them.
|
|
57
|
+
|
|
58
|
+
### CORS
|
|
59
|
+
|
|
60
|
+
The relay owns CORS responses for browser-facing workspace requests. Do not
|
|
61
|
+
forward upstream CORS headers as the source of truth. Add allowed product
|
|
62
|
+
origins in the relay/server CORS configuration and keep wildcard origins out of
|
|
63
|
+
credentialed deployments.
|
|
64
|
+
|
|
65
|
+
### Metrics
|
|
66
|
+
|
|
67
|
+
`GET /metrics` is privileged operational data. In production, set
|
|
68
|
+
`CLAXEDO_RELAY_METRICS_TOKEN` and scrape with `Authorization: Bearer <token>`.
|
|
69
|
+
When no metrics token is configured, the endpoint only allows callers that the
|
|
70
|
+
adapter identifies as loopback; if no remote-address resolver exists, it fails
|
|
71
|
+
closed.
|
|
72
|
+
|
|
73
|
+
### User-Hosted Topology
|
|
74
|
+
|
|
75
|
+
Production v1 supports one active relay instance for user-hosted traffic, or a
|
|
76
|
+
load balancer with strict stickiness that keeps each `hostId` on the relay
|
|
77
|
+
process that owns its tunnel socket. Non-sticky horizontal scaling needs an
|
|
78
|
+
external directory and tunnel routing owner before it is safe to advertise as
|
|
79
|
+
durable.
|
|
7
80
|
|
|
8
81
|
## Why one package, no parallel impl
|
|
9
82
|
|
|
@@ -24,7 +97,7 @@ Re-exported from [`src/index.ts`](src/index.ts):
|
|
|
24
97
|
| Concern | Module | Notable exports |
|
|
25
98
|
| --- | --- | --- |
|
|
26
99
|
| 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` |
|
|
27
|
-
| Hono HTTP surface | [`src/server.ts`](src/server.ts) | `
|
|
100
|
+
| Hono HTTP surface | [`src/server.ts`](src/server.ts) | `createWorkspaceRelay`, `authorizeWorkspaceRelayRequest`, types `WorkspaceRelayOptions`, `WorkspaceRelayTarget`, `RuntimeAccessTokenActiveResult`, `WorkspaceRelayAuditEvent`, `WorkspaceRelayMetricsSources`, `RelayHostPublicKey` |
|
|
28
101
|
| Active-host directory | [`src/directory.ts`](src/directory.ts) | `createWorkspaceRelayDirectory`, `disposeWorkspaceRelayDirectory`, types `WorkspaceRelayDirectory`, `HostTunnelPresence` |
|
|
29
102
|
| Bun-specific server bootstrap | [`src/bun.ts`](src/bun.ts) | `createWorkspaceRelayBun`, type `WorkspaceRelayBunOptions`, `WorkspaceRelayBunDrainController`, metrics `getFragmentationStats`, `getSlowConsumerStats` |
|
|
30
103
|
|
|
@@ -50,14 +123,20 @@ production fail-closed gate at `src/main.ts`.
|
|
|
50
123
|
| `CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM` | Inline public key (alternative to JWKS). |
|
|
51
124
|
| `CLAXEDO_RELAY_HOST_VERIFY_PEM` | Public PEM the relay uses to verify host-tunnel tokens. |
|
|
52
125
|
| `CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM` | Private PEM the relay uses to mint relay-host tokens. |
|
|
126
|
+
| `CLAXEDO_RELAY_METRICS_TOKEN` | Optional bearer token for `/metrics`. Without it, `/metrics` requires a trusted loopback remote-address resolver. |
|
|
127
|
+
| `CLAXEDO_RELAY_DRAIN_TIMEOUT_MS` | Graceful shutdown wait before force-closing sockets. Defaults to 30000. |
|
|
53
128
|
| `NODE_ENV` | `production` / `development` / `test`. Switches the production fail-closed gate. |
|
|
54
129
|
|
|
55
130
|
Token verification is pluggable today via the
|
|
56
131
|
`RelayKey | RelayKeyResolver` pair on `verifyRuntimeAccessToken` and
|
|
57
132
|
friends, **and** via the unified
|
|
58
133
|
[`TokenVerifier`](../workspace-relay-protocol/src/token-verifier.ts)
|
|
59
|
-
interface in `@claxedo/workspace-relay-protocol`.
|
|
60
|
-
|
|
134
|
+
interface in `@claxedo/workspace-relay-protocol`. A custom verifier is only the
|
|
135
|
+
crypto/introspection authority; the relay still validates its output into
|
|
136
|
+
`RuntimeAccessTokenClaims`, binds the URL workspace id to the claims, applies
|
|
137
|
+
revocation, and allowlists roles. Missing, unknown, or malformed roles deny.
|
|
138
|
+
|
|
139
|
+
To swap the relay's auth backend in a self-hosted deployment:
|
|
61
140
|
|
|
62
141
|
```ts
|
|
63
142
|
import { createStaticTokenVerifier } from "@claxedo/workspace-relay-protocol"
|
|
@@ -67,8 +146,7 @@ const verifier = createStaticTokenVerifier({
|
|
|
67
146
|
"tok-tenant-1": { subject: "u1", scopes: ["workspace:write"], claims: {} },
|
|
68
147
|
},
|
|
69
148
|
})
|
|
70
|
-
// Pass `verifier`
|
|
71
|
-
// option when the relay accepts custom verification backends directly.
|
|
149
|
+
// Pass `verifier` as `WorkspaceRelayOptions.tokenVerifier`.
|
|
72
150
|
```
|
|
73
151
|
|
|
74
152
|
The `TokenVerifier` interface intentionally contains only `verify(token)`.
|
|
@@ -78,6 +156,62 @@ package: `createHttpTokenVerifier` for remote verifiers
|
|
|
78
156
|
(Clerk-style introspection, custom OIDC), and `createStaticTokenVerifier`
|
|
79
157
|
for tests and self-hosted single-tenant setups.
|
|
80
158
|
|
|
159
|
+
`createHttpTokenVerifier` is a reference implementation. Its HTTPS endpoint is
|
|
160
|
+
the crypto authority and must enforce issuer, audience, expiry, key selection,
|
|
161
|
+
and replay policy before returning claims. Treat the endpoint as trusted
|
|
162
|
+
operator configuration, not as tenant/user input.
|
|
163
|
+
|
|
164
|
+
Long-lived relayed sockets are authorized at establishment. Revocation is
|
|
165
|
+
checked for new HTTP requests and WebSocket upgrades; already-established
|
|
166
|
+
WebSocket/SSE/PTY streams may live until their normal close, reconnect, relay
|
|
167
|
+
drain, or process restart.
|
|
168
|
+
|
|
169
|
+
## Tunnel Lifecycle
|
|
170
|
+
|
|
171
|
+
Host-tunnel reconnects replace the old socket deterministically. Replacement
|
|
172
|
+
cleans the old socket's pending HTTP responses, child WebSocket channels,
|
|
173
|
+
heartbeat timer, and buffered work before installing the new socket. Stale close
|
|
174
|
+
events identity-check the current owner before deleting presence, so an old
|
|
175
|
+
socket cannot mark a replacement offline.
|
|
176
|
+
|
|
177
|
+
Relay drain sets `/health` unhealthy, rejects new workspace requests and tunnel
|
|
178
|
+
registrations with `503 relay_draining`, closes active host tunnels so runtimes
|
|
179
|
+
reconnect promptly, waits for pending work up to the configured timeout, then
|
|
180
|
+
stops the server. Truly uncaught exceptions and unhandled rejections are fatal:
|
|
181
|
+
the relay marks itself draining, stops accepting work, disposes timers, and
|
|
182
|
+
exits for supervisor restart.
|
|
183
|
+
|
|
184
|
+
## External Directory Design
|
|
185
|
+
|
|
186
|
+
The current `WorkspaceRelayDirectory` is an in-memory implementation of the
|
|
187
|
+
directory contract:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
type WorkspaceRelayDirectory = {
|
|
191
|
+
registerHostTunnel(input: { hostId: string; workspaceIds: string[] }): HostTunnelPresence
|
|
192
|
+
recordPong(hostId: string): HostTunnelPresence | undefined
|
|
193
|
+
disconnectHost(hostId: string): void
|
|
194
|
+
activeHost(input: { hostId: string; workspaceId: string }): HostTunnelPresence | undefined
|
|
195
|
+
sweep(): void
|
|
196
|
+
dispose(): void
|
|
197
|
+
size(): number
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A Redis, Durable Object, or equivalent implementation should keep the same
|
|
202
|
+
semantics:
|
|
203
|
+
|
|
204
|
+
- one active owner for a `hostId`;
|
|
205
|
+
- TTL extension on heartbeat pong;
|
|
206
|
+
- immediate removal on disconnect;
|
|
207
|
+
- workspace membership checks before user-hosted forwarding;
|
|
208
|
+
- split-brain prevention when a replacement tunnel connects;
|
|
209
|
+
- observability for active host count and stale owner cleanup.
|
|
210
|
+
|
|
211
|
+
The missing piece for true multi-instance user-hosted relay is not just durable
|
|
212
|
+
presence storage. HTTP/WebSocket/SSE/PTY traffic must also route to the process
|
|
213
|
+
or durable object that owns the live tunnel socket for that `hostId`.
|
|
214
|
+
|
|
81
215
|
## Routing
|
|
82
216
|
|
|
83
217
|
The relay has **no** `/w/{workspaceId}/*` URL prefix of its own. The
|
package/dist/auth.d.ts
CHANGED
|
@@ -5,8 +5,15 @@ export declare const runtimeAccessTokenAudience = "workspace-relay";
|
|
|
5
5
|
export declare const hostTunnelTokenAudience = "workspace-relay-host-tunnel";
|
|
6
6
|
export declare const relayHostTokenIssuer = "workspace-relay";
|
|
7
7
|
export declare const relayHostTokenAudience = "workspace-host-service";
|
|
8
|
-
export type
|
|
9
|
-
|
|
8
|
+
export type RelayClaimPair = {
|
|
9
|
+
access: "cloud";
|
|
10
|
+
backing: "cloud-vm";
|
|
11
|
+
} | {
|
|
12
|
+
access: "user-hosted";
|
|
13
|
+
backing: "local-worktree";
|
|
14
|
+
};
|
|
15
|
+
export type RelayAccess = RelayClaimPair["access"];
|
|
16
|
+
export type RelayBacking = RelayClaimPair["backing"];
|
|
10
17
|
export type RelayJwtAlgorithm = (typeof algorithms)[number];
|
|
11
18
|
export type RelayRole = "viewer" | "editor" | "admin" | "owner";
|
|
12
19
|
export type RuntimeAccessTokenClaims = {
|
|
@@ -28,12 +35,10 @@ export type RelayHostTokenClaims = {
|
|
|
28
35
|
org_id: string;
|
|
29
36
|
workspace_id: string;
|
|
30
37
|
host_id: string;
|
|
31
|
-
access: RelayAccess;
|
|
32
|
-
backing: RelayBacking;
|
|
33
38
|
exp: number;
|
|
34
39
|
iat: number;
|
|
35
40
|
jti: string;
|
|
36
|
-
};
|
|
41
|
+
} & RelayClaimPair;
|
|
37
42
|
export type HostTunnelTokenClaims = {
|
|
38
43
|
iss: typeof runtimeAccessTokenIssuer;
|
|
39
44
|
aud: typeof hostTunnelTokenAudience;
|
|
@@ -58,9 +63,7 @@ type RuntimeInput = {
|
|
|
58
63
|
jti?: string;
|
|
59
64
|
now?: number;
|
|
60
65
|
};
|
|
61
|
-
type RelayHostInput = RuntimeInput & {
|
|
62
|
-
access: RelayAccess;
|
|
63
|
-
backing: RelayBacking;
|
|
66
|
+
type RelayHostInput = RuntimeInput & RelayClaimPair & {
|
|
64
67
|
/**
|
|
65
68
|
* Optional `kid` to embed in the JWT protected header. Verifiers using a
|
|
66
69
|
* JWKS resolver dispatch on this to pick the matching key, so a freshly
|
|
@@ -86,6 +89,10 @@ type ExpectedHostTunnel = {
|
|
|
86
89
|
hostId: string;
|
|
87
90
|
workspaceIds: string[];
|
|
88
91
|
};
|
|
92
|
+
export declare function isRelayClaimPair(input: {
|
|
93
|
+
access?: unknown;
|
|
94
|
+
backing?: unknown;
|
|
95
|
+
}): input is RelayClaimPair;
|
|
89
96
|
/**
|
|
90
97
|
* Resolver function returned by `createRemoteJWKSet` / `createLocalJWKSet`.
|
|
91
98
|
* Accepted by `jwtVerify` directly — jose dispatches based on whether the
|
|
@@ -100,6 +107,7 @@ export type RelayKey = CryptoKey | Uint8Array | RelayKeyResolver;
|
|
|
100
107
|
type RelaySigningKey = CryptoKey | Uint8Array;
|
|
101
108
|
export declare function mintRuntimeAccessToken(input: RuntimeInput, key: RelaySigningKey, alg: RelayJwtAlgorithm): Promise<string>;
|
|
102
109
|
export declare function verifyRuntimeAccessToken(token: string, key: RelayKey, expected: ExpectedTarget): Promise<RuntimeAccessTokenClaims>;
|
|
110
|
+
export declare function validateRuntimeAccessTokenClaims(input: Record<string, unknown>, expected: ExpectedTarget): RuntimeAccessTokenClaims;
|
|
103
111
|
export declare function mintHostTunnelToken(input: HostTunnelInput, key: RelaySigningKey, alg: RelayJwtAlgorithm): Promise<string>;
|
|
104
112
|
export declare function verifyHostTunnelToken(token: string, key: RelayKey, expected: ExpectedHostTunnel): Promise<HostTunnelTokenClaims>;
|
|
105
113
|
export declare function mintRelayHostToken(input: RelayHostInput, key: RelaySigningKey, alg: RelayJwtAlgorithm): Promise<string>;
|
package/dist/bun.d.ts
CHANGED
|
@@ -3,8 +3,21 @@ type RelayClientWebSocketData = {
|
|
|
3
3
|
kind: "client";
|
|
4
4
|
upstreamUrl: string;
|
|
5
5
|
headers: Record<string, string>;
|
|
6
|
-
queue: Array<
|
|
6
|
+
queue: Array<{
|
|
7
|
+
payload: string | Buffer<ArrayBuffer>;
|
|
8
|
+
queuedAt: number;
|
|
9
|
+
}>;
|
|
7
10
|
upstream?: WebSocket;
|
|
11
|
+
upstreamOpenTimer?: ReturnType<typeof setTimeout>;
|
|
12
|
+
trace?: RelayClientWebSocketTrace;
|
|
13
|
+
};
|
|
14
|
+
type RelayClientWebSocketTrace = {
|
|
15
|
+
acceptedAt: number;
|
|
16
|
+
upstreamStartedAt?: number;
|
|
17
|
+
upstreamOpenMs?: number;
|
|
18
|
+
queuedFrames: number;
|
|
19
|
+
maxQueuedDelayMs: number;
|
|
20
|
+
emitted: boolean;
|
|
8
21
|
};
|
|
9
22
|
type RelayHostTunnelWebSocketData = {
|
|
10
23
|
kind: "host-tunnel";
|
|
@@ -13,6 +26,7 @@ type RelayHostTunnelWebSocketData = {
|
|
|
13
26
|
pending: Map<string, PendingTunnelHttpResponse>;
|
|
14
27
|
channels: Map<string, Bun.ServerWebSocket<RelayUserHostedClientWebSocketData>>;
|
|
15
28
|
heartbeat?: ReturnType<typeof setInterval>;
|
|
29
|
+
missedPongs: number;
|
|
16
30
|
messageBuffer: string;
|
|
17
31
|
};
|
|
18
32
|
type RelayUserHostedClientWebSocketData = {
|
|
@@ -24,6 +38,9 @@ type RelayUserHostedClientWebSocketData = {
|
|
|
24
38
|
relayHostToken: string;
|
|
25
39
|
};
|
|
26
40
|
type RelayWebSocketData = RelayClientWebSocketData | RelayHostTunnelWebSocketData | RelayUserHostedClientWebSocketData;
|
|
41
|
+
type UpstreamWebSocketConstructor = {
|
|
42
|
+
new (url: string, options: Bun.WebSocketOptions): WebSocket;
|
|
43
|
+
};
|
|
27
44
|
type PendingTunnelHttpResponse = {
|
|
28
45
|
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
29
46
|
stream: ReadableStream<Uint8Array>;
|
|
@@ -41,13 +58,28 @@ export type WorkspaceRelayHostTunnelOptions = {
|
|
|
41
58
|
workspaceIds: string[];
|
|
42
59
|
}) => boolean | Promise<boolean>;
|
|
43
60
|
hostTunnelPingIntervalMs?: number;
|
|
61
|
+
hostTunnelMaxMissedPongs?: number;
|
|
44
62
|
hostTunnelStateDebounceMs?: number;
|
|
45
63
|
};
|
|
46
64
|
export type WorkspaceRelayBackpressureOptions = {
|
|
47
65
|
slowConsumerHighWaterMarkBytes?: number;
|
|
48
66
|
slowConsumerTimeoutMs?: number;
|
|
67
|
+
tunnelRequestBodyMaxBytes?: number;
|
|
68
|
+
tunnelHttpResponseTimeoutMs?: number;
|
|
69
|
+
directHttpTimeoutMs?: number;
|
|
70
|
+
directHttpConcurrency?: number;
|
|
71
|
+
upstreamWebSocketOpenTimeoutMs?: number;
|
|
72
|
+
upstreamWebSocketPreOpenQueueMaxFrames?: number;
|
|
73
|
+
upstreamWebSocket?: UpstreamWebSocketConstructor;
|
|
74
|
+
webSocketBufferedAmountMaxBytes?: number;
|
|
49
75
|
};
|
|
50
76
|
export type WorkspaceRelayBunOptions = WorkspaceRelayHostTunnelOptions & WorkspaceRelayBackpressureOptions;
|
|
77
|
+
export type WorkspaceRelayBunTelemetry = {
|
|
78
|
+
getFragmentationStats(): FragmentationStats;
|
|
79
|
+
resetFragmentationStats(): void;
|
|
80
|
+
getSlowConsumerStats(): SlowConsumerStats;
|
|
81
|
+
resetSlowConsumerStats(): void;
|
|
82
|
+
};
|
|
51
83
|
/**
|
|
52
84
|
* T9: drain controller exposed by `createWorkspaceRelayBun`.
|
|
53
85
|
*
|
|
@@ -56,8 +88,7 @@ export type WorkspaceRelayBunOptions = WorkspaceRelayHostTunnelOptions & Workspa
|
|
|
56
88
|
* - returns 503 from `/health` so Fly removes the instance from routing
|
|
57
89
|
* - rejects new HTTP requests with 503 (`relay_draining`) before doing auth
|
|
58
90
|
* - rejects new host-tunnel WebSocket upgrades with 503
|
|
59
|
-
* -
|
|
60
|
-
* multiplexed WS channels alone so they can complete naturally
|
|
91
|
+
* - closes active host tunnels with 1012 so host clients reconnect promptly
|
|
61
92
|
*
|
|
62
93
|
* `pendingCount()` reports the total number of in-flight tunnel HTTP
|
|
63
94
|
* responses across every connected host tunnel — this is what the operator
|
|
@@ -72,27 +103,27 @@ export type WorkspaceRelayBunDrainController = {
|
|
|
72
103
|
remaining: number;
|
|
73
104
|
}>;
|
|
74
105
|
};
|
|
75
|
-
export
|
|
106
|
+
export type FragmentationStats = {
|
|
76
107
|
fragmentsBuffered: number;
|
|
77
108
|
oversizedClosed: number;
|
|
78
109
|
};
|
|
79
|
-
export
|
|
80
|
-
export declare function getSlowConsumerStats(): {
|
|
110
|
+
export type SlowConsumerStats = {
|
|
81
111
|
overflowEvents: number;
|
|
82
112
|
timerFired: number;
|
|
83
113
|
droppedRequests: number;
|
|
84
114
|
};
|
|
85
|
-
|
|
86
|
-
export declare function __resetHostTunnelStateDebounce(): void;
|
|
115
|
+
declare function createSlowConsumerStats(): SlowConsumerStats;
|
|
87
116
|
declare function enqueueChunkWithBackpressure(input: {
|
|
88
117
|
ws: Bun.ServerWebSocket<RelayHostTunnelWebSocketData>;
|
|
89
118
|
requestId: string;
|
|
90
119
|
entry: PendingTunnelHttpResponse;
|
|
91
120
|
chunk: Uint8Array;
|
|
92
121
|
slowConsumerTimeoutMs: number;
|
|
122
|
+
slowConsumerStats: SlowConsumerStats;
|
|
93
123
|
}): void;
|
|
94
124
|
export declare const __slowConsumerInternalsForTest: {
|
|
95
125
|
enqueueChunkWithBackpressure: typeof enqueueChunkWithBackpressure;
|
|
126
|
+
createSlowConsumerStats: typeof createSlowConsumerStats;
|
|
96
127
|
};
|
|
97
128
|
export declare function createWorkspaceRelayBun(options: WorkspaceRelayOptions, bunOptions?: WorkspaceRelayBunOptions): {
|
|
98
129
|
fetch(request: Request, server: Bun.Server<RelayWebSocketData>): Promise<Response | undefined>;
|
|
@@ -103,5 +134,6 @@ export declare function createWorkspaceRelayBun(options: WorkspaceRelayOptions,
|
|
|
103
134
|
close(ws: Bun.ServerWebSocket<RelayWebSocketData>, code: number, reason: string): void;
|
|
104
135
|
};
|
|
105
136
|
drain: WorkspaceRelayBunDrainController;
|
|
137
|
+
telemetry: WorkspaceRelayBunTelemetry;
|
|
106
138
|
};
|
|
107
139
|
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type AuthorizedWorkspaceRelayRequest, type WorkspaceRelayOptions } from "./server";
|
|
2
|
+
export type WorkspaceRelayDurableObjectId = unknown;
|
|
3
|
+
export type WorkspaceRelayDurableObjectStub = {
|
|
4
|
+
fetch: (request: Request) => Promise<Response>;
|
|
5
|
+
};
|
|
6
|
+
export type WorkspaceRelayDurableObjectGetOptions = {
|
|
7
|
+
locationHint?: string;
|
|
8
|
+
};
|
|
9
|
+
export type WorkspaceRelayDurableObjectNamespace = {
|
|
10
|
+
idFromName: (name: string) => WorkspaceRelayDurableObjectId;
|
|
11
|
+
get: (id: WorkspaceRelayDurableObjectId, options?: WorkspaceRelayDurableObjectGetOptions) => WorkspaceRelayDurableObjectStub;
|
|
12
|
+
};
|
|
13
|
+
export declare const DEFAULT_RELAY_LOCATION_HINT = "apac";
|
|
14
|
+
export type WorkspaceRelayDurableObjectEnv = Record<string, unknown>;
|
|
15
|
+
export type WorkspaceRelayDurableObjectGatewayOptions = {
|
|
16
|
+
bindingName?: string;
|
|
17
|
+
namespace?: WorkspaceRelayDurableObjectNamespace;
|
|
18
|
+
};
|
|
19
|
+
export type WorkspaceRelayDurableObjectSocketEvent = {
|
|
20
|
+
data?: unknown;
|
|
21
|
+
code?: number;
|
|
22
|
+
reason?: string;
|
|
23
|
+
};
|
|
24
|
+
export type WorkspaceRelayDurableObjectSocket = {
|
|
25
|
+
accept?: () => void;
|
|
26
|
+
readyState?: number;
|
|
27
|
+
binaryType?: string;
|
|
28
|
+
send?: (message: string | ArrayBuffer | Uint8Array) => void;
|
|
29
|
+
addEventListener?: (type: "open" | "message" | "close" | "error", listener: (event?: WorkspaceRelayDurableObjectSocketEvent) => void) => void;
|
|
30
|
+
close?: (code?: number, reason?: string) => void;
|
|
31
|
+
serializeAttachment?: (attachment: WorkspaceRelaySocketAttachment) => void;
|
|
32
|
+
deserializeAttachment?: () => WorkspaceRelaySocketAttachment | undefined;
|
|
33
|
+
};
|
|
34
|
+
export type WorkspaceRelayDurableObjectSocketPair = {
|
|
35
|
+
client: WorkspaceRelayDurableObjectSocket;
|
|
36
|
+
server: WorkspaceRelayDurableObjectSocket;
|
|
37
|
+
};
|
|
38
|
+
export type WorkspaceRelayDurableObjectConnectWebSocket = (url: string, init: {
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
}) => WorkspaceRelayDurableObjectSocket | Promise<WorkspaceRelayDurableObjectSocket>;
|
|
41
|
+
export type WorkspaceRelayDurableObjectRoomOptions = WorkspaceRelayOptions & {
|
|
42
|
+
createWebSocketPair?: () => WorkspaceRelayDurableObjectSocketPair;
|
|
43
|
+
connectWebSocket?: WorkspaceRelayDurableObjectConnectWebSocket;
|
|
44
|
+
hibernation?: WorkspaceRelayDurableObjectHibernation;
|
|
45
|
+
upstreamWebSocketPreOpenQueueMaxFrames?: number;
|
|
46
|
+
tunnelPendingHttpCap?: number;
|
|
47
|
+
tunnelStartedStreamCap?: number;
|
|
48
|
+
tunnelChannelCap?: number;
|
|
49
|
+
tunnelRequestBodyMaxBytes?: number;
|
|
50
|
+
tunnelResponseBodyMaxBytes?: number;
|
|
51
|
+
slowConsumerHighWaterMarkBytes?: number;
|
|
52
|
+
slowConsumerTimeoutMs?: number;
|
|
53
|
+
runtimeAccessTokenActiveCheckIntervalMs?: number;
|
|
54
|
+
workspaceTargetActiveCheckIntervalMs?: number;
|
|
55
|
+
traceSampleRate?: number;
|
|
56
|
+
traceLog?: (event: WorkspaceRelayTraceLogEvent) => void;
|
|
57
|
+
now?: () => number;
|
|
58
|
+
};
|
|
59
|
+
export type WorkspaceRelayTraceLogEvent = {
|
|
60
|
+
event: "workspace_relay_request";
|
|
61
|
+
traceId: string;
|
|
62
|
+
workspaceId?: string;
|
|
63
|
+
routeKind: string;
|
|
64
|
+
method: string;
|
|
65
|
+
path: string;
|
|
66
|
+
status: number;
|
|
67
|
+
totalMs: number;
|
|
68
|
+
phases: Record<string, number>;
|
|
69
|
+
};
|
|
70
|
+
export type WorkspaceRelayDurableObjectHibernation = {
|
|
71
|
+
acceptWebSocket: (socket: WorkspaceRelayDurableObjectSocket) => void;
|
|
72
|
+
getWebSockets: () => WorkspaceRelayDurableObjectSocket[];
|
|
73
|
+
};
|
|
74
|
+
export type WorkspaceRelaySocketAttachment = {
|
|
75
|
+
kind: "host-tunnel";
|
|
76
|
+
hostId: string;
|
|
77
|
+
workspaceIds: string[];
|
|
78
|
+
connectedAt: number;
|
|
79
|
+
} | {
|
|
80
|
+
kind: "user-hosted-client";
|
|
81
|
+
channelId: string;
|
|
82
|
+
claims: RuntimeAccessAttachmentClaims;
|
|
83
|
+
target: WorkspaceRelayTargetAttachment;
|
|
84
|
+
path: string;
|
|
85
|
+
connectedAt: number;
|
|
86
|
+
};
|
|
87
|
+
type RuntimeAccessAttachmentClaims = AuthorizedWorkspaceRelayRequest["claims"];
|
|
88
|
+
type WorkspaceRelayTargetAttachment = AuthorizedWorkspaceRelayRequest["target"];
|
|
89
|
+
export type WorkspaceRelayDurableObjectDrainController = {
|
|
90
|
+
isDraining(): boolean;
|
|
91
|
+
setDraining(value: boolean): void;
|
|
92
|
+
pendingCount(): number;
|
|
93
|
+
waitForDrain(timeoutMs: number): Promise<{
|
|
94
|
+
drained: boolean;
|
|
95
|
+
remaining: number;
|
|
96
|
+
}>;
|
|
97
|
+
};
|
|
98
|
+
export declare function workspaceRelayDurableObjectRoomName(workspaceId: string): string;
|
|
99
|
+
export declare function workspaceRelayDurableObjectWorkspaceId(request: Request): string | undefined;
|
|
100
|
+
export declare function setWorkspaceRelayAppOrigins(raw: string | undefined): void;
|
|
101
|
+
export declare function createWorkspaceRelayDurableObjectGateway(options?: WorkspaceRelayDurableObjectGatewayOptions): {
|
|
102
|
+
fetch(request: Request, env?: WorkspaceRelayDurableObjectEnv): Promise<Response>;
|
|
103
|
+
};
|
|
104
|
+
export declare function createWorkspaceRelayDurableObjectRoom(options: WorkspaceRelayDurableObjectRoomOptions): {
|
|
105
|
+
fetch(request: Request): Promise<Response>;
|
|
106
|
+
webSocketMessage(socket: WorkspaceRelayDurableObjectSocket, message: string | ArrayBuffer): Promise<void>;
|
|
107
|
+
webSocketClose(socket: WorkspaceRelayDurableObjectSocket, code: number, reason: string): void;
|
|
108
|
+
webSocketError(socket: WorkspaceRelayDurableObjectSocket): void;
|
|
109
|
+
state(): {
|
|
110
|
+
hostTunnelCount: number;
|
|
111
|
+
clientCount: number;
|
|
112
|
+
hostIds: string[];
|
|
113
|
+
};
|
|
114
|
+
drain: WorkspaceRelayDurableObjectDrainController;
|
|
115
|
+
};
|
|
116
|
+
export {};
|