@claxedo/workspace-relay 0.1.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/README.md +135 -0
- package/dist/auth.d.ts +107 -0
- package/dist/bun.d.ts +107 -0
- package/dist/directory.d.ts +55 -0
- package/dist/http.d.ts +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +1563 -0
- package/dist/main.d.ts +160 -0
- package/dist/main.mjs +1933 -0
- package/dist/server.d.ts +226 -0
- package/dist/synthetic.d.ts +60 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# `@claxedo/workspace-relay`
|
|
2
|
+
|
|
3
|
+
The Claxedo workspace relay — a single canonical bidirectional tunnel
|
|
4
|
+
between cloud-hosted browsers and workspace-runtime hosts (whether the
|
|
5
|
+
host is a cloud VM or a user laptop). One package, one process, one
|
|
6
|
+
config surface — there is no separate `dev-relay` codepath.
|
|
7
|
+
|
|
8
|
+
## Why one package, no parallel impl
|
|
9
|
+
|
|
10
|
+
In the fa9cabf9 design pass the working assumption was clarified:
|
|
11
|
+
|
|
12
|
+
> "I think this creates a parallel implementation problem — why is there
|
|
13
|
+
> no devmode in the prod relay?"
|
|
14
|
+
|
|
15
|
+
So this package is the relay. Local-dev configuration is selected by
|
|
16
|
+
environment variables, not by a sibling `dev-relay.ts` module. If a
|
|
17
|
+
future cycle re-introduces a separate `dev-relay.*` file in
|
|
18
|
+
`packages/claxedo-server/`, that should be flagged as a regression.
|
|
19
|
+
|
|
20
|
+
## Public surface
|
|
21
|
+
|
|
22
|
+
Re-exported from [`src/index.ts`](src/index.ts):
|
|
23
|
+
|
|
24
|
+
| Concern | Module | Notable exports |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| 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) | `createWorkspaceRelayServer`, types `WorkspaceRelayOptions`, `WorkspaceRelayTarget`, `RuntimeAccessTokenActiveResult`, `WorkspaceRelayAuditEvent`, `WorkspaceRelayMetricsSources`, `RelayHostPublicKey` |
|
|
28
|
+
| Active-host directory | [`src/directory.ts`](src/directory.ts) | `createWorkspaceRelayDirectory`, `disposeWorkspaceRelayDirectory`, types `WorkspaceRelayDirectory`, `HostTunnelPresence` |
|
|
29
|
+
| Bun-specific server bootstrap | [`src/bun.ts`](src/bun.ts) | `createWorkspaceRelayBun`, type `WorkspaceRelayBunOptions`, `WorkspaceRelayBunDrainController`, metrics `getFragmentationStats`, `getSlowConsumerStats` |
|
|
30
|
+
|
|
31
|
+
Wire types live in the sibling package
|
|
32
|
+
[`@claxedo/workspace-relay-protocol`](../workspace-relay-protocol/)
|
|
33
|
+
(`TUNNEL_PROTOCOL_VERSION`, `TunnelMessage`, `isTunnelMessage`,
|
|
34
|
+
`makeTunnelPong`). Keep that split — it lets non-Node consumers
|
|
35
|
+
implement the tunnel protocol without pulling Hono and Jose.
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
All knobs are environment variables. The relay refuses to boot in
|
|
40
|
+
production if `CLAXEDO_RELAY_RESOLVER_TOKEN` is missing — the
|
|
41
|
+
production fail-closed gate at `src/main.ts`.
|
|
42
|
+
|
|
43
|
+
| Env var | Purpose |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `CLAXEDO_RELAY_BIND_HOST`, `CLAXEDO_RELAY_BIND_PORT` | Listening socket. Loopback by default. |
|
|
46
|
+
| `CLAXEDO_RELAY_PUBLIC_URL` | Externally-resolvable URL the relay advertises in tokens. |
|
|
47
|
+
| `CLAXEDO_RELAY_RESOLVER_URL` | Where to fetch `RuntimeAccessTokenActiveResult`s (typically `claxedo-server/internal/relay/target`). |
|
|
48
|
+
| `CLAXEDO_RELAY_RESOLVER_TOKEN` | Bearer token the relay sends to the resolver. **Required in production.** |
|
|
49
|
+
| `CLAXEDO_RELAY_JWKS_URL` | Optional remote JWKS the relay uses to verify runtime-access tokens. |
|
|
50
|
+
| `CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM` | Inline public key (alternative to JWKS). |
|
|
51
|
+
| `CLAXEDO_RELAY_HOST_VERIFY_PEM` | Public PEM the relay uses to verify host-tunnel tokens. |
|
|
52
|
+
| `CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM` | Private PEM the relay uses to mint relay-host tokens. |
|
|
53
|
+
| `NODE_ENV` | `production` / `development` / `test`. Switches the production fail-closed gate. |
|
|
54
|
+
|
|
55
|
+
Token verification is pluggable today via the
|
|
56
|
+
`RelayKey | RelayKeyResolver` pair on `verifyRuntimeAccessToken` and
|
|
57
|
+
friends, **and** via the unified
|
|
58
|
+
[`TokenVerifier`](../workspace-relay-protocol/src/token-verifier.ts)
|
|
59
|
+
interface in `@claxedo/workspace-relay-protocol`. To swap the relay's
|
|
60
|
+
auth backend in a self-hosted deployment:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { createStaticTokenVerifier } from "@claxedo/workspace-relay-protocol"
|
|
64
|
+
|
|
65
|
+
const verifier = createStaticTokenVerifier({
|
|
66
|
+
tokens: {
|
|
67
|
+
"tok-tenant-1": { subject: "u1", scopes: ["workspace:write"], claims: {} },
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
// Pass `verifier` into a future `WorkspaceRelayOptions.tokenVerifier`
|
|
71
|
+
// option when the relay accepts custom verification backends directly.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The `TokenVerifier` interface intentionally contains only `verify(token)`.
|
|
75
|
+
JWKS fetching, audience binding, and replay caches belong to the
|
|
76
|
+
implementation. Two reference implementations ship in the protocol
|
|
77
|
+
package: `createHttpTokenVerifier` for remote verifiers
|
|
78
|
+
(Clerk-style introspection, custom OIDC), and `createStaticTokenVerifier`
|
|
79
|
+
for tests and self-hosted single-tenant setups.
|
|
80
|
+
|
|
81
|
+
## Routing
|
|
82
|
+
|
|
83
|
+
The relay has **no** `/w/{workspaceId}/*` URL prefix of its own. The
|
|
84
|
+
gateway pattern lives in `claxedo-server`:
|
|
85
|
+
|
|
86
|
+
- The user's browser calls `/api/claxedo/...`.
|
|
87
|
+
- `claxedo-server` mounts `workspaceRuntimeProxy` middleware
|
|
88
|
+
(`packages/claxedo-server/src/proxy.ts:205`) on its top-level Hono
|
|
89
|
+
app at `packages/claxedo-server/src/server.ts:93`.
|
|
90
|
+
- The proxy resolves the active workspace target via
|
|
91
|
+
`internal-relay.ts` (control-plane auth — see "Seam: internal-relay
|
|
92
|
+
vs workspace-relay" below), strips its own prefix, and forwards
|
|
93
|
+
to the relay's tunnel endpoint.
|
|
94
|
+
|
|
95
|
+
If you are adding a new HTTP-level workspace surface, attach it to
|
|
96
|
+
`claxedo-server`'s gateway, not to `workspace-relay`. The relay is
|
|
97
|
+
intentionally narrow — it only handles tunnel traffic and the auth
|
|
98
|
+
checks that gate it.
|
|
99
|
+
|
|
100
|
+
## Seam: `internal-relay` vs `workspace-relay`
|
|
101
|
+
|
|
102
|
+
| | `claxedo-server/src/routes/internal-relay.ts` | `@claxedo/workspace-relay` |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| Layer | Control-plane auth | Tunnel transport |
|
|
105
|
+
| Trust | Authenticates the relay process itself (loopback or bearer) | Authenticates the user's runtime-access token |
|
|
106
|
+
| Routes | `GET /internal/relay/target`, `GET /internal/relay/revocation` | WS upgrade + tunnel framing |
|
|
107
|
+
| Owners | `claxedo-server` (depends on Convex authority, Clerk, audit log) | `workspace-relay` (no Convex/Clerk dependency) |
|
|
108
|
+
| Lives in | `packages/claxedo-server/` (server-side only) | `packages/workspace-relay/` (own process) |
|
|
109
|
+
|
|
110
|
+
This split is deliberate: the relay never reads Convex or Clerk
|
|
111
|
+
directly. It calls back to `claxedo-server` over HTTP for resolver
|
|
112
|
+
decisions. The decision data crosses the seam as
|
|
113
|
+
`RuntimeAccessTokenActiveResult` (defined in `server.ts`).
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
bun --cwd packages/workspace-relay dev # hot-reload main.ts
|
|
119
|
+
bun --cwd packages/workspace-relay test # run all *.test.ts
|
|
120
|
+
bun --cwd packages/workspace-relay typecheck
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The TS error baseline for this package is **0** — keep it that way.
|
|
124
|
+
|
|
125
|
+
## Boundary rule
|
|
126
|
+
|
|
127
|
+
Consumers outside this package import `@claxedo/workspace-relay` only —
|
|
128
|
+
not `@claxedo/workspace-relay/src/...`. Direct deep-source imports
|
|
129
|
+
break the package boundary. The check is
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
grep -rn "workspace-relay/src/" packages/claxedo-{server,app}/src
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
— required to return zero hits.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type JWTVerifyGetKey } from "jose";
|
|
2
|
+
declare const algorithms: readonly ["EdDSA", "ES256", "RS256"];
|
|
3
|
+
export declare const runtimeAccessTokenIssuer = "claxedo-control-plane";
|
|
4
|
+
export declare const runtimeAccessTokenAudience = "workspace-relay";
|
|
5
|
+
export declare const hostTunnelTokenAudience = "workspace-relay-host-tunnel";
|
|
6
|
+
export declare const relayHostTokenIssuer = "workspace-relay";
|
|
7
|
+
export declare const relayHostTokenAudience = "workspace-host-service";
|
|
8
|
+
export type RelayAccess = "cloud" | "user-hosted";
|
|
9
|
+
export type RelayBacking = "local-worktree" | "cloud-vm";
|
|
10
|
+
export type RelayJwtAlgorithm = (typeof algorithms)[number];
|
|
11
|
+
export type RelayRole = "viewer" | "editor" | "admin" | "owner";
|
|
12
|
+
export type RuntimeAccessTokenClaims = {
|
|
13
|
+
iss: typeof runtimeAccessTokenIssuer;
|
|
14
|
+
aud: typeof runtimeAccessTokenAudience;
|
|
15
|
+
sub: string;
|
|
16
|
+
org_id: string;
|
|
17
|
+
workspace_id: string;
|
|
18
|
+
host_id: string;
|
|
19
|
+
role: RelayRole;
|
|
20
|
+
exp: number;
|
|
21
|
+
iat: number;
|
|
22
|
+
jti: string;
|
|
23
|
+
};
|
|
24
|
+
export type RelayHostTokenClaims = {
|
|
25
|
+
iss: typeof relayHostTokenIssuer;
|
|
26
|
+
aud: typeof relayHostTokenAudience;
|
|
27
|
+
sub: string;
|
|
28
|
+
org_id: string;
|
|
29
|
+
workspace_id: string;
|
|
30
|
+
host_id: string;
|
|
31
|
+
access: RelayAccess;
|
|
32
|
+
backing: RelayBacking;
|
|
33
|
+
exp: number;
|
|
34
|
+
iat: number;
|
|
35
|
+
jti: string;
|
|
36
|
+
};
|
|
37
|
+
export type HostTunnelTokenClaims = {
|
|
38
|
+
iss: typeof runtimeAccessTokenIssuer;
|
|
39
|
+
aud: typeof hostTunnelTokenAudience;
|
|
40
|
+
sub: string;
|
|
41
|
+
host_id: string;
|
|
42
|
+
workspace_ids: string[];
|
|
43
|
+
exp: number;
|
|
44
|
+
iat: number;
|
|
45
|
+
jti: string;
|
|
46
|
+
};
|
|
47
|
+
export declare class WorkspaceRelayAuthError extends Error {
|
|
48
|
+
readonly code: "invalid_relay_token" | "relay_token_workspace_mismatch" | "relay_token_host_mismatch" | "relay_token_claims_invalid";
|
|
49
|
+
constructor(code: "invalid_relay_token" | "relay_token_workspace_mismatch" | "relay_token_host_mismatch" | "relay_token_claims_invalid", message: string);
|
|
50
|
+
}
|
|
51
|
+
type RuntimeInput = {
|
|
52
|
+
subject: string;
|
|
53
|
+
orgId: string;
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
hostId: string;
|
|
56
|
+
role: RelayRole;
|
|
57
|
+
ttlSeconds?: number;
|
|
58
|
+
jti?: string;
|
|
59
|
+
now?: number;
|
|
60
|
+
};
|
|
61
|
+
type RelayHostInput = RuntimeInput & {
|
|
62
|
+
access: RelayAccess;
|
|
63
|
+
backing: RelayBacking;
|
|
64
|
+
/**
|
|
65
|
+
* Optional `kid` to embed in the JWT protected header. Verifiers using a
|
|
66
|
+
* JWKS resolver dispatch on this to pick the matching key, so a freshly
|
|
67
|
+
* minted RHT must carry the same `kid` that the relay publishes at
|
|
68
|
+
* `/.well-known/jwks.json`. When omitted, the header has no `kid` and
|
|
69
|
+
* verifiers fall back to single-key matching (PEM env-var path).
|
|
70
|
+
*/
|
|
71
|
+
kid?: string;
|
|
72
|
+
};
|
|
73
|
+
type HostTunnelInput = {
|
|
74
|
+
subject: string;
|
|
75
|
+
hostId: string;
|
|
76
|
+
workspaceIds: string[];
|
|
77
|
+
ttlSeconds?: number;
|
|
78
|
+
jti?: string;
|
|
79
|
+
now?: number;
|
|
80
|
+
};
|
|
81
|
+
type ExpectedTarget = {
|
|
82
|
+
workspaceId: string;
|
|
83
|
+
hostId?: string;
|
|
84
|
+
};
|
|
85
|
+
type ExpectedHostTunnel = {
|
|
86
|
+
hostId: string;
|
|
87
|
+
workspaceIds: string[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Resolver function returned by `createRemoteJWKSet` / `createLocalJWKSet`.
|
|
91
|
+
* Accepted by `jwtVerify` directly — jose dispatches based on whether the
|
|
92
|
+
* argument is callable.
|
|
93
|
+
*/
|
|
94
|
+
export type RelayKeyResolver = JWTVerifyGetKey;
|
|
95
|
+
export type RelayKey = CryptoKey | Uint8Array | RelayKeyResolver;
|
|
96
|
+
/**
|
|
97
|
+
* Mint variants accept only a signing key — never a resolver. Resolvers are
|
|
98
|
+
* verification-only. This narrowed alias keeps the mint signatures unchanged.
|
|
99
|
+
*/
|
|
100
|
+
type RelaySigningKey = CryptoKey | Uint8Array;
|
|
101
|
+
export declare function mintRuntimeAccessToken(input: RuntimeInput, key: RelaySigningKey, alg: RelayJwtAlgorithm): Promise<string>;
|
|
102
|
+
export declare function verifyRuntimeAccessToken(token: string, key: RelayKey, expected: ExpectedTarget): Promise<RuntimeAccessTokenClaims>;
|
|
103
|
+
export declare function mintHostTunnelToken(input: HostTunnelInput, key: RelaySigningKey, alg: RelayJwtAlgorithm): Promise<string>;
|
|
104
|
+
export declare function verifyHostTunnelToken(token: string, key: RelayKey, expected: ExpectedHostTunnel): Promise<HostTunnelTokenClaims>;
|
|
105
|
+
export declare function mintRelayHostToken(input: RelayHostInput, key: RelaySigningKey, alg: RelayJwtAlgorithm): Promise<string>;
|
|
106
|
+
export declare function verifyRelayHostToken(token: string, key: RelayKey, expected: ExpectedTarget): Promise<RelayHostTokenClaims>;
|
|
107
|
+
export {};
|
package/dist/bun.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type WorkspaceRelayOptions } from "./server";
|
|
2
|
+
type RelayClientWebSocketData = {
|
|
3
|
+
kind: "client";
|
|
4
|
+
upstreamUrl: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
queue: Array<string | Buffer<ArrayBuffer>>;
|
|
7
|
+
upstream?: WebSocket;
|
|
8
|
+
};
|
|
9
|
+
type RelayHostTunnelWebSocketData = {
|
|
10
|
+
kind: "host-tunnel";
|
|
11
|
+
hostId: string;
|
|
12
|
+
workspaceIds: string[];
|
|
13
|
+
pending: Map<string, PendingTunnelHttpResponse>;
|
|
14
|
+
channels: Map<string, Bun.ServerWebSocket<RelayUserHostedClientWebSocketData>>;
|
|
15
|
+
heartbeat?: ReturnType<typeof setInterval>;
|
|
16
|
+
messageBuffer: string;
|
|
17
|
+
};
|
|
18
|
+
type RelayUserHostedClientWebSocketData = {
|
|
19
|
+
kind: "user-hosted-client";
|
|
20
|
+
hostId: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
channelId: string;
|
|
23
|
+
path: string;
|
|
24
|
+
relayHostToken: string;
|
|
25
|
+
};
|
|
26
|
+
type RelayWebSocketData = RelayClientWebSocketData | RelayHostTunnelWebSocketData | RelayUserHostedClientWebSocketData;
|
|
27
|
+
type PendingTunnelHttpResponse = {
|
|
28
|
+
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
29
|
+
stream: ReadableStream<Uint8Array>;
|
|
30
|
+
resolve: (response: Response) => void;
|
|
31
|
+
reject: (error: Error) => void;
|
|
32
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
33
|
+
pendingChunks: Uint8Array[];
|
|
34
|
+
bytesQueued: number;
|
|
35
|
+
slowConsumerTimeout?: ReturnType<typeof setTimeout>;
|
|
36
|
+
responseStarted: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type WorkspaceRelayHostTunnelOptions = {
|
|
39
|
+
authorizeHostTunnel?: (request: Request, input: {
|
|
40
|
+
hostId: string;
|
|
41
|
+
workspaceIds: string[];
|
|
42
|
+
}) => boolean | Promise<boolean>;
|
|
43
|
+
hostTunnelPingIntervalMs?: number;
|
|
44
|
+
hostTunnelStateDebounceMs?: number;
|
|
45
|
+
};
|
|
46
|
+
export type WorkspaceRelayBackpressureOptions = {
|
|
47
|
+
slowConsumerHighWaterMarkBytes?: number;
|
|
48
|
+
slowConsumerTimeoutMs?: number;
|
|
49
|
+
};
|
|
50
|
+
export type WorkspaceRelayBunOptions = WorkspaceRelayHostTunnelOptions & WorkspaceRelayBackpressureOptions;
|
|
51
|
+
/**
|
|
52
|
+
* T9: drain controller exposed by `createWorkspaceRelayBun`.
|
|
53
|
+
*
|
|
54
|
+
* On SIGTERM the operator (see `installShutdownDrainHandler` in main.ts)
|
|
55
|
+
* flips `setDraining(true)`; from that point onward the relay
|
|
56
|
+
* - returns 503 from `/health` so Fly removes the instance from routing
|
|
57
|
+
* - rejects new HTTP requests with 503 (`relay_draining`) before doing auth
|
|
58
|
+
* - rejects new host-tunnel WebSocket upgrades with 503
|
|
59
|
+
* - leaves all existing in-flight HTTP requests, host tunnels, and
|
|
60
|
+
* multiplexed WS channels alone so they can complete naturally
|
|
61
|
+
*
|
|
62
|
+
* `pendingCount()` reports the total number of in-flight tunnel HTTP
|
|
63
|
+
* responses across every connected host tunnel — this is what the operator
|
|
64
|
+
* polls via `waitForDrain(timeoutMs)` before hard-closing remaining sockets.
|
|
65
|
+
*/
|
|
66
|
+
export type WorkspaceRelayBunDrainController = {
|
|
67
|
+
isDraining(): boolean;
|
|
68
|
+
setDraining(value: boolean): void;
|
|
69
|
+
pendingCount(): number;
|
|
70
|
+
waitForDrain(timeoutMs: number): Promise<{
|
|
71
|
+
drained: boolean;
|
|
72
|
+
remaining: number;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
export declare function getFragmentationStats(): {
|
|
76
|
+
fragmentsBuffered: number;
|
|
77
|
+
oversizedClosed: number;
|
|
78
|
+
};
|
|
79
|
+
export declare function resetFragmentationStats(): void;
|
|
80
|
+
export declare function getSlowConsumerStats(): {
|
|
81
|
+
overflowEvents: number;
|
|
82
|
+
timerFired: number;
|
|
83
|
+
droppedRequests: number;
|
|
84
|
+
};
|
|
85
|
+
export declare function resetSlowConsumerStats(): void;
|
|
86
|
+
export declare function __resetHostTunnelStateDebounce(): void;
|
|
87
|
+
declare function enqueueChunkWithBackpressure(input: {
|
|
88
|
+
ws: Bun.ServerWebSocket<RelayHostTunnelWebSocketData>;
|
|
89
|
+
requestId: string;
|
|
90
|
+
entry: PendingTunnelHttpResponse;
|
|
91
|
+
chunk: Uint8Array;
|
|
92
|
+
slowConsumerTimeoutMs: number;
|
|
93
|
+
}): void;
|
|
94
|
+
export declare const __slowConsumerInternalsForTest: {
|
|
95
|
+
enqueueChunkWithBackpressure: typeof enqueueChunkWithBackpressure;
|
|
96
|
+
};
|
|
97
|
+
export declare function createWorkspaceRelayBun(options: WorkspaceRelayOptions, bunOptions?: WorkspaceRelayBunOptions): {
|
|
98
|
+
fetch(request: Request, server: Bun.Server<RelayWebSocketData>): Promise<Response | undefined>;
|
|
99
|
+
websocket: {
|
|
100
|
+
maxPayloadLength: number;
|
|
101
|
+
message(ws: Bun.ServerWebSocket<RelayWebSocketData>, message: string | Buffer<ArrayBuffer>): void;
|
|
102
|
+
open(ws: Bun.ServerWebSocket<RelayWebSocketData>): void;
|
|
103
|
+
close(ws: Bun.ServerWebSocket<RelayWebSocketData>, code: number, reason: string): void;
|
|
104
|
+
};
|
|
105
|
+
drain: WorkspaceRelayBunDrainController;
|
|
106
|
+
};
|
|
107
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type HostTunnelPresence = {
|
|
2
|
+
hostId: string;
|
|
3
|
+
workspaceIds: string[];
|
|
4
|
+
connectedAt: number;
|
|
5
|
+
lastPongAt: number;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
};
|
|
8
|
+
export type WorkspaceRelayDirectory = {
|
|
9
|
+
registerHostTunnel(input: {
|
|
10
|
+
hostId: string;
|
|
11
|
+
workspaceIds: string[];
|
|
12
|
+
}): HostTunnelPresence;
|
|
13
|
+
recordPong(hostId: string): HostTunnelPresence | undefined;
|
|
14
|
+
disconnectHost(hostId: string): void;
|
|
15
|
+
activeHost(input: {
|
|
16
|
+
hostId: string;
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
}): HostTunnelPresence | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Manually run an expiry sweep over the presence map. Removes any entry
|
|
21
|
+
* whose `expiresAt <= now()`. Safe to call on a fresh/empty directory.
|
|
22
|
+
* Useful in tests; the production path runs this on a timer (see
|
|
23
|
+
* `sweepIntervalMs`).
|
|
24
|
+
*/
|
|
25
|
+
sweep(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Stop the active sweep timer (if one was started). Safe to call multiple
|
|
28
|
+
* times and on directories that were created with `sweepIntervalMs: 0`.
|
|
29
|
+
*/
|
|
30
|
+
dispose(): void;
|
|
31
|
+
/**
|
|
32
|
+
* T31: count of currently-active (non-expired) host presence entries.
|
|
33
|
+
* Equivalent to running `sweep()` then returning the residual map size, but
|
|
34
|
+
* implemented as a single pass without mutating eviction state when the
|
|
35
|
+
* caller only wants a snapshot. Surfaced via the `/metrics` endpoint as
|
|
36
|
+
* `directory.activeHostCount`.
|
|
37
|
+
*/
|
|
38
|
+
size(): number;
|
|
39
|
+
};
|
|
40
|
+
export declare function createWorkspaceRelayDirectory(options?: {
|
|
41
|
+
ttlMs?: number;
|
|
42
|
+
now?: () => number;
|
|
43
|
+
/**
|
|
44
|
+
* Interval in milliseconds at which a background sweep runs to evict
|
|
45
|
+
* expired host presence entries. Defaults to 30_000. Set to `0` to disable
|
|
46
|
+
* the timer entirely (useful for tests; callers can still drive cleanup
|
|
47
|
+
* manually via `sweep()`).
|
|
48
|
+
*/
|
|
49
|
+
sweepIntervalMs?: number;
|
|
50
|
+
}): WorkspaceRelayDirectory;
|
|
51
|
+
/**
|
|
52
|
+
* Convenience companion to `createWorkspaceRelayDirectory` for callers that
|
|
53
|
+
* prefer a free function over a method. Equivalent to `directory.dispose()`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function disposeWorkspaceRelayDirectory(directory: WorkspaceRelayDirectory): void;
|
package/dist/http.d.ts
ADDED
package/dist/index.d.ts
ADDED