@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/dist/server.d.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { TokenVerifier } from "@claxedo/workspace-relay-protocol";
|
|
3
|
+
import { type RelayAccess, type RelayBacking, type RelayJwtAlgorithm, type RelayKey, type RelayRole, type RuntimeAccessTokenClaims } from "./auth";
|
|
4
|
+
import type { WorkspaceRelayDirectory } from "./directory";
|
|
5
|
+
export type RelayHostPublicKey = {
|
|
6
|
+
publicKey: CryptoKey | Uint8Array;
|
|
7
|
+
kid: string;
|
|
8
|
+
};
|
|
9
|
+
export type WorkspaceRelayTarget = {
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
hostId: string;
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
access: RelayAccess;
|
|
14
|
+
backing: RelayBacking;
|
|
15
|
+
};
|
|
16
|
+
export type RuntimeAccessTokenActiveResult = {
|
|
17
|
+
active: true;
|
|
18
|
+
} | {
|
|
19
|
+
active: false;
|
|
20
|
+
code: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
export type WorkspaceRelayAuditEvent = {
|
|
24
|
+
action: "relay.request.accepted" | "relay.request.denied" | "relay.request.suppressed_summary" | "host_tunnel.connected" | "host_tunnel.disconnected";
|
|
25
|
+
result: "allow" | "deny";
|
|
26
|
+
reason?: string;
|
|
27
|
+
subject?: string;
|
|
28
|
+
role?: RelayRole;
|
|
29
|
+
workspaceId?: string;
|
|
30
|
+
hostId?: string;
|
|
31
|
+
method: string;
|
|
32
|
+
path: string;
|
|
33
|
+
/**
|
|
34
|
+
* T16: only set on `relay.request.suppressed_summary` events. Counts the
|
|
35
|
+
* number of `relay.request.accepted` audit events that were dropped by the
|
|
36
|
+
* sampler in the previous flush interval.
|
|
37
|
+
*/
|
|
38
|
+
suppressedCount?: number;
|
|
39
|
+
};
|
|
40
|
+
export type WorkspaceRelayAuthOptions = {
|
|
41
|
+
runtimeAccessKey: RelayKey;
|
|
42
|
+
/**
|
|
43
|
+
* P-shared: optional override for runtime-access-token verification.
|
|
44
|
+
*
|
|
45
|
+
* When set, the relay calls this verifier instead of decoding the JWT
|
|
46
|
+
* with `runtimeAccessKey`. The verifier returns `{ subject, scopes,
|
|
47
|
+
* claims }`; the relay continues to use `claims` to drive
|
|
48
|
+
* `resolveTarget` and the workspace/host audit fields. `runtimeAccessKey`
|
|
49
|
+
* is then only used for legacy token paths and tests that exercise the
|
|
50
|
+
* raw JWT codepath.
|
|
51
|
+
*
|
|
52
|
+
* This is the seam downstream users plug their own auth backend
|
|
53
|
+
* through (Clerk-issued tokens, OIDC introspection, custom keys, etc.).
|
|
54
|
+
* See `@claxedo/workspace-relay-protocol/token-verifier` for the
|
|
55
|
+
* `HttpTokenVerifier` and `StaticTokenVerifier` reference impls.
|
|
56
|
+
*/
|
|
57
|
+
tokenVerifier?: TokenVerifier;
|
|
58
|
+
isRuntimeAccessTokenActive?: (claims: RuntimeAccessTokenClaims) => RuntimeAccessTokenActiveResult | Promise<RuntimeAccessTokenActiveResult>;
|
|
59
|
+
};
|
|
60
|
+
export type WorkspaceRelayHostTokenOptions = {
|
|
61
|
+
relayHostSigningKey: CryptoKey | Uint8Array;
|
|
62
|
+
relayHostAlgorithm: RelayJwtAlgorithm;
|
|
63
|
+
/**
|
|
64
|
+
* Public keys to publish at `/.well-known/jwks.json`. The first entry is the
|
|
65
|
+
* "current" mint key; any further entries are "next" keys (for rolling
|
|
66
|
+
* rotation). Verifiers (workspace-runtime → workspace-host-service) consume
|
|
67
|
+
* this set via `jose.createRemoteJWKSet`. Omit to disable the endpoint
|
|
68
|
+
* (during rollout / for tests that do not need JWKS).
|
|
69
|
+
*/
|
|
70
|
+
relayHostPublicKeys?: RelayHostPublicKey[];
|
|
71
|
+
/**
|
|
72
|
+
* `kid` to embed in the protected header of freshly minted RHTs. When
|
|
73
|
+
* provided, verifiers can dispatch on `kid` to pick the matching key from
|
|
74
|
+
* the published JWKS. Should match the `kid` of the first entry in
|
|
75
|
+
* `relayHostPublicKeys` so JWKS and mint agree by construction.
|
|
76
|
+
*/
|
|
77
|
+
relayHostMintKid?: string;
|
|
78
|
+
};
|
|
79
|
+
export type WorkspaceRelayRoutingOptions = {
|
|
80
|
+
resolveTarget: (claims: RuntimeAccessTokenClaims) => WorkspaceRelayTarget | Promise<WorkspaceRelayTarget | undefined>;
|
|
81
|
+
fetch?: typeof fetch;
|
|
82
|
+
directory?: WorkspaceRelayDirectory;
|
|
83
|
+
};
|
|
84
|
+
export type WorkspaceRelayDrainOptions = {
|
|
85
|
+
/**
|
|
86
|
+
* T9: when this returns true the relay reports unhealthy via `/health` and
|
|
87
|
+
* fast-paths every workspace request to a 503 (`relay_draining`) before
|
|
88
|
+
* doing any auth or upstream work. Wired by `createWorkspaceRelayBun` to
|
|
89
|
+
* the drain controller it exposes.
|
|
90
|
+
*/
|
|
91
|
+
isDraining?: () => boolean;
|
|
92
|
+
};
|
|
93
|
+
export type WorkspaceRelayTelemetryOptions = {
|
|
94
|
+
audit?: (event: WorkspaceRelayAuditEvent) => void | Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* T16: probability (0.0 – 1.0) at which `relay.request.accepted` audit
|
|
97
|
+
* events are emitted to the user `audit` callback. Denies and tunnel
|
|
98
|
+
* lifecycle events are never sampled. When unset, defaults to 1.0
|
|
99
|
+
* (emit every accept). Suppressed accepts are aggregated into a periodic
|
|
100
|
+
* `relay.request.suppressed_summary` event (see `auditFlushIntervalMs`).
|
|
101
|
+
*/
|
|
102
|
+
auditAcceptSampleRate?: number;
|
|
103
|
+
/**
|
|
104
|
+
* T16: how often (ms) the sampler emits a `relay.request.suppressed_summary`
|
|
105
|
+
* audit event with the count of suppressed accepts in the past interval.
|
|
106
|
+
* Defaults to 60_000 (one minute). When sample rate is 1.0 no interval is
|
|
107
|
+
* armed (nothing to flush). Exposed for tests.
|
|
108
|
+
*/
|
|
109
|
+
auditFlushIntervalMs?: number;
|
|
110
|
+
/**
|
|
111
|
+
* T16: random source for the sampler. Defaults to `Math.random`. Override
|
|
112
|
+
* for deterministic tests.
|
|
113
|
+
*/
|
|
114
|
+
random?: () => number;
|
|
115
|
+
};
|
|
116
|
+
export type WorkspaceRelayMetricsOptions = {
|
|
117
|
+
/**
|
|
118
|
+
* T31: bearer token required to access `GET /metrics` from non-loopback
|
|
119
|
+
* remotes. When unset, the endpoint allows loopback callers only (using
|
|
120
|
+
* `metricsRemoteAddress` to identify the caller). When set, every request
|
|
121
|
+
* must present `Authorization: Bearer <metricsToken>` regardless of origin.
|
|
122
|
+
* Wired from `CLAXEDO_RELAY_METRICS_TOKEN` in main.ts.
|
|
123
|
+
*/
|
|
124
|
+
metricsToken?: string;
|
|
125
|
+
/**
|
|
126
|
+
* T31: returns the remote IP of the inbound `/metrics` request (used to
|
|
127
|
+
* gate access to the loopback when `metricsToken` is unset). When this is
|
|
128
|
+
* also unset, the relay falls back to "allow" for dev convenience — the
|
|
129
|
+
* production deploy is expected to either set `metricsToken` or wire this
|
|
130
|
+
* resolver to `Bun.Server.requestIP`.
|
|
131
|
+
*/
|
|
132
|
+
metricsRemoteAddress?: (request: Request) => string | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* T31: optional providers used to assemble the `/metrics` response body. The
|
|
135
|
+
* fragmentation/slow-consumer counters live in the bun adapter (module-
|
|
136
|
+
* scoped state in `bun.ts`), and the in-flight pending count lives on the
|
|
137
|
+
* drain controller — `createWorkspaceRelayBun` wires these to make ops
|
|
138
|
+
* counters visible on `/metrics` without giving `server.ts` a hard
|
|
139
|
+
* dependency on the bun-only state.
|
|
140
|
+
*/
|
|
141
|
+
metricsSources?: WorkspaceRelayMetricsSources;
|
|
142
|
+
};
|
|
143
|
+
export type WorkspaceRelayOptions = WorkspaceRelayAuthOptions & WorkspaceRelayHostTokenOptions & WorkspaceRelayRoutingOptions & WorkspaceRelayDrainOptions & WorkspaceRelayTelemetryOptions & WorkspaceRelayMetricsOptions;
|
|
144
|
+
export type WorkspaceRelayMetricsSources = {
|
|
145
|
+
fragmentation?: () => {
|
|
146
|
+
fragmentsBuffered: number;
|
|
147
|
+
oversizedClosed: number;
|
|
148
|
+
};
|
|
149
|
+
slowConsumer?: () => {
|
|
150
|
+
overflowEvents: number;
|
|
151
|
+
timerFired: number;
|
|
152
|
+
droppedRequests: number;
|
|
153
|
+
};
|
|
154
|
+
drainPending?: () => number;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* T31: shape of the JSON body returned by `GET /metrics`. Exported so the
|
|
158
|
+
* bun adapter and ops dashboards can type-narrow against it.
|
|
159
|
+
*/
|
|
160
|
+
export type WorkspaceRelayMetrics = {
|
|
161
|
+
fragmentation: {
|
|
162
|
+
fragmentsBuffered: number;
|
|
163
|
+
oversizedClosed: number;
|
|
164
|
+
};
|
|
165
|
+
slowConsumer: {
|
|
166
|
+
overflowEvents: number;
|
|
167
|
+
timerFired: number;
|
|
168
|
+
droppedRequests: number;
|
|
169
|
+
};
|
|
170
|
+
directory: {
|
|
171
|
+
activeHostCount: number;
|
|
172
|
+
};
|
|
173
|
+
audit: {
|
|
174
|
+
suppressedAccepts: number;
|
|
175
|
+
};
|
|
176
|
+
drain?: {
|
|
177
|
+
pendingCount: number;
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
export type AuthorizedWorkspaceRelayRequest = {
|
|
181
|
+
claims: RuntimeAccessTokenClaims;
|
|
182
|
+
target: WorkspaceRelayTarget;
|
|
183
|
+
relayHostToken: string;
|
|
184
|
+
path: string;
|
|
185
|
+
};
|
|
186
|
+
export type WorkspaceRelayForwardHeadersOptions = {
|
|
187
|
+
/**
|
|
188
|
+
* When true, also strip the `Cookie` request header. User-hosted workspaces
|
|
189
|
+
* run on the user's laptop where the host process may share a cookie jar
|
|
190
|
+
* with the browser (e.g. localhost dev tooling); passing through cookies
|
|
191
|
+
* verbatim risks leaking sensitive session data to the host. Cloud-vm
|
|
192
|
+
* workspaces sit behind a dedicated network boundary where session cookies
|
|
193
|
+
* may legitimately be needed (e.g. workspace dashboards), so the default
|
|
194
|
+
* is to NOT strip cookies. T26.
|
|
195
|
+
*/
|
|
196
|
+
userHosted?: boolean;
|
|
197
|
+
};
|
|
198
|
+
export declare function workspaceRelayTargetUrl(target: WorkspaceRelayTarget, path: string, search: string): URL;
|
|
199
|
+
export declare function workspaceRelayForwardHeaders(input: Headers, relayHostToken: string, workspaceId: string, options?: WorkspaceRelayForwardHeadersOptions): Headers;
|
|
200
|
+
/**
|
|
201
|
+
* T16: dispose the sampler's periodic flush timer for a given options object.
|
|
202
|
+
* Tests call this in cleanup so the bun process can exit.
|
|
203
|
+
*/
|
|
204
|
+
export declare function disposeAuditSampler(options: WorkspaceRelayOptions): void;
|
|
205
|
+
export declare function authorizeWorkspaceRelayRequest(options: WorkspaceRelayOptions, request: Request, workspaceId: string): Promise<{
|
|
206
|
+
ok: true;
|
|
207
|
+
request: AuthorizedWorkspaceRelayRequest;
|
|
208
|
+
} | {
|
|
209
|
+
ok: false;
|
|
210
|
+
response: Response;
|
|
211
|
+
}>;
|
|
212
|
+
export type WorkspaceRelayApp = Hono & {
|
|
213
|
+
/**
|
|
214
|
+
* T16: clear the sampler's periodic flush timer. Tests must call this so
|
|
215
|
+
* `setInterval` doesn't keep the process alive after the test exits.
|
|
216
|
+
*/
|
|
217
|
+
disposeAuditSampler(): void;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* T31: read the suppressed-accept counter for a given options object. Surfaced
|
|
221
|
+
* for the `/metrics` route handler. Returns 0 if no sampler state has been
|
|
222
|
+
* created yet (no requests have been audit-sampled), which is the same value
|
|
223
|
+
* an external observer would see.
|
|
224
|
+
*/
|
|
225
|
+
export declare function getAuditSuppressedCount(options: WorkspaceRelayOptions): number;
|
|
226
|
+
export declare function createWorkspaceRelay(options: WorkspaceRelayOptions): WorkspaceRelayApp;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T10: Synthetic end-to-end probe.
|
|
3
|
+
*
|
|
4
|
+
* Liveness depends on metrics; metrics can lie. A synthetic probe periodically
|
|
5
|
+
* exercises the relay's HTTP path and records RTT so we have an independent
|
|
6
|
+
* signal that the data path actually works.
|
|
7
|
+
*
|
|
8
|
+
* Implementation notes
|
|
9
|
+
* --------------------
|
|
10
|
+
* - The probe targets `${relayUrl}/health` rather than minting a Runtime
|
|
11
|
+
* Access Token and hitting `/workspaces/<id>/...`. `/health` validates HTTP
|
|
12
|
+
* connectivity end-to-end without pretending the Relay owns Control Plane
|
|
13
|
+
* token issuance.
|
|
14
|
+
* - All clocks, timers and `fetch` are injectable so tests stay deterministic.
|
|
15
|
+
* - "Loud" logs after 3 consecutive failures use `console.error` by default;
|
|
16
|
+
* pageability (PagerDuty / equivalent) is wired separately.
|
|
17
|
+
*/
|
|
18
|
+
export type SyntheticProbeStats = {
|
|
19
|
+
successCount: number;
|
|
20
|
+
failureCount: number;
|
|
21
|
+
consecutiveFailures: number;
|
|
22
|
+
/** RTT (ms) of the most recent probe attempt. Undefined before the first probe completes. */
|
|
23
|
+
lastRttMs?: number;
|
|
24
|
+
/** p50 / p99 over the rolling RTT window. Undefined if no successes yet. */
|
|
25
|
+
p50?: number;
|
|
26
|
+
p99?: number;
|
|
27
|
+
/** Number of RTTs currently in the rolling window. */
|
|
28
|
+
windowSize: number;
|
|
29
|
+
};
|
|
30
|
+
export type SyntheticProbe = {
|
|
31
|
+
stop(): void;
|
|
32
|
+
stats(): SyntheticProbeStats;
|
|
33
|
+
};
|
|
34
|
+
export type SyntheticProbeOptions = {
|
|
35
|
+
/** Base URL of the relay, e.g. `http://127.0.0.1:7777`. No trailing slash required. */
|
|
36
|
+
relayUrl: string;
|
|
37
|
+
/** Probe cadence. Default 60_000 (60 s). */
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
/** Summary log cadence (p50/p99). Default 300_000 (5 min). */
|
|
40
|
+
summaryIntervalMs?: number;
|
|
41
|
+
/** Path to probe. Default `/health`. */
|
|
42
|
+
probePath?: string;
|
|
43
|
+
/** Rolling window size for RTT percentile calculation. Default 100. */
|
|
44
|
+
rttWindowSize?: number;
|
|
45
|
+
/** Number of consecutive failures that triggers a loud log. Default 3. */
|
|
46
|
+
loudFailureThreshold?: number;
|
|
47
|
+
/** Override `fetch`. Defaults to global `fetch`. */
|
|
48
|
+
fetch?: typeof fetch;
|
|
49
|
+
/** Override clock for tests. Defaults to `Date.now`. */
|
|
50
|
+
now?: () => number;
|
|
51
|
+
/** Override `setInterval` for tests. Defaults to global `setInterval`. */
|
|
52
|
+
setInterval?: typeof setInterval;
|
|
53
|
+
/** Override `clearInterval` for tests. Defaults to global `clearInterval`. */
|
|
54
|
+
clearInterval?: typeof clearInterval;
|
|
55
|
+
/** Loud-failure logger. Defaults to `console.error`. */
|
|
56
|
+
logError?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
57
|
+
/** Periodic summary logger (p50/p99). Defaults to `console.log`. */
|
|
58
|
+
logSummary?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
59
|
+
};
|
|
60
|
+
export declare function startSyntheticProbe(options: SyntheticProbeOptions): SyntheticProbe;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@claxedo/workspace-relay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"workspace-relay": "./dist/main.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "bun run src/main.ts",
|
|
18
|
+
"dev": "bun run --hot src/main.ts",
|
|
19
|
+
"build": "tsx scripts/build.ts",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
22
|
+
"test": "bun test src"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@claxedo/workspace-relay-protocol": "^0.1.0",
|
|
26
|
+
"hono": "4.10.7",
|
|
27
|
+
"jose": "6.0.11"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@tsconfig/node-lts": "^22.0.0",
|
|
31
|
+
"@types/bun": "catalog:",
|
|
32
|
+
"esbuild": "^0.25.0",
|
|
33
|
+
"tsx": "^4.0.0",
|
|
34
|
+
"typescript": "catalog:"
|
|
35
|
+
}
|
|
36
|
+
}
|