@claxedo/workspace-relay 0.7.0 → 0.10.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.
@@ -1,54 +1,17 @@
1
1
  import { type AuthorizedWorkspaceRelayRequest, type WorkspaceRelayOptions } from "./server";
2
+ import { type SessionHostNamespace } from "./session-host-forwarding";
2
3
  export type WorkspaceRelayDurableObjectId = unknown;
3
4
  export type WorkspaceRelayDurableObjectStub = {
4
5
  fetch: (request: Request) => Promise<Response>;
5
6
  };
6
7
  export type WorkspaceRelayDurableObjectGetOptions = {
8
+ /** Cloudflare's placement hint, honoured on the room's first creation only: beside the workspace's runtime (`workspaceRelayRoomPlacement`). */
7
9
  locationHint?: string;
8
10
  };
9
11
  export type WorkspaceRelayDurableObjectNamespace = {
10
12
  idFromName: (name: string) => WorkspaceRelayDurableObjectId;
11
13
  get: (id: WorkspaceRelayDurableObjectId, options?: WorkspaceRelayDurableObjectGetOptions) => WorkspaceRelayDurableObjectStub;
12
14
  };
13
- export declare const DEFAULT_RELAY_LOCATION_HINT = "apac";
14
- /**
15
- * The complete set of Cloudflare Durable Object location hints.
16
- *
17
- * Cloudflare rejects an unknown hint, and a rejected `get()` fails the request —
18
- * so an unrecognised region must fall back rather than be passed through. This
19
- * list is the validator for that.
20
- */
21
- export declare const RELAY_LOCATION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
22
- export type RelayLocationHint = typeof RELAY_LOCATION_HINTS[number];
23
- /** A workspace region carried on the request, if the caller supplied one. */
24
- export declare const RELAY_REGION_HEADER = "x-claxedo-workspace-region";
25
- /**
26
- * Chooses the location hint for a workspace's Durable Object.
27
- *
28
- * This decision is PERMANENT: a DO's location is fixed at first creation and
29
- * never migrates, so a workspace created with the wrong hint pays the extra
30
- * ocean crossing on every frame for its entire life. That is why an
31
- * unrecognised value falls back to the configured default instead of being
32
- * passed through or guessed at.
33
- *
34
- * Precedence, most authoritative first:
35
- * 1. the workspace's own region (header or `?region=`), which is what the
36
- * control plane knows and the only signal tied to the WORKSPACE rather than
37
- * to whoever happens to be connecting first;
38
- * 2. the requesting user's Cloudflare edge country, a decent proxy when the
39
- * workspace has no region yet;
40
- * 3. the deployment-wide configured hint (`CLAXEDO_RELAY_LOCATION_HINT`), then
41
- * `DEFAULT_RELAY_LOCATION_HINT`.
42
- */
43
- export declare function relayLocationHint(input: {
44
- configured?: string;
45
- region?: string;
46
- country?: string;
47
- }): string;
48
- /** The workspace region a request declares, if any. */
49
- export declare function relayRequestRegion(request: Request): string | undefined;
50
- /** The Cloudflare edge country for the request, when running on Cloudflare. */
51
- export declare function relayRequestCountry(request: Request): string | undefined;
52
15
  export type WorkspaceRelayDurableObjectEnv = Record<string, unknown>;
53
16
  export type WorkspaceRelayDurableObjectGatewayOptions = {
54
17
  bindingName?: string;
@@ -77,6 +40,7 @@ export type WorkspaceRelayDurableObjectConnectWebSocket = (url: string, init: {
77
40
  headers: Record<string, string>;
78
41
  }) => WorkspaceRelayDurableObjectSocket | Promise<WorkspaceRelayDurableObjectSocket>;
79
42
  export type WorkspaceRelayDurableObjectRoomOptions = WorkspaceRelayOptions & {
43
+ sessionHost?: SessionHostNamespace;
80
44
  createWebSocketPair?: () => WorkspaceRelayDurableObjectSocketPair;
81
45
  connectWebSocket?: WorkspaceRelayDurableObjectConnectWebSocket;
82
46
  hibernation?: WorkspaceRelayDurableObjectHibernation;
@@ -89,6 +53,15 @@ export type WorkspaceRelayDurableObjectRoomOptions = WorkspaceRelayOptions & {
89
53
  tunnelResponseBodyMaxBytes?: number;
90
54
  slowConsumerHighWaterMarkBytes?: number;
91
55
  slowConsumerTimeoutMs?: number;
56
+ /**
57
+ * How often established client WebSockets are re-checked for revocation.
58
+ * Defaults to 30s; 0 disables the re-check. Because the check may answer
59
+ * from the revocation lookup's cache, a revoked token closes the socket
60
+ * within this interval plus the lookup's cache TTL — see
61
+ * `runtimeAccessTokenRevocationDelayMs` in `./server`. A resolver outage
62
+ * gets `resolverOutageGraceAttempts` and never extends the socket past the
63
+ * token's `exp`, which the watcher enforces locally.
64
+ */
92
65
  runtimeAccessTokenActiveCheckIntervalMs?: number;
93
66
  workspaceTargetActiveCheckIntervalMs?: number;
94
67
  /**
@@ -100,8 +73,16 @@ export type WorkspaceRelayDurableObjectRoomOptions = WorkspaceRelayOptions & {
100
73
  /**
101
74
  * Interval for the alarm-based revocation re-check that covers hibernated
102
75
  * connections, where `setInterval` watchers cannot survive. 0 disables it.
76
+ * The hibernated host-generation check rides the same alarm.
103
77
  */
104
78
  hibernatedRevocationCheckIntervalMs?: number;
79
+ /**
80
+ * How often an established host tunnel's serving generation is re-checked
81
+ * against `resolveHostGeneration` on the non-hibernating path. Defaults to
82
+ * 30s; 0 disables it. The lookup may answer from its cache, so a superseded
83
+ * tunnel closes within this interval plus the lookup's cache TTL.
84
+ */
85
+ hostGenerationCheckIntervalMs?: number;
105
86
  /**
106
87
  * Durable Object alarm surface, supplied by the Worker (`state.storage`) so the
107
88
  * room can schedule the hibernation-safe revocation re-check. Absent on the
@@ -146,8 +127,12 @@ export type WorkspaceRelaySocketAttachment = {
146
127
  hostId: string;
147
128
  workspaceIds: string[];
148
129
  connectedAt: number;
130
+ enrollmentId: string;
131
+ generation: number;
132
+ /** Consecutive unavailable generation lookups, persisted so hibernation cannot reset the outage grace. */
133
+ generationCheckFailures?: number;
149
134
  } | {
150
- kind: "user-hosted-client";
135
+ kind: "host-tunnel-client";
151
136
  channelId: string;
152
137
  claims: RuntimeAccessAttachmentClaims;
153
138
  target: WorkspaceRelayTargetAttachment;
@@ -191,8 +176,6 @@ export declare function relayFrameDiagnostics(data: unknown): Promise<{
191
176
  }>;
192
177
  export declare function workspaceRelayDurableObjectRoomName(workspaceId: string): string;
193
178
  export declare function workspaceRelayDurableObjectWorkspaceId(request: Request): string | undefined;
194
- export declare function setWorkspaceRelayAppOrigins(raw: string | undefined): void;
195
- export declare function setWorkspaceRelayAllowedOrigins(raw: string | undefined): void;
196
179
  export declare function createWorkspaceRelayDurableObjectGateway(options?: WorkspaceRelayDurableObjectGatewayOptions): {
197
180
  fetch(request: Request, env?: WorkspaceRelayDurableObjectEnv): Promise<Response>;
198
181
  };
@@ -201,7 +184,7 @@ export declare function createWorkspaceRelayDurableObjectRoom(options: Workspace
201
184
  webSocketMessage(socket: WorkspaceRelayDurableObjectSocket, message: string | ArrayBuffer): Promise<void>;
202
185
  /**
203
186
  * Durable Object alarm entry point. Runs the hibernation-safe revocation
204
- * sweep (W6b.2). The Worker must forward `alarm()` here.
187
+ * sweep. The Worker must forward `alarm()` here.
205
188
  */
206
189
  alarm(): Promise<void>;
207
190
  webSocketClose(socket: WorkspaceRelayDurableObjectSocket, code: number, reason: string): void;
@@ -1,3 +1,5 @@
1
1
  export declare const DEFAULT_RELAY_APP_ORIGINS: string[];
2
+ /** Request headers a browser may send on relayed workspace requests, as the `access-control-allow-headers` value. */
3
+ export declare const RELAY_ALLOWED_REQUEST_HEADERS: string;
2
4
  export declare function parseAllowedOrigins(raw: string | undefined): string[] | undefined;
3
5
  export declare function createOriginMatcher(patterns: string[]): (origin: string) => boolean;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * One tunnel's presence, recorded once per workspace it serves. `workspaceIds`
3
+ * is the full set that tunnel registered, so a reader that found the entry
4
+ * through one workspace still sees the others.
5
+ */
1
6
  export type HostTunnelPresence = {
2
7
  hostId: string;
3
8
  workspaceIds: string[];
@@ -5,6 +10,11 @@ export type HostTunnelPresence = {
5
10
  lastPongAt: number;
6
11
  expiresAt: number;
7
12
  };
13
+ /**
14
+ * Presence is keyed by (host, workspace), so a later registration for a
15
+ * workspace takes that workspace over without touching the host's other
16
+ * entries. `recordPong` and `disconnectHost` act on every entry of the host.
17
+ */
8
18
  export type WorkspaceRelayDirectory = {
9
19
  registerHostTunnel(input: {
10
20
  hostId: string;
@@ -16,40 +26,8 @@ export type WorkspaceRelayDirectory = {
16
26
  hostId: string;
17
27
  workspaceId: string;
18
28
  }): 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
29
  };
40
30
  export declare function createWorkspaceRelayDirectory(options?: {
41
31
  ttlMs?: number;
42
32
  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
33
  }): 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;
@@ -0,0 +1,7 @@
1
+ import type { WorkspaceRelayDurableObjectEnv } from "./cloudflare";
2
+ /**
3
+ * Where the gateway creates the room of the request's workspace. The room's
4
+ * location is fixed at its first creation and never migrates, so this
5
+ * decision is permanent for the workspace.
6
+ */
7
+ export declare function workspaceRelayRoomPlacement(request: Request, env: WorkspaceRelayDurableObjectEnv, workspaceId: string): Promise<import("./relay-location").RelayRoomPlacement>;
@@ -0,0 +1,7 @@
1
+ export type HostTunnelFailureCode = "host_tunnel_timeout" | "host_tunnel_response_body_too_large" | "host_tunnel_unavailable";
2
+ export declare function hostTunnelFailure(code: HostTunnelFailureCode): Error & {
3
+ code: HostTunnelFailureCode;
4
+ status: number;
5
+ retryable: boolean;
6
+ };
7
+ export declare function hostTunnelFailureResponse(error: unknown): Response;
@@ -0,0 +1,29 @@
1
+ import type { RelayBacking } from "./auth";
2
+ /**
3
+ * Whether a relay target is reached through a host tunnel.
4
+ *
5
+ * A `local-worktree` placement runs on an enrolled machine that dials the
6
+ * relay and multiplexes its traffic over that one socket; a `cloud-vm`
7
+ * placement runs on the provisioner's machine, which the relay reaches by a
8
+ * direct upstream connection.
9
+ */
10
+ export declare function isHostTunnelTarget(target: {
11
+ backing: RelayBacking;
12
+ }): boolean;
13
+ export type WorkspaceRelaySocketKind = "host-tunnel-client" | "client";
14
+ export declare function socketKindFor(target: {
15
+ backing: RelayBacking;
16
+ }): WorkspaceRelaySocketKind;
17
+ /**
18
+ * Privacy rule: never forward the browser's `Cookie` header through a host
19
+ * tunnel. The host process runs on a machine somebody uses, whose cookie jar
20
+ * the browser may share (localhost dev tooling), so passing cookies verbatim
21
+ * risks leaking session data to it. A cloud-vm sits behind a dedicated network
22
+ * boundary where a session cookie may legitimately be needed, so it passes
23
+ * `Cookie` through unchanged.
24
+ *
25
+ * Returns a new `Headers` instance; `headers` is not mutated.
26
+ */
27
+ export declare function forwardHeadersFor(target: {
28
+ backing: RelayBacking;
29
+ }, headers: Headers): Headers;
package/dist/http.d.ts CHANGED
@@ -1,7 +1,12 @@
1
- export declare function bearerToken(header: string | null | undefined): string | undefined;
1
+ import { bearerToken } from "@claxedo/helpers/string";
2
+ export { bearerToken };
2
3
  export declare function errorBody(code: string, message: string): {
3
4
  error: {
4
5
  code: string;
5
6
  message: string;
6
7
  };
7
8
  };
9
+ /** The host a tunnel request names, `/host-tunnels/<hostId>`. */
10
+ export declare function hostIdFromTunnelPath(pathname: string): string | undefined;
11
+ /** The workspaces a tunnel request registers for, under either spelling of the query name. */
12
+ export declare function workspaceIdsFromSearch(url: URL): string[];
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export * from "./auth";
2
- export * from "./bun";
3
2
  export * from "./cloudflare";
3
+ export * from "./relay-cors";
4
4
  export * from "./cors-origins";
5
5
  export * from "./directory";
6
+ export * from "./gateway-placement";
7
+ export * from "./relay-location";
8
+ export * from "./relay-target";
6
9
  export * from "./server";
10
+ export * from "./resolver-clients";