@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/dist/worker.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type WorkspaceRelayDurableObjectNamespace, type WorkspaceRelayDurableObjectRoomOptions, type WorkspaceRelayDurableObjectSocket } from "./cloudflare";
2
- import type { RuntimeAccessTokenActiveResult, WorkspaceRelayTarget } from "./server";
3
- export type WorkspaceRelayWorkerEnv = Record<string, unknown> & {
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: unknown, env: WorkspaceRelayWorkerEnv);
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 server that routes authenticated traffic to workspace runtimes: JWT workspace tokens, header-stripping trust boundary, Bun and Cloudflare adapters",
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
- "typecheck:bench": "tsc --noEmit -p bench/tsconfig.json",
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/workspace-relay-protocol": "0.7.0",
53
- "hono": "4.10.7",
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;