@claxedo/workspace-relay 0.1.0 → 0.5.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/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
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";
2
+ import type { RuntimeAccessVerifierClaims, TokenVerifier } from "@claxedo/workspace-relay-protocol";
3
+ import { type RelayClaimPair, type RelayJwtAlgorithm, type RelayKey, type RelayRole, type RuntimeAccessTokenClaims } from "./auth";
4
4
  import type { WorkspaceRelayDirectory } from "./directory";
5
5
  export type RelayHostPublicKey = {
6
6
  publicKey: CryptoKey | Uint8Array;
@@ -10,9 +10,8 @@ export type WorkspaceRelayTarget = {
10
10
  workspaceId: string;
11
11
  hostId: string;
12
12
  baseUrl: string;
13
- access: RelayAccess;
14
- backing: RelayBacking;
15
- };
13
+ upstreamHeaders?: Record<string, string>;
14
+ } & RelayClaimPair;
16
15
  export type RuntimeAccessTokenActiveResult = {
17
16
  active: true;
18
17
  } | {
@@ -20,6 +19,29 @@ export type RuntimeAccessTokenActiveResult = {
20
19
  code: string;
21
20
  reason: string;
22
21
  };
22
+ export type RevocationLookupArgs = {
23
+ jti: string;
24
+ workspaceId: string;
25
+ hostId: string;
26
+ };
27
+ export type RevocationLookup = (args: RevocationLookupArgs) => Promise<RuntimeAccessTokenActiveResult>;
28
+ export type CachedRevocationOptions = {
29
+ /** TTL in milliseconds for cached revocation responses. Defaults to 10_000. */
30
+ ttlMs?: number;
31
+ /** Clock injection for tests. Defaults to `Date.now`. */
32
+ now?: () => number;
33
+ };
34
+ export type TargetLookupArgs = {
35
+ workspaceId: string;
36
+ hostId: string;
37
+ };
38
+ export type TargetLookup = (args: TargetLookupArgs) => Promise<WorkspaceRelayTarget | undefined>;
39
+ export type CachedTargetOptions = {
40
+ /** TTL in milliseconds for positive target responses. Defaults to 5_000. */
41
+ ttlMs?: number;
42
+ /** Clock injection for tests. Defaults to `Date.now`. */
43
+ now?: () => number;
44
+ };
23
45
  export type WorkspaceRelayAuditEvent = {
24
46
  action: "relay.request.accepted" | "relay.request.denied" | "relay.request.suppressed_summary" | "host_tunnel.connected" | "host_tunnel.disconnected";
25
47
  result: "allow" | "deny";
@@ -39,6 +61,13 @@ export type WorkspaceRelayAuditEvent = {
39
61
  };
40
62
  export type WorkspaceRelayAuthOptions = {
41
63
  runtimeAccessKey: RelayKey;
64
+ /**
65
+ * Cache signature/introspection-verified Runtime Access Token claims by the
66
+ * full token string. Revocation/active checks, role enforcement, and target
67
+ * resolution still run for every request after this cache hit. Defaults to
68
+ * 10s and is always capped by the token's exp claim minus skew.
69
+ */
70
+ runtimeAccessTokenCacheTtlMs?: number;
42
71
  /**
43
72
  * P-shared: optional override for runtime-access-token verification.
44
73
  *
@@ -54,7 +83,7 @@ export type WorkspaceRelayAuthOptions = {
54
83
  * See `@claxedo/workspace-relay-protocol/token-verifier` for the
55
84
  * `HttpTokenVerifier` and `StaticTokenVerifier` reference impls.
56
85
  */
57
- tokenVerifier?: TokenVerifier;
86
+ tokenVerifier?: TokenVerifier<RuntimeAccessVerifierClaims>;
58
87
  isRuntimeAccessTokenActive?: (claims: RuntimeAccessTokenClaims) => RuntimeAccessTokenActiveResult | Promise<RuntimeAccessTokenActiveResult>;
59
88
  };
60
89
  export type WorkspaceRelayHostTokenOptions = {
@@ -75,10 +104,18 @@ export type WorkspaceRelayHostTokenOptions = {
75
104
  * `relayHostPublicKeys` so JWKS and mint agree by construction.
76
105
  */
77
106
  relayHostMintKid?: string;
107
+ /**
108
+ * Cache Relay Host Tokens minted from an already-verified Runtime Access
109
+ * Token. The relay still verifies the RAT, checks revocation, enforces role,
110
+ * and resolves the target on every request; this only avoids repeated signing
111
+ * work for the same jti/target after those gates pass. Defaults to 30s.
112
+ */
113
+ relayHostTokenCacheTtlMs?: number;
78
114
  };
79
115
  export type WorkspaceRelayRoutingOptions = {
80
116
  resolveTarget: (claims: RuntimeAccessTokenClaims) => WorkspaceRelayTarget | Promise<WorkspaceRelayTarget | undefined>;
81
117
  fetch?: typeof fetch;
118
+ forwardTimeoutMs?: number;
82
119
  directory?: WorkspaceRelayDirectory;
83
120
  };
84
121
  export type WorkspaceRelayDrainOptions = {
@@ -125,18 +162,17 @@ export type WorkspaceRelayMetricsOptions = {
125
162
  /**
126
163
  * T31: returns the remote IP of the inbound `/metrics` request (used to
127
164
  * 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`.
165
+ * also unset, `/metrics` fails closed. The Bun adapter wires this resolver
166
+ * through `Bun.Server.requestIP`; other callers should set either this or
167
+ * `metricsToken`.
131
168
  */
132
169
  metricsRemoteAddress?: (request: Request) => string | undefined;
133
170
  /**
134
171
  * 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.
172
+ * fragmentation/slow-consumer counters live in the bun adapter's per-handler
173
+ * telemetry, and the in-flight pending count lives on the drain controller.
174
+ * `createWorkspaceRelayBun` wires these to make ops counters visible on
175
+ * `/metrics` without giving `server.ts` a hard dependency on bun-only state.
140
176
  */
141
177
  metricsSources?: WorkspaceRelayMetricsSources;
142
178
  };
@@ -183,6 +219,28 @@ export type AuthorizedWorkspaceRelayRequest = {
183
219
  relayHostToken: string;
184
220
  path: string;
185
221
  };
222
+ export type WorkspaceRelayAuthorizeTrace = {
223
+ span<T>(name: string, run: () => Promise<T>): Promise<T>;
224
+ };
225
+ export type WorkspaceRelayTracePhase = {
226
+ name: string;
227
+ ms: number;
228
+ };
229
+ export type WorkspaceRelayTrace = WorkspaceRelayAuthorizeTrace & {
230
+ phases: WorkspaceRelayTracePhase[];
231
+ };
232
+ /**
233
+ * Wraps a revocation lookup with a small in-memory cache keyed by `jti`.
234
+ * Both positive and negative results are cached for the full TTL; revocation
235
+ * lag is bounded by the same TTL by design. Concurrent misses share one lookup.
236
+ */
237
+ export declare function createCachedRevocationClient(inner: RevocationLookup, options?: CachedRevocationOptions): RevocationLookup;
238
+ /**
239
+ * Caches positive workspace target lookups by workspace+host. Missing targets
240
+ * are only coalesced while in flight, not retained, so cold-start polling can
241
+ * see readiness as soon as the control plane records it.
242
+ */
243
+ export declare function createCachedTargetClient(inner: TargetLookup, options?: CachedTargetOptions): TargetLookup;
186
244
  export type WorkspaceRelayForwardHeadersOptions = {
187
245
  /**
188
246
  * When true, also strip the `Cookie` request header. User-hosted workspaces
@@ -194,21 +252,31 @@ export type WorkspaceRelayForwardHeadersOptions = {
194
252
  * is to NOT strip cookies. T26.
195
253
  */
196
254
  userHosted?: boolean;
255
+ upstreamHeaders?: Record<string, string>;
256
+ };
257
+ export type WorkspaceRelayForwardRequestInitOptions = WorkspaceRelayForwardHeadersOptions & {
258
+ signal?: AbortSignal;
197
259
  };
198
260
  export declare function workspaceRelayTargetUrl(target: WorkspaceRelayTarget, path: string, search: string): URL;
199
261
  export declare function workspaceRelayForwardHeaders(input: Headers, relayHostToken: string, workspaceId: string, options?: WorkspaceRelayForwardHeadersOptions): Headers;
262
+ export declare function workspaceRelayForwardRequestInit(request: Request, relayHostToken: string, workspaceId: string, options?: WorkspaceRelayForwardRequestInitOptions): RequestInit;
200
263
  /**
201
264
  * T16: dispose the sampler's periodic flush timer for a given options object.
202
265
  * Tests call this in cleanup so the bun process can exit.
203
266
  */
204
267
  export declare function disposeAuditSampler(options: WorkspaceRelayOptions): void;
205
- export declare function authorizeWorkspaceRelayRequest(options: WorkspaceRelayOptions, request: Request, workspaceId: string): Promise<{
268
+ export declare function createWorkspaceRelayTrace(): WorkspaceRelayTrace;
269
+ export declare function workspaceRelayServerTiming(trace: WorkspaceRelayTrace): string;
270
+ export declare function workspaceRelayTimingResponse(response: Response, trace: WorkspaceRelayTrace): Response;
271
+ export declare function authorizeWorkspaceRelayRequest(options: WorkspaceRelayOptions, request: Request, workspaceId: string, trace?: WorkspaceRelayAuthorizeTrace): Promise<{
206
272
  ok: true;
207
273
  request: AuthorizedWorkspaceRelayRequest;
208
274
  } | {
209
275
  ok: false;
210
276
  response: Response;
277
+ code: string;
211
278
  }>;
279
+ export declare function forwardWorkspaceRelayRequest(request: Request, target: WorkspaceRelayTarget, relayHostToken: string, path: string, requestFetch: typeof fetch, timeoutMs: number, trace?: WorkspaceRelayAuthorizeTrace): Promise<Response>;
212
280
  export type WorkspaceRelayApp = Hono & {
213
281
  /**
214
282
  * T16: clear the sampler's periodic flush timer. Tests must call this so
@@ -0,0 +1,50 @@
1
+ import { type WorkspaceRelayDurableObjectNamespace, type WorkspaceRelayDurableObjectRoomOptions, type WorkspaceRelayDurableObjectSocket } from "./cloudflare";
2
+ import type { RuntimeAccessTokenActiveResult, WorkspaceRelayTarget } from "./server";
3
+ export type WorkspaceRelayWorkerEnv = Record<string, unknown> & {
4
+ WORKSPACE_RELAY_ROOM?: WorkspaceRelayDurableObjectNamespace;
5
+ CLAXEDO_RELAY_RESOLVER_URL?: string;
6
+ CLAXEDO_CENTRAL_URL?: string;
7
+ CLAXEDO_RELAY_RESOLVER_TOKEN?: string;
8
+ CLAXEDO_CONTROL_PLANE_JWKS_URL?: string;
9
+ CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM?: string;
10
+ CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM?: string;
11
+ CLAXEDO_RELAY_HOST_PUBLIC_KEY_PEM?: string;
12
+ CLAXEDO_RELAY_HOST_NEXT_PUBLIC_KEY_PEM?: string;
13
+ CLAXEDO_RELAY_HOST_KID?: string;
14
+ CLAXEDO_RELAY_HOST_NEXT_KID?: string;
15
+ CLAXEDO_RELAY_AUDIT_ACCEPT_SAMPLE_RATE?: string;
16
+ CLAXEDO_RELAY_TUNNEL_CHANNEL_CAP?: string;
17
+ CLAXEDO_RELAY_TRACE_SAMPLE_RATE?: string;
18
+ CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS?: string;
19
+ CLAXEDO_RELAY_TARGET_CACHE_TTL_MS?: string;
20
+ CLAXEDO_APP_ORIGINS?: string;
21
+ };
22
+ type ResolverClient = {
23
+ target(workspaceId: string, hostId: string): Promise<WorkspaceRelayTarget | undefined>;
24
+ revocation(args: {
25
+ jti: string;
26
+ workspaceId: string;
27
+ hostId: string;
28
+ }): Promise<RuntimeAccessTokenActiveResult>;
29
+ };
30
+ type ResolverFetch = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
31
+ export declare function workspaceRelayWorkerResolverUrl(env: WorkspaceRelayWorkerEnv): string;
32
+ export declare function workspaceRelayWorkerResolverClient(env: WorkspaceRelayWorkerEnv, fetcher?: ResolverFetch): ResolverClient;
33
+ export declare function workspaceRelayDurableObjectOptions(env: WorkspaceRelayWorkerEnv): Promise<WorkspaceRelayDurableObjectRoomOptions>;
34
+ export declare class WorkspaceRelayRoom {
35
+ private state;
36
+ private env;
37
+ private room?;
38
+ private loading?;
39
+ constructor(state: unknown, env: WorkspaceRelayWorkerEnv);
40
+ private hibernation;
41
+ private loadRoom;
42
+ fetch(request: Request): Promise<Response>;
43
+ webSocketMessage(socket: WorkspaceRelayDurableObjectSocket, message: string | ArrayBuffer): Promise<void>;
44
+ webSocketClose(socket: WorkspaceRelayDurableObjectSocket, code: number, reason: string, wasClean: boolean): Promise<void>;
45
+ webSocketError(socket: WorkspaceRelayDurableObjectSocket, error: unknown): Promise<void>;
46
+ }
47
+ declare const _default: {
48
+ fetch(request: Request, env: WorkspaceRelayWorkerEnv): Promise<Response>;
49
+ };
50
+ export default _default;
package/package.json CHANGED
@@ -1,13 +1,23 @@
1
1
  {
2
2
  "name": "@claxedo/workspace-relay",
3
- "version": "0.1.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "bun": "./dist/index.mjs",
11
+ "development": "./dist/index.mjs",
12
+ "import": "./dist/index.mjs",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
7
16
  "bin": {
8
- "workspace-relay": "./dist/main.mjs"
17
+ "workspace-relay": "dist/main.mjs"
9
18
  },
10
19
  "files": [
20
+ "README.md",
11
21
  "dist"
12
22
  ],
13
23
  "publishConfig": {
@@ -22,15 +32,15 @@
22
32
  "test": "bun test src"
23
33
  },
24
34
  "dependencies": {
25
- "@claxedo/workspace-relay-protocol": "^0.1.0",
35
+ "@claxedo/workspace-relay-protocol": "0.5.0",
26
36
  "hono": "4.10.7",
27
37
  "jose": "6.0.11"
28
38
  },
29
39
  "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:"
40
+ "@tsconfig/node-lts": "22.0.4",
41
+ "@types/bun": "1.3.13",
42
+ "esbuild": "0.25.12",
43
+ "tsx": "4.22.3",
44
+ "typescript": "5.8.2"
35
45
  }
36
46
  }