@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.
- package/README.md +114 -191
- package/dist/auth.d.ts +130 -20
- package/dist/cloudflare.d.ts +26 -43
- package/dist/cors-origins.d.ts +2 -0
- package/dist/directory.d.ts +10 -32
- package/dist/gateway-placement.d.ts +7 -0
- package/dist/host-tunnel-failure.d.ts +7 -0
- package/dist/host-tunnel-forwarding.d.ts +29 -0
- package/dist/http.d.ts +6 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.mjs +1285 -2049
- package/dist/relay-audit.d.ts +4 -0
- package/dist/relay-cors.d.ts +12 -0
- package/dist/relay-location.d.ts +93 -0
- package/dist/relay-target.d.ts +25 -0
- package/dist/resolver-clients.d.ts +116 -0
- package/dist/server.d.ts +51 -188
- package/dist/session-host-forwarding.d.ts +22 -0
- package/dist/test-support/unfenced-host-token.d.ts +6 -0
- package/dist/worker.d.ts +32 -4
- package/dist/workerd-fixture/boot.d.ts +18 -0
- package/package.json +9 -20
- package/dist/bun.d.ts +0 -170
- package/dist/main.d.ts +0 -246
- package/dist/main.mjs +0 -23219
- package/dist/synthetic.d.ts +0 -60
package/dist/worker.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type WorkspaceRelayDurableObjectNamespace, type WorkspaceRelayDurableObjectRoomOptions, type WorkspaceRelayDurableObjectSocket } from "./cloudflare";
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import { type WorkspaceRelayTarget } from "./relay-target";
|
|
3
|
+
import { type HostGenerationLookup, type RuntimeAccessTokenActiveResult } from "./resolver-clients";
|
|
4
|
+
type WorkspaceRelayWorkerBindings = {
|
|
4
5
|
WORKSPACE_RELAY_ROOM?: WorkspaceRelayDurableObjectNamespace;
|
|
5
6
|
CLAXEDO_RELAY_RESOLVER_URL?: string;
|
|
6
7
|
CLAXEDO_CENTRAL_URL?: string;
|
|
@@ -18,27 +19,54 @@ export type WorkspaceRelayWorkerEnv = Record<string, unknown> & {
|
|
|
18
19
|
CLAXEDO_RELAY_TRACE_FORCE_SECRET?: string;
|
|
19
20
|
CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS?: string;
|
|
20
21
|
CLAXEDO_RELAY_TARGET_CACHE_TTL_MS?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Absolute URL of the control plane's host-generation lookup. Unset derives
|
|
24
|
+
* `<resolver base>/host-generation` from the same base `/target` and
|
|
25
|
+
* `/revocation` are derived from.
|
|
26
|
+
*/
|
|
27
|
+
CLAXEDO_RELAY_HOST_GENERATION_URL?: string;
|
|
28
|
+
CLAXEDO_RELAY_HOST_GENERATION_CACHE_TTL_MS?: string;
|
|
21
29
|
CLAXEDO_APP_ORIGINS?: string;
|
|
22
30
|
CLAXEDO_RELAY_ALLOWED_ORIGINS?: string;
|
|
23
31
|
};
|
|
32
|
+
export type WorkspaceRelayWorkerEnv = Record<string, unknown> & WorkspaceRelayWorkerBindings;
|
|
24
33
|
type ResolverClient = {
|
|
25
|
-
target(workspaceId: string, hostId: string): Promise<WorkspaceRelayTarget | undefined>;
|
|
34
|
+
target(workspaceId: string, hostId: string, routingId?: string): Promise<WorkspaceRelayTarget | undefined>;
|
|
26
35
|
revocation(args: {
|
|
27
36
|
jti: string;
|
|
28
37
|
workspaceId: string;
|
|
29
38
|
hostId: string;
|
|
30
39
|
}): Promise<RuntimeAccessTokenActiveResult>;
|
|
40
|
+
hostGeneration: HostGenerationLookup;
|
|
31
41
|
};
|
|
32
42
|
type ResolverFetch = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
33
43
|
export declare function workspaceRelayWorkerResolverUrl(env: WorkspaceRelayWorkerEnv): string;
|
|
34
44
|
export declare function workspaceRelayWorkerResolverClient(env: WorkspaceRelayWorkerEnv, fetcher?: ResolverFetch): ResolverClient;
|
|
35
45
|
export declare function workspaceRelayDurableObjectOptions(env: WorkspaceRelayWorkerEnv): Promise<WorkspaceRelayDurableObjectRoomOptions>;
|
|
46
|
+
/**
|
|
47
|
+
* The `DurableObjectState` workerd hands a room, modelled structurally like the
|
|
48
|
+
* rest of this package's Cloudflare surface (see `./cloudflare`) — this package
|
|
49
|
+
* deliberately carries no `@cloudflare/workers-types` dependency.
|
|
50
|
+
*
|
|
51
|
+
* Everything is optional because `cloudflare.ts` treats hibernation and alarms
|
|
52
|
+
* as capabilities to DETECT: an older runtime, or a harness standing in for
|
|
53
|
+
* one, may provide neither, and the room degrades rather than failing.
|
|
54
|
+
*/
|
|
55
|
+
export type WorkspaceRelayRoomState = {
|
|
56
|
+
acceptWebSocket?: (socket: WorkspaceRelayDurableObjectSocket) => void;
|
|
57
|
+
getWebSockets?: () => WorkspaceRelayDurableObjectSocket[];
|
|
58
|
+
storage?: {
|
|
59
|
+
getAlarm?: () => Promise<number | null>;
|
|
60
|
+
setAlarm?: (scheduledTime: number) => Promise<void>;
|
|
61
|
+
deleteAlarm?: () => Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
36
64
|
export declare class WorkspaceRelayRoom {
|
|
37
65
|
private state;
|
|
38
66
|
private env;
|
|
39
67
|
private room?;
|
|
40
68
|
private loading?;
|
|
41
|
-
constructor(state:
|
|
69
|
+
constructor(state: WorkspaceRelayRoomState, env: WorkspaceRelayWorkerEnv);
|
|
42
70
|
private hibernation;
|
|
43
71
|
/**
|
|
44
72
|
* The DO alarm surface, used for the hibernation-safe revocation re-check.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Miniflare, type MiniflareOptions } from "miniflare";
|
|
2
|
+
/**
|
|
3
|
+
* Kill this process's workerd children outright. The `afterAll` replacement
|
|
4
|
+
* for `Miniflare#dispose()`, which must never be called (see header). Only
|
|
5
|
+
* direct children of this process whose command line names workerd can match,
|
|
6
|
+
* so nothing outside this test process is touched — and because `bun test`
|
|
7
|
+
* runs files sequentially, the only children alive when a file's `afterAll`
|
|
8
|
+
* runs are ones whose tests have already finished.
|
|
9
|
+
*/
|
|
10
|
+
export declare function reapWorkerd(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Boot workerd once and return the ready Miniflare. Call it exactly once per
|
|
13
|
+
* test file, from `beforeAll`, with a `workers` array hosting every
|
|
14
|
+
* configuration the file tests. Do NOT dispose the result — not in `afterAll`,
|
|
15
|
+
* not anywhere: disposal wedges every later boot in the process (see header).
|
|
16
|
+
* Tear down with `reapWorkerd()` in `afterAll` instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare function bootWorkerd(options: MiniflareOptions): Promise<Miniflare>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@claxedo/workspace-relay",
|
|
3
|
-
"version": "0.7.0",
|
|
4
2
|
"type": "module",
|
|
5
|
-
"description": "Relay/tunnel
|
|
3
|
+
"description": "Relay/tunnel that routes authenticated traffic to workspace runtimes on Cloudflare: JWT workspace tokens and a header-stripping trust boundary",
|
|
6
4
|
"license": "MIT",
|
|
7
5
|
"repository": {
|
|
8
6
|
"type": "git",
|
|
@@ -19,16 +17,13 @@
|
|
|
19
17
|
"types": "./dist/index.d.ts",
|
|
20
18
|
"exports": {
|
|
21
19
|
".": {
|
|
20
|
+
"development": "./src/index.ts",
|
|
21
|
+
"bun": "./src/index.ts",
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
|
-
"bun": "./dist/index.mjs",
|
|
24
|
-
"development": "./dist/index.mjs",
|
|
25
23
|
"import": "./dist/index.mjs",
|
|
26
24
|
"default": "./dist/index.mjs"
|
|
27
25
|
}
|
|
28
26
|
},
|
|
29
|
-
"bin": {
|
|
30
|
-
"workspace-relay": "dist/main.mjs"
|
|
31
|
-
},
|
|
32
27
|
"files": [
|
|
33
28
|
"README.md",
|
|
34
29
|
"LICENSE",
|
|
@@ -37,26 +32,20 @@
|
|
|
37
32
|
"publishConfig": {
|
|
38
33
|
"access": "public"
|
|
39
34
|
},
|
|
35
|
+
"name": "@claxedo/workspace-relay",
|
|
36
|
+
"version": "0.10.0",
|
|
40
37
|
"scripts": {
|
|
41
|
-
"start": "bun run src/main.ts",
|
|
42
|
-
"dev": "bun run --hot src/main.ts",
|
|
43
38
|
"build": "tsx scripts/build.ts",
|
|
44
39
|
"prepublishOnly": "npm run build",
|
|
45
40
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
46
|
-
"
|
|
47
|
-
"test": "mkdir -p .artifacts/unit && bun test src bench --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
|
|
48
|
-
"bench:gate": "bun bench/local-dry-run.ts",
|
|
49
|
-
"bench:gate:cf": "bun bench/cf-dev-smoke.ts"
|
|
41
|
+
"test": "mkdir -p .artifacts/unit && node ../../script/test-deadline.mjs bun test src scripts --path-ignore-patterns='**/relay-workerd-*' --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml && node ../../script/test-deadline.mjs bun test src/relay-workerd-backpressure.test.ts src/relay-workerd-binary.test.ts src/relay-workerd-session-host.test.ts --timeout 240000 --reporter=junit --reporter-outfile=.artifacts/unit/junit-workerd.xml"
|
|
50
42
|
},
|
|
51
43
|
"dependencies": {
|
|
52
|
-
"@claxedo/
|
|
53
|
-
"
|
|
54
|
-
"jose": "6.0.11"
|
|
55
|
-
"posthog-node": "4.18.0"
|
|
44
|
+
"@claxedo/helpers": "0.2.0",
|
|
45
|
+
"@claxedo/workspace-relay-protocol": "0.10.0",
|
|
46
|
+
"jose": "6.0.11"
|
|
56
47
|
},
|
|
57
48
|
"devDependencies": {
|
|
58
|
-
"@claxedo/sandbox-manager": "0.7.0",
|
|
59
|
-
"@daytona/sdk": "0.192.0",
|
|
60
49
|
"@tsconfig/node-lts": "22.0.4",
|
|
61
50
|
"@types/bun": "1.3.13",
|
|
62
51
|
"esbuild": "0.25.12",
|
package/dist/bun.d.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { type WorkspaceRelayOptions } from "./server";
|
|
2
|
-
type RelayClientWebSocketData = {
|
|
3
|
-
kind: "client";
|
|
4
|
-
upstreamUrl: string;
|
|
5
|
-
headers: Record<string, string>;
|
|
6
|
-
queue: Array<{
|
|
7
|
-
payload: string | Buffer<ArrayBuffer>;
|
|
8
|
-
queuedAt: number;
|
|
9
|
-
}>;
|
|
10
|
-
/** Running byte size of `queue`; reset when the queue is flushed upstream. */
|
|
11
|
-
queuedBytes?: number;
|
|
12
|
-
upstream?: WebSocket;
|
|
13
|
-
upstreamOpenTimer?: ReturnType<typeof setTimeout>;
|
|
14
|
-
trace?: RelayClientWebSocketTrace;
|
|
15
|
-
};
|
|
16
|
-
type RelayClientWebSocketTrace = {
|
|
17
|
-
acceptedAt: number;
|
|
18
|
-
upstreamStartedAt?: number;
|
|
19
|
-
upstreamOpenMs?: number;
|
|
20
|
-
queuedFrames: number;
|
|
21
|
-
maxQueuedDelayMs: number;
|
|
22
|
-
emitted: boolean;
|
|
23
|
-
};
|
|
24
|
-
type RelayHostTunnelWebSocketData = {
|
|
25
|
-
kind: "host-tunnel";
|
|
26
|
-
hostId: string;
|
|
27
|
-
workspaceIds: string[];
|
|
28
|
-
pending: Map<string, PendingTunnelHttpResponse>;
|
|
29
|
-
channels: Map<string, Bun.ServerWebSocket<RelayUserHostedClientWebSocketData>>;
|
|
30
|
-
heartbeat?: ReturnType<typeof setInterval>;
|
|
31
|
-
missedPongs: number;
|
|
32
|
-
messageBuffer: string;
|
|
33
|
-
};
|
|
34
|
-
type RelayUserHostedClientWebSocketData = {
|
|
35
|
-
kind: "user-hosted-client";
|
|
36
|
-
hostId: string;
|
|
37
|
-
workspaceId: string;
|
|
38
|
-
channelId: string;
|
|
39
|
-
path: string;
|
|
40
|
-
relayHostToken: string;
|
|
41
|
-
};
|
|
42
|
-
type RelayWebSocketData = RelayClientWebSocketData | RelayHostTunnelWebSocketData | RelayUserHostedClientWebSocketData;
|
|
43
|
-
type UpstreamWebSocketConstructor = {
|
|
44
|
-
new (url: string, options: Bun.WebSocketOptions): WebSocket;
|
|
45
|
-
};
|
|
46
|
-
type PendingTunnelHttpResponse = {
|
|
47
|
-
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
48
|
-
stream: ReadableStream<Uint8Array>;
|
|
49
|
-
resolve: (response: Response) => void;
|
|
50
|
-
reject: (error: Error) => void;
|
|
51
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
52
|
-
pendingChunks: Uint8Array[];
|
|
53
|
-
bytesQueued: number;
|
|
54
|
-
slowConsumerTimeout?: ReturnType<typeof setTimeout>;
|
|
55
|
-
responseStarted: boolean;
|
|
56
|
-
};
|
|
57
|
-
export type WorkspaceRelayHostTunnelOptions = {
|
|
58
|
-
authorizeHostTunnel?: (request: Request, input: {
|
|
59
|
-
hostId: string;
|
|
60
|
-
workspaceIds: string[];
|
|
61
|
-
}) => boolean | Promise<boolean>;
|
|
62
|
-
hostTunnelPingIntervalMs?: number;
|
|
63
|
-
hostTunnelMaxMissedPongs?: number;
|
|
64
|
-
hostTunnelStateDebounceMs?: number;
|
|
65
|
-
};
|
|
66
|
-
export type WorkspaceRelayBackpressureOptions = {
|
|
67
|
-
slowConsumerHighWaterMarkBytes?: number;
|
|
68
|
-
slowConsumerTimeoutMs?: number;
|
|
69
|
-
tunnelRequestBodyMaxBytes?: number;
|
|
70
|
-
tunnelHttpResponseTimeoutMs?: number;
|
|
71
|
-
directHttpTimeoutMs?: number;
|
|
72
|
-
directHttpConcurrency?: number;
|
|
73
|
-
upstreamWebSocketOpenTimeoutMs?: number;
|
|
74
|
-
upstreamWebSocketPreOpenQueueMaxFrames?: number;
|
|
75
|
-
upstreamWebSocketPreOpenQueueMaxBytes?: number;
|
|
76
|
-
upstreamWebSocket?: UpstreamWebSocketConstructor;
|
|
77
|
-
webSocketBufferedAmountMaxBytes?: number;
|
|
78
|
-
};
|
|
79
|
-
export type WorkspaceRelayBunOptions = WorkspaceRelayHostTunnelOptions & WorkspaceRelayBackpressureOptions;
|
|
80
|
-
export type WorkspaceRelayBunTelemetry = {
|
|
81
|
-
getFragmentationStats(): FragmentationStats;
|
|
82
|
-
resetFragmentationStats(): void;
|
|
83
|
-
getSlowConsumerStats(): SlowConsumerStats;
|
|
84
|
-
resetSlowConsumerStats(): void;
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* T9: drain controller exposed by `createWorkspaceRelayBun`.
|
|
88
|
-
*
|
|
89
|
-
* On SIGTERM the operator (see `installShutdownDrainHandler` in main.ts)
|
|
90
|
-
* flips `setDraining(true)`; from that point onward the relay
|
|
91
|
-
* - returns 503 from `/health` so Fly removes the instance from routing
|
|
92
|
-
* - rejects new HTTP requests with 503 (`relay_draining`) before doing auth
|
|
93
|
-
* - rejects new host-tunnel WebSocket upgrades with 503
|
|
94
|
-
* - closes active host tunnels with 1012 so host clients reconnect promptly
|
|
95
|
-
*
|
|
96
|
-
* `pendingCount()` reports the total number of in-flight tunnel HTTP
|
|
97
|
-
* responses across every connected host tunnel — this is what the operator
|
|
98
|
-
* polls via `waitForDrain(timeoutMs)` before hard-closing remaining sockets.
|
|
99
|
-
*/
|
|
100
|
-
export type WorkspaceRelayBunDrainController = {
|
|
101
|
-
isDraining(): boolean;
|
|
102
|
-
setDraining(value: boolean): void;
|
|
103
|
-
pendingCount(): number;
|
|
104
|
-
waitForDrain(timeoutMs: number): Promise<{
|
|
105
|
-
drained: boolean;
|
|
106
|
-
remaining: number;
|
|
107
|
-
}>;
|
|
108
|
-
};
|
|
109
|
-
/**
|
|
110
|
-
* Bytes queued in a socket's send buffer, or `undefined` if it cannot report.
|
|
111
|
-
*
|
|
112
|
-
* Bun spells this `getBufferedAmount()` — a method, not the browser's
|
|
113
|
-
* `bufferedAmount` property. The two guards below used to read the property
|
|
114
|
-
* behind a non-null assertion, which typechecked and then evaluated
|
|
115
|
-
* `undefined > 8388608` forever: neither had ever closed a connection. Measured
|
|
116
|
-
* in `relay-workerd-backpressure.test.ts` at 16.8 MB queued, twice the limit,
|
|
117
|
-
* guard silent.
|
|
118
|
-
*
|
|
119
|
-
* FAILS OPEN on a socket that cannot report, deliberately. Test doubles and any
|
|
120
|
-
* exotic socket must not be closed for lacking the accessor — an unguarded
|
|
121
|
-
* healthy connection beats a guard that kills healthy connections. Note this is
|
|
122
|
-
* also why the same guard is NOT portable to `cloudflare.ts`: workerd's
|
|
123
|
-
* WebSocket exposes no buffer depth in any form, so there it would fail open
|
|
124
|
-
* unconditionally. Bounding the Cloudflare path needs a protocol-level
|
|
125
|
-
* credit/ack window instead.
|
|
126
|
-
*/
|
|
127
|
-
export declare const relayBufferedBytes: (socket: unknown) => number | undefined;
|
|
128
|
-
/**
|
|
129
|
-
* True only when the socket reports a depth over the limit. Unknown ⇒ false.
|
|
130
|
-
*
|
|
131
|
-
* Exported for `bun.test.ts`. Forcing a real over-limit buffer through the
|
|
132
|
-
* server's own sockets is not achievable locally — a loopback peer drains far
|
|
133
|
-
* faster than a test can outrun it, which is precisely why the dead guard went
|
|
134
|
-
* unnoticed for so long — so the decision itself is asserted directly.
|
|
135
|
-
*/
|
|
136
|
-
export declare const relayOverBackpressureLimit: (socket: unknown, limitBytes: number) => boolean;
|
|
137
|
-
export type FragmentationStats = {
|
|
138
|
-
fragmentsBuffered: number;
|
|
139
|
-
oversizedClosed: number;
|
|
140
|
-
};
|
|
141
|
-
export type SlowConsumerStats = {
|
|
142
|
-
overflowEvents: number;
|
|
143
|
-
timerFired: number;
|
|
144
|
-
droppedRequests: number;
|
|
145
|
-
};
|
|
146
|
-
declare function createSlowConsumerStats(): SlowConsumerStats;
|
|
147
|
-
declare function enqueueChunkWithBackpressure(input: {
|
|
148
|
-
ws: Bun.ServerWebSocket<RelayHostTunnelWebSocketData>;
|
|
149
|
-
requestId: string;
|
|
150
|
-
entry: PendingTunnelHttpResponse;
|
|
151
|
-
chunk: Uint8Array;
|
|
152
|
-
slowConsumerTimeoutMs: number;
|
|
153
|
-
slowConsumerStats: SlowConsumerStats;
|
|
154
|
-
}): void;
|
|
155
|
-
export declare const __slowConsumerInternalsForTest: {
|
|
156
|
-
enqueueChunkWithBackpressure: typeof enqueueChunkWithBackpressure;
|
|
157
|
-
createSlowConsumerStats: typeof createSlowConsumerStats;
|
|
158
|
-
};
|
|
159
|
-
export declare function createWorkspaceRelayBun(options: WorkspaceRelayOptions, bunOptions?: WorkspaceRelayBunOptions): {
|
|
160
|
-
fetch(request: Request, server: Bun.Server<RelayWebSocketData>): Promise<Response | undefined>;
|
|
161
|
-
websocket: {
|
|
162
|
-
maxPayloadLength: number;
|
|
163
|
-
message(ws: Bun.ServerWebSocket<RelayWebSocketData>, message: string | Buffer<ArrayBuffer>): void;
|
|
164
|
-
open(ws: Bun.ServerWebSocket<RelayWebSocketData>): void;
|
|
165
|
-
close(ws: Bun.ServerWebSocket<RelayWebSocketData>, code: number, reason: string): void;
|
|
166
|
-
};
|
|
167
|
-
drain: WorkspaceRelayBunDrainController;
|
|
168
|
-
telemetry: WorkspaceRelayBunTelemetry;
|
|
169
|
-
};
|
|
170
|
-
export {};
|
package/dist/main.d.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { type WorkspaceRelayBunDrainController } from "./bun";
|
|
2
|
-
import { type WorkspaceRelayDirectory } from "./directory";
|
|
3
|
-
import { type RelayHostPublicKey, type RevocationLookup, type WorkspaceRelayTarget } from "./server";
|
|
4
|
-
import type { RelayKey } from "./auth";
|
|
5
|
-
export { createCachedRevocationClient, createCachedTargetClient } from "./server";
|
|
6
|
-
/**
|
|
7
|
-
* W2c (2026-07-28-001): PostHog is the relay's error sink — one vendor
|
|
8
|
-
* carries error tracking for every runtime behind one distinct_id space.
|
|
9
|
-
*
|
|
10
|
-
* Sending requires two independent opt-ins: `CLAXEDO_TELEMETRY_MODE=on` AND
|
|
11
|
-
* `CLAXEDO_POSTHOG_KEY` (`POSTHOG_KEY` accepted as a fallback — the same
|
|
12
|
-
* unprefixed name claxedo-server's posthog.ts already reads). Any other
|
|
13
|
-
* combination → no `PostHog` client is ever constructed: zero SDK overhead,
|
|
14
|
-
* zero network — a clean no-op safe to ship before the telemetry account
|
|
15
|
-
* exists. Host defaults to `https://us.i.posthog.com` (the canonical ingest
|
|
16
|
-
* host — NOT the legacy `app.posthog.com` default some SDKs ship). Release =
|
|
17
|
-
* git SHA passed by the D11 deploy workflow (`CLAXEDO_RELEASE`; `GIT_SHA`
|
|
18
|
-
* accepted as alias); captures are tagged unit=relay + deployment_mode
|
|
19
|
-
* (absent → "self-host", mirroring D9's default).
|
|
20
|
-
*
|
|
21
|
-
* `posthog-node` runs fine on Bun (`Bun.serve`), unlike the Cloudflare
|
|
22
|
-
* Worker, which keeps it off its import graph entirely (see worker.ts).
|
|
23
|
-
*/
|
|
24
|
-
export type RelayObservabilityEnv = {
|
|
25
|
-
/** Only `on` permits sending, matched case-insensitively after trimming. */
|
|
26
|
-
CLAXEDO_TELEMETRY_MODE?: string | undefined;
|
|
27
|
-
CLAXEDO_POSTHOG_KEY?: string | undefined;
|
|
28
|
-
POSTHOG_KEY?: string | undefined;
|
|
29
|
-
CLAXEDO_POSTHOG_HOST?: string | undefined;
|
|
30
|
-
CLAXEDO_RELEASE?: string | undefined;
|
|
31
|
-
GIT_SHA?: string | undefined;
|
|
32
|
-
CLAXEDO_DEPLOYMENT_MODE?: string | undefined;
|
|
33
|
-
/** Accept process.env verbatim (extra keys are ignored). */
|
|
34
|
-
[key: string]: string | undefined;
|
|
35
|
-
};
|
|
36
|
-
export type RelayTelemetryOptions = {
|
|
37
|
-
key: string;
|
|
38
|
-
host: string;
|
|
39
|
-
release?: string;
|
|
40
|
-
tags: {
|
|
41
|
-
unit: "relay";
|
|
42
|
-
deployment_mode: string;
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
/**
|
|
46
|
-
* Pure env → PostHog options resolver. `undefined` = do NOT construct a client
|
|
47
|
-
* (no client, no network), which is the answer unless BOTH opt-ins are
|
|
48
|
-
* present. The mode is read before the key so that a deployment which has not
|
|
49
|
-
* said `on` stays silent no matter which keys reach its environment.
|
|
50
|
-
* Exported for tests.
|
|
51
|
-
*/
|
|
52
|
-
export declare function relayTelemetryOptions(env: RelayObservabilityEnv): RelayTelemetryOptions | undefined;
|
|
53
|
-
export declare function initRelayObservability(env?: RelayObservabilityEnv): {
|
|
54
|
-
enabled: boolean;
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* Capture-and-flush used by the fatal handlers and the startup catch. With no
|
|
58
|
-
* initialized client (key absent) this is a documented no-op. Never throws —
|
|
59
|
-
* observability must not preempt the exit path.
|
|
60
|
-
*
|
|
61
|
-
* `distinctId: "system"` mirrors the ops-plane convention: process-fatal
|
|
62
|
-
* events carry no user identity. `client.flush()` has no built-in timeout,
|
|
63
|
-
* so it races a 2s timer; the flush promise's
|
|
64
|
-
* rejection is swallowed even when the timer wins the race, so a slow network
|
|
65
|
-
* failure that resolves after the timeout never surfaces as a second
|
|
66
|
-
* unhandledRejection mid-shutdown.
|
|
67
|
-
*/
|
|
68
|
-
export declare function reportFatal(error: unknown): Promise<void>;
|
|
69
|
-
/**
|
|
70
|
-
* Production fail-closed gate for the relay → claxedo-server resolver channel.
|
|
71
|
-
*
|
|
72
|
-
* If `CLAXEDO_RELAY_RESOLVER_TOKEN` is missing/empty in production the relay
|
|
73
|
-
* would otherwise call the resolver unauthenticated. Refuse to boot.
|
|
74
|
-
*
|
|
75
|
-
* Allowed in dev/test for ergonomics — the resolver itself falls back to a
|
|
76
|
-
* loopback-only check when no token is configured.
|
|
77
|
-
*/
|
|
78
|
-
export type ValidateProductionEnvInput = {
|
|
79
|
-
NODE_ENV?: string | undefined;
|
|
80
|
-
CLAXEDO_RELAY_RESOLVER_TOKEN?: string | undefined;
|
|
81
|
-
};
|
|
82
|
-
export type ValidateProductionEnvResult = {
|
|
83
|
-
ok: true;
|
|
84
|
-
} | {
|
|
85
|
-
ok: false;
|
|
86
|
-
exitCode: 2;
|
|
87
|
-
message: string;
|
|
88
|
-
};
|
|
89
|
-
export declare function validateProductionEnv(env: ValidateProductionEnvInput): ValidateProductionEnvResult;
|
|
90
|
-
/**
|
|
91
|
-
* Resolve the verification key (or keyset resolver) for Runtime Access Tokens.
|
|
92
|
-
*
|
|
93
|
-
* Precedence: `CLAXEDO_CONTROL_PLANE_JWKS_URL` > `CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM`.
|
|
94
|
-
* The PEM env var is kept as a fallback so JWKS rollouts can ship gradually.
|
|
95
|
-
*
|
|
96
|
-
* Exported for tests.
|
|
97
|
-
*/
|
|
98
|
-
export type LoadRuntimeAccessKeyEnv = {
|
|
99
|
-
CLAXEDO_CONTROL_PLANE_JWKS_URL?: string | undefined;
|
|
100
|
-
CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM?: string | undefined;
|
|
101
|
-
};
|
|
102
|
-
export declare function loadRuntimeAccessKeyOrJwks(env: LoadRuntimeAccessKeyEnv): Promise<RelayKey>;
|
|
103
|
-
/**
|
|
104
|
-
* Resolve the RHT key material — both the private key used to sign RHTs and
|
|
105
|
-
* the public key(s) we publish at `/.well-known/jwks.json`.
|
|
106
|
-
*
|
|
107
|
-
* Precedence for the public counterpart:
|
|
108
|
-
* 1. `CLAXEDO_RELAY_HOST_PUBLIC_KEY_PEM` (explicit override; useful when
|
|
109
|
-
* the private key is held in an HSM-style import that strips public bits).
|
|
110
|
-
* 2. Derived from the private key via `exportJWK` (works because we now
|
|
111
|
-
* load the private key with `extractable: true`).
|
|
112
|
-
*
|
|
113
|
-
* `kid` is the explicit `CLAXEDO_RELAY_HOST_KID` env var if set, otherwise
|
|
114
|
-
* SHA-256(public-key-x).slice(0, 16). Mint and JWKS use the same derivation
|
|
115
|
-
* so they always agree.
|
|
116
|
-
*
|
|
117
|
-
* `next` is optional and is included in JWKS only — never used to mint. It
|
|
118
|
-
* exists to support rolling rotation: publish the next key, drain the
|
|
119
|
-
* current one, swap signing key, retire the old.
|
|
120
|
-
*
|
|
121
|
-
* Exported for tests.
|
|
122
|
-
*/
|
|
123
|
-
export type LoadRelayHostKeyMaterialEnv = {
|
|
124
|
-
/**
|
|
125
|
-
* Used to gate the production fail-closed check for the RHT signing key.
|
|
126
|
-
* If `production` and no signing key PEM is configured, the loader exits
|
|
127
|
-
* with code 2 rather than silently generating an ephemeral key (which
|
|
128
|
-
* would be different on every instance and break verification across a
|
|
129
|
-
* multi-instance deploy).
|
|
130
|
-
*
|
|
131
|
-
* Note: T2's `validateProductionEnv` is the canonical home for top-level
|
|
132
|
-
* env validation, but this check lives here because the loader already
|
|
133
|
-
* inspects the signing-key env var and the failure mode is local. The
|
|
134
|
-
* boot path could optionally re-route through `validateProductionEnv` in
|
|
135
|
-
* a future refactor.
|
|
136
|
-
*/
|
|
137
|
-
NODE_ENV?: string | undefined;
|
|
138
|
-
CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM?: string | undefined;
|
|
139
|
-
CLAXEDO_RELAY_HOST_PUBLIC_KEY_PEM?: string | undefined;
|
|
140
|
-
CLAXEDO_RELAY_HOST_NEXT_PUBLIC_KEY_PEM?: string | undefined;
|
|
141
|
-
CLAXEDO_RELAY_HOST_KID?: string | undefined;
|
|
142
|
-
CLAXEDO_RELAY_HOST_NEXT_KID?: string | undefined;
|
|
143
|
-
};
|
|
144
|
-
export type RelayHostKeyMaterial = {
|
|
145
|
-
privateKey: CryptoKey;
|
|
146
|
-
current: RelayHostPublicKey;
|
|
147
|
-
next?: RelayHostPublicKey;
|
|
148
|
-
};
|
|
149
|
-
export declare function loadRelayHostKeyMaterial(env: LoadRelayHostKeyMaterialEnv): Promise<RelayHostKeyMaterial>;
|
|
150
|
-
/**
|
|
151
|
-
* T16: parse `CLAXEDO_RELAY_AUDIT_ACCEPT_SAMPLE_RATE` into a number in [0, 1].
|
|
152
|
-
* Falls back to a NODE_ENV-gated default (production: 0.1, dev/test: 1.0) if
|
|
153
|
-
* unset, empty, or out of range. Exported for tests.
|
|
154
|
-
*/
|
|
155
|
-
export type ParseAuditAcceptSampleRateEnv = {
|
|
156
|
-
CLAXEDO_RELAY_AUDIT_ACCEPT_SAMPLE_RATE?: string | undefined;
|
|
157
|
-
NODE_ENV?: string | undefined;
|
|
158
|
-
};
|
|
159
|
-
export declare function parseAuditAcceptSampleRate(env: ParseAuditAcceptSampleRateEnv): number;
|
|
160
|
-
/**
|
|
161
|
-
* T31: parse the `CLAXEDO_RELAY_METRICS_TOKEN` env var. Returns `undefined`
|
|
162
|
-
* for missing/empty/whitespace-only values so the relay can fall back to its
|
|
163
|
-
* loopback-only `/metrics` access policy.
|
|
164
|
-
*/
|
|
165
|
-
export type ParseMetricsTokenEnv = {
|
|
166
|
-
CLAXEDO_RELAY_METRICS_TOKEN?: string | undefined;
|
|
167
|
-
};
|
|
168
|
-
export declare function parseMetricsToken(env: ParseMetricsTokenEnv): string | undefined;
|
|
169
|
-
export type RuntimeAccessTokenCacheEnv = {
|
|
170
|
-
CLAXEDO_RELAY_RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS?: string | undefined;
|
|
171
|
-
};
|
|
172
|
-
export declare function runtimeAccessTokenCacheTtlMsFromEnv(env: RuntimeAccessTokenCacheEnv): number;
|
|
173
|
-
export type DirectHttpConcurrencyEnv = {
|
|
174
|
-
CLAXEDO_RELAY_DIRECT_HTTP_CONCURRENCY?: string | undefined;
|
|
175
|
-
};
|
|
176
|
-
export declare function directHttpConcurrencyFromEnv(env: DirectHttpConcurrencyEnv): number | undefined;
|
|
177
|
-
export type ResolverClientCacheOptions = {
|
|
178
|
-
targetCacheTtlMs?: number;
|
|
179
|
-
revocationCacheTtlMs?: number;
|
|
180
|
-
};
|
|
181
|
-
export type ResolverClientCacheEnv = {
|
|
182
|
-
CLAXEDO_RELAY_TARGET_CACHE_TTL_MS?: string | undefined;
|
|
183
|
-
CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS?: string | undefined;
|
|
184
|
-
};
|
|
185
|
-
export declare function resolverClientCacheOptionsFromEnv(env: ResolverClientCacheEnv): ResolverClientCacheOptions;
|
|
186
|
-
export declare function createResolverClient(baseUrl: string, token: string | undefined, options?: ResolverClientCacheOptions): {
|
|
187
|
-
target: (workspaceId: string, hostId: string) => Promise<WorkspaceRelayTarget | undefined>;
|
|
188
|
-
revocation: RevocationLookup;
|
|
189
|
-
};
|
|
190
|
-
/**
|
|
191
|
-
* T9: graceful SIGTERM drain wiring.
|
|
192
|
-
*
|
|
193
|
-
* On `SIGTERM` (or `SIGINT` for local dev) the relay must:
|
|
194
|
-
* 1. Flip the drain flag so `/health` reports 503 and Fly removes the
|
|
195
|
-
* machine from routing, and so new HTTP requests + tunnel registrations
|
|
196
|
-
* fast-path to 503 before doing any auth or upstream work.
|
|
197
|
-
* 2. Wait up to `drainTimeoutMs` (default 30 s, override via
|
|
198
|
-
* `CLAXEDO_RELAY_DRAIN_TIMEOUT_MS`) for the in-flight tunnel HTTP
|
|
199
|
-
* response map to empty across every connected host tunnel.
|
|
200
|
-
* 3. Release the directory's TTL sweep timer (T18 added `dispose()`) so
|
|
201
|
-
* the process can exit cleanly without a dangling `setInterval`.
|
|
202
|
-
* 4. Exit with code 0.
|
|
203
|
-
*
|
|
204
|
-
* Exposed for tests so the trigger logic can be exercised without actually
|
|
205
|
-
* raising signals or stopping the process. Pass `register: false` in tests
|
|
206
|
-
* to skip the real `process.on(...)` registration.
|
|
207
|
-
*/
|
|
208
|
-
export type ShutdownDrainHandlerOptions = {
|
|
209
|
-
drain: WorkspaceRelayBunDrainController;
|
|
210
|
-
directory: Pick<WorkspaceRelayDirectory, "dispose">;
|
|
211
|
-
drainTimeoutMs: number;
|
|
212
|
-
/** Defaults to `process.exit`. Override for tests. */
|
|
213
|
-
exit?: (code?: number) => void;
|
|
214
|
-
/** Defaults to a no-op log. */
|
|
215
|
-
log?: (message: string) => void;
|
|
216
|
-
/** When true (default), register `SIGTERM` and `SIGINT` handlers on `process`. */
|
|
217
|
-
register?: boolean;
|
|
218
|
-
/** Signals to listen for. Defaults to `["SIGTERM", "SIGINT"]`. */
|
|
219
|
-
signals?: NodeJS.Signals[];
|
|
220
|
-
/** Defaults to a no-op (we let Bun.serve get GC'd as part of process exit). */
|
|
221
|
-
stopServer?: () => Promise<void> | void;
|
|
222
|
-
};
|
|
223
|
-
export type ShutdownDrainHandle = {
|
|
224
|
-
trigger(): Promise<void>;
|
|
225
|
-
isShuttingDown(): boolean;
|
|
226
|
-
};
|
|
227
|
-
export declare function installShutdownDrainHandler(options: ShutdownDrainHandlerOptions): ShutdownDrainHandle;
|
|
228
|
-
export type FatalProcessHandlerOptions = {
|
|
229
|
-
drain: WorkspaceRelayBunDrainController;
|
|
230
|
-
directory: Pick<WorkspaceRelayDirectory, "dispose">;
|
|
231
|
-
exit?: (code?: number) => void;
|
|
232
|
-
log?: (message: string) => void;
|
|
233
|
-
register?: boolean;
|
|
234
|
-
/**
|
|
235
|
-
* W2c: error-tracker hook, invoked (and awaited) before teardown so a fatal
|
|
236
|
-
* crash reaches PostHog before the process exits. Failures are logged and
|
|
237
|
-
* never block the exit path. Defaults to none (tests, key-absent runs).
|
|
238
|
-
*/
|
|
239
|
-
report?: (error: unknown, source: "uncaughtException" | "unhandledRejection") => void | Promise<void>;
|
|
240
|
-
stopServer?: () => Promise<void> | void;
|
|
241
|
-
};
|
|
242
|
-
export type FatalProcessHandle = {
|
|
243
|
-
trigger(error: unknown, source: "uncaughtException" | "unhandledRejection"): Promise<void>;
|
|
244
|
-
isFatal(): boolean;
|
|
245
|
-
};
|
|
246
|
-
export declare function installFatalProcessHandlers(options: FatalProcessHandlerOptions): FatalProcessHandle;
|