@claxedo/workspace-relay 0.10.2 → 0.10.3

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Exported for `relay-base64.test.ts`, which proves byte-equality against a
3
+ * per-byte reference across the chunk boundaries. This sits on the path every
4
+ * frame pays, so its equivalence is worth asserting directly rather than
5
+ * inferring from a round-trip.
6
+ */
7
+ export declare function bytesToBase64(bytes: Uint8Array): string;
8
+ export declare function base64ToBytes(input: string): Uint8Array<ArrayBuffer>;
9
+ export declare function concatBytes(chunks: Uint8Array[]): Uint8Array<ArrayBuffer>;
10
+ export declare function readRequestBodyCapped(request: Request, maxBytes: number): Promise<"too_large" | Uint8Array<ArrayBuffer>>;
@@ -12,7 +12,7 @@ export declare const DEFAULT_RELAY_LOCATION_HINT = "apac";
12
12
  *
13
13
  * The room sits on the path of every request to the object and asks the
14
14
  * control plane (IAD) for the token's revocation and the target on a cold
15
- * start and every 10 s: from apac each lookup is ~220 ms, from enam ~10 ms,
15
+ * start and every 30 s: from apac each lookup is ~220 ms, from enam ~10 ms,
16
16
  * while the browser's path through the room is the same ~115 ms either way
17
17
  * (docs/perf/pi-durable-latency-2026-10-18/README.md). A machine's room stays
18
18
  * beside its machine, where those lookups would cost a browser in apac
@@ -28,7 +28,6 @@ export declare const SESSION_HOST_LOCATION_HINT = "enam";
28
28
  */
29
29
  export declare const RELAY_LOCATION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
30
30
  export type RelayLocationHint = typeof RELAY_LOCATION_HINTS[number];
31
- /** A workspace region carried on the request, if the caller supplied one. */
32
31
  export declare const RELAY_REGION_HEADER = "x-claxedo-workspace-region";
33
32
  /**
34
33
  * Chooses the location hint for a workspace's Durable Object.
@@ -40,11 +39,13 @@ export declare const RELAY_REGION_HEADER = "x-claxedo-workspace-region";
40
39
  * passed through or guessed at.
41
40
  *
42
41
  * Precedence, most authoritative first:
43
- * 1. the workspace's own region (header or `?region=`), which is what the
44
- * control plane knows and the only signal tied to the WORKSPACE rather than
45
- * to whoever happens to be connecting first;
46
- * 2. the requesting user's Cloudflare edge country, a decent proxy when the
47
- * workspace has no region yet;
42
+ * 1. the workspace's own region (header or `?region=`). The caller passes it
43
+ * only where the region is a fact about the runtime: see
44
+ * `RelayPlacementCredential.declaresRegion`;
45
+ * 2. the requesting user's Cloudflare edge country. A control-plane Worker
46
+ * calling on a person's behalf carries that person's country, not its own
47
+ * (measured: a Worker placed in IAD, called from India, reaches the next
48
+ * Worker with `cf.country` IN);
48
49
  * 3. the deployment-wide configured hint (`CLAXEDO_RELAY_LOCATION_HINT`), then
49
50
  * `DEFAULT_RELAY_LOCATION_HINT`.
50
51
  */
@@ -53,7 +54,6 @@ export declare function relayLocationHint(input: {
53
54
  region?: string;
54
55
  country?: string;
55
56
  }): string;
56
- /** The workspace region a request declares, if any. */
57
57
  export declare function relayRequestRegion(request: Request): string | undefined;
58
58
  export type RelayRoomPlacement = {
59
59
  locationHint: string;
@@ -66,9 +66,18 @@ export type RelayRoomPlacement = {
66
66
  * room's workspace: the host it reaches. A `session-do:` host is the control
67
67
  * plane's signed statement that the workspace's sessions run on their own
68
68
  * Durable Objects.
69
+ *
70
+ * `declaresRegion` says whether a region on the request is a fact about the
71
+ * runtime. It is for a cloud runtime, whose token carries the routing id the
72
+ * control plane assigned with its sandbox, and for a machine's own tunnel
73
+ * dial. It is not for anyone else's request to a machine: a machine workspace
74
+ * has no home region, the control plane fills the column with the deployment
75
+ * default (`us-east`), and a room placed by that default sits 228 ms from a
76
+ * machine and owner who are both in India, for good.
69
77
  */
70
78
  export type RelayPlacementCredential = {
71
79
  hostId: string;
80
+ declaresRegion: boolean;
72
81
  };
73
82
  /**
74
83
  * Where a workspace's room is created, decided from what the request declares
@@ -83,11 +92,11 @@ export type RelayPlacementCredential = {
83
92
  * drag the room elsewhere, and a forged token verifies for no workspace.
84
93
  * Request-declared inputs (region, edge country) move the hint off the
85
94
  * configured floor only when the credential verifies, so anonymous first-touch
86
- * traffic cannot pin a workspace's room placement.
95
+ * traffic cannot pin a workspace's room placement. The region counts only from
96
+ * a credential that `declaresRegion`; the edge country counts from any.
87
97
  */
88
98
  export declare function relayRoomPlacement(request: Request, input: {
89
99
  configured?: string;
90
100
  credential: () => Promise<RelayPlacementCredential | undefined>;
91
101
  }): Promise<RelayRoomPlacement>;
92
- /** The Cloudflare edge country for the request, when running on Cloudflare. */
93
102
  export declare function relayRequestCountry(request: Request): string | undefined;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * What the relay records about a machine's tunnel. A relayed request that
3
+ * succeeds quickly writes nothing; these are the changes and failures that
4
+ * explain why a machine could not be reached.
5
+ */
6
+ export type RelayLogEvent = {
7
+ event: "tunnel_admitted";
8
+ hostId: string;
9
+ workspaceIds: string[];
10
+ generation: number;
11
+ replacedAgeMs?: number;
12
+ } | {
13
+ event: "tunnel_refused";
14
+ hostId?: string;
15
+ code: string;
16
+ } | {
17
+ event: "tunnel_ended";
18
+ hostId: string;
19
+ generation: number;
20
+ reason: string;
21
+ ageMs: number;
22
+ pendingFailed: number;
23
+ channelsClosed: number;
24
+ closeCode?: number;
25
+ closeReason?: string;
26
+ socketError?: string;
27
+ } | {
28
+ event: "tunnel_timeout";
29
+ hostId: string;
30
+ traceId?: string;
31
+ requestId: string;
32
+ method: string;
33
+ path: string;
34
+ bodyBytes: number;
35
+ timeoutMs: number;
36
+ tunnelAgeMs: number;
37
+ pending: number;
38
+ }
39
+ /** The machine answered a request the relay had already timed out: it was slow, not gone. */
40
+ | {
41
+ event: "tunnel_late_response";
42
+ hostId: string;
43
+ requestId: string;
44
+ lateMs: number;
45
+ } | {
46
+ event: "tunnel_frame_dropped";
47
+ hostId: string;
48
+ reason: string;
49
+ dropped: number;
50
+ type?: string;
51
+ protocol?: string;
52
+ };
53
+ export type RelayLog = (event: RelayLogEvent) => void;
54
+ export declare function relayLogOf(options: {
55
+ relayLog?: RelayLog;
56
+ }): RelayLog;
57
+ /** A machine speaking another protocol drops every frame, so only the first few and then each hundredth are written. */
58
+ export declare function frameDropWorthLogging(dropped: number): boolean;
59
+ /** How many timed-out request ids a tunnel remembers so a late answer can be told from an unknown one. */
60
+ export declare const TIMED_OUT_REQUESTS_REMEMBERED = 32;
61
+ export declare function rememberTimedOut(timedOut: Map<string, number>, requestId: string, at: number): void;
@@ -19,6 +19,8 @@ export type WorkspaceRelayTraceLogEvent = {
19
19
  export type RelayTraceSampling = {
20
20
  traceSampleRate?: number;
21
21
  traceForceHeaderSecret?: string;
22
+ /** The data center the room runs in once it is known; asked on a sampled trace only. */
23
+ roomColo?: () => string | undefined;
22
24
  random?: () => number;
23
25
  };
24
26
  export type RelayTrace = {
@@ -30,6 +32,7 @@ export type RelayTrace = {
30
32
  name: string;
31
33
  ms: number;
32
34
  }>;
35
+ roomColo?: string;
33
36
  };
34
37
  export declare function traceRequest(request: Request, routeKind: string, options: RelayTraceSampling): RelayTrace;
35
38
  export declare function traceSpan<T>(trace: RelayTrace | undefined, name: string, fn: () => Promise<T>): Promise<T>;
@@ -49,6 +52,8 @@ export declare function webSocketTraceFrame(upstreamStartedAt: number, queueTime
49
52
  maxQueuedDelayMs: number;
50
53
  };
51
54
  export declare function withTraceHeaders(response: Response, trace: RelayTrace): Response;
55
+ /** A request that fails or takes longer than this is written whether or not its trace was sampled. */
56
+ export declare const SLOW_REQUEST_MS = 5000;
52
57
  export declare function logTrace(options: {
53
58
  traceLog?: (event: WorkspaceRelayTraceLogEvent) => void;
54
59
  }, request: Request, workspaceId: string | undefined, trace: RelayTrace, response: Response): void;
@@ -31,9 +31,8 @@ export type RevocationLookupArgs = {
31
31
  };
32
32
  export type RevocationLookup = (args: RevocationLookupArgs) => Promise<RuntimeAccessTokenActiveResult>;
33
33
  export type CachedRevocationOptions = {
34
- /** TTL in milliseconds for cached revocation responses. Defaults to 10_000. */
34
+ /** TTL in milliseconds for cached revocation responses. Defaults to 30_000. */
35
35
  ttlMs?: number;
36
- /** Clock injection for tests. Defaults to `Date.now`. */
37
36
  now?: () => number;
38
37
  };
39
38
  /**
@@ -51,7 +50,6 @@ export type HostGenerationLookup = (args: HostGenerationLookupArgs) => Promise<H
51
50
  export type CachedHostGenerationOptions = {
52
51
  /** TTL in milliseconds for cached host-generation answers. Defaults to 10_000. */
53
52
  ttlMs?: number;
54
- /** Clock injection for tests. Defaults to `Date.now`. */
55
53
  now?: () => number;
56
54
  };
57
55
  export type TargetLookupArgs = {
@@ -63,12 +61,29 @@ export type TargetLookup = (args: TargetLookupArgs) => Promise<WorkspaceRelayTar
63
61
  export declare function pruneExpiringCache<T extends {
64
62
  expiresAt: number;
65
63
  }>(cache: Map<string, T>, now: number, maxEntries: number): void;
64
+ /** The code of an answer that says the authority could not be asked, as opposed to one that says no. */
65
+ export declare const REVOCATION_RESOLVER_UNAVAILABLE = "relay_revocation_resolver_unavailable";
66
+ export type RuntimeAccessTokenStanding = {
67
+ result: RuntimeAccessTokenActiveResult;
68
+ checkedAt: number;
69
+ };
70
+ export type CachedRevocationLookup = RevocationLookup & {
71
+ /**
72
+ * The token's standing as the authority gave it no more than `maxAgeMs`
73
+ * ago, asked now when the held answer is older. A request that arrives
74
+ * while it is being asked still gets the held answer until that expires, and
75
+ * an authority that cannot be reached leaves the held answer in place.
76
+ */
77
+ within(args: RevocationLookupArgs, maxAgeMs: number): Promise<RuntimeAccessTokenStanding>;
78
+ /** How long an answer is held, which is the bound on how late a revocation is seen. */
79
+ ttlMs: number;
80
+ };
66
81
  /**
67
82
  * Wraps a revocation lookup with a small in-memory cache keyed by `jti`.
68
83
  * Both positive and negative results are cached for the full TTL; revocation
69
84
  * lag is bounded by the same TTL by design. Concurrent misses share one lookup.
70
85
  */
71
- export declare function createCachedRevocationClient(inner: RevocationLookup, options?: CachedRevocationOptions): RevocationLookup;
86
+ export declare function createCachedRevocationClient(inner: RevocationLookup, options?: CachedRevocationOptions): CachedRevocationLookup;
72
87
  /**
73
88
  * The longest a revoked Runtime Access Token can keep working after the
74
89
  * authority records the revocation, derived from the configured TTLs rather
package/dist/server.d.ts CHANGED
@@ -34,23 +34,15 @@ export type WorkspaceRelayAuthOptions = {
34
34
  * Cache signature/introspection-verified Runtime Access Token claims by the
35
35
  * full token string. Revocation/active checks, role enforcement, and target
36
36
  * resolution still run for every request after this cache hit. Defaults to
37
- * 10s and is always capped by the token's exp claim minus skew.
37
+ * 30s and is always capped by the token's exp claim minus skew.
38
38
  */
39
39
  runtimeAccessTokenCacheTtlMs?: number;
40
40
  /**
41
- * P-shared: optional override for runtime-access-token verification.
42
- *
43
- * When set, the relay calls this verifier instead of decoding the JWT
44
- * with `runtimeAccessKey`. The verifier returns `{ subject, scopes,
45
- * claims }`; the relay continues to use `claims` to drive
46
- * `resolveTarget` and the workspace/host audit fields. `runtimeAccessKey`
47
- * is then only used for legacy token paths and tests that exercise the
48
- * raw JWT codepath.
49
- *
50
- * This is the seam downstream users plug their own auth backend
51
- * through (issuer-signed tokens, OIDC introspection, custom keys, etc.).
52
- * See `@claxedo/workspace-relay-protocol/token-verifier` for the
53
- * `HttpTokenVerifier` and `StaticTokenVerifier` reference impls.
41
+ * Verifies Runtime Access Tokens in place of decoding the JWT with
42
+ * `runtimeAccessKey`, for a deployment with its own auth backend (issuer
43
+ * tokens, OIDC introspection). Its `claims` drive `resolveTarget` and the
44
+ * audit fields. `@claxedo/workspace-relay-protocol/token-verifier` has HTTP,
45
+ * OIDC and static implementations.
54
46
  */
55
47
  tokenVerifier?: TokenVerifier<RuntimeAccessVerifierClaims>;
56
48
  isRuntimeAccessTokenActive?: (claims: RuntimeAccessTokenClaims) => RuntimeAccessTokenActiveResult | Promise<RuntimeAccessTokenActiveResult>;
@@ -0,0 +1,47 @@
1
+ import type { TunnelWsFrame } from "@claxedo/workspace-relay-protocol";
2
+ /** What a frame awaiting the upstream socket costs the pre-open byte bound. */
3
+ export declare function preOpenFrameBytes(input: string | ArrayBuffer | Uint8Array): number;
4
+ /**
5
+ * Socket `data` → a tunnel `ws.frame` payload. Returns a promise only for
6
+ * `Blob` data, which a listener-path socket delivers for a binary frame at
7
+ * `compatibility_date >= 2026-03-17`, the relay's deployed date among them;
8
+ * string and ArrayBuffer frames stay synchronous and pay no extra microtask.
9
+ */
10
+ export declare function socketFrame(input: unknown): Promise<{
11
+ binary: boolean;
12
+ data_base64: string;
13
+ }> | {
14
+ binary: boolean;
15
+ data_base64: string;
16
+ } | undefined;
17
+ /**
18
+ * Wraps a per-socket frame handler so frames are delivered in ARRIVAL order
19
+ * even when some of them resolve asynchronously.
20
+ *
21
+ * `socketPayload` is synchronous for string/ArrayBuffer/view data and only
22
+ * returns a promise for `Blob`. Awaiting that promise inline inside the
23
+ * listener would let the next frame — resolved synchronously — overtake it,
24
+ * silently reordering an ordered byte stream: a terminal would render its
25
+ * output scrambled. So the first pending Blob installs a tail promise and every
26
+ * later frame chains onto it until the chain drains, at which point the
27
+ * synchronous fast path resumes.
28
+ */
29
+ export declare function orderedFrameDelivery(deliver: (payload: string | ArrayBuffer | Uint8Array) => void): (data: unknown) => void;
30
+ /**
31
+ * Runs the two frame decoders over one runtime-delivered `event.data` and
32
+ * reports what they produced.
33
+ *
34
+ * Exists so `relay-workerd-binary.test.ts` can assert the decoders against what
35
+ * REAL workerd delivers. The rest of the suite drives hand-rolled socket
36
+ * doubles, which by construction hand us whatever we chose to hand ourselves —
37
+ * so they cannot catch the hazard that binary `event.data` becomes a `Blob` at
38
+ * `compatibility_date >= 2026-03-17`. Test-support surface, not a relay path.
39
+ */
40
+ export declare function relayFrameDiagnostics(data: unknown): Promise<{
41
+ frameDropped: boolean;
42
+ payloadDropped: boolean;
43
+ binary?: boolean | undefined;
44
+ dataBase64?: string | undefined;
45
+ payloadBase64: string | undefined;
46
+ }>;
47
+ export declare function tunnelFrame(input: TunnelWsFrame): string | Uint8Array<ArrayBuffer>;
@@ -0,0 +1,24 @@
1
+ import type { RuntimeAccessTokenClaims } from "./auth";
2
+ import { type RuntimeAccessTokenStanding } from "./resolver-clients";
3
+ export type StreamStandingOptions = {
4
+ /** The token's standing no older than `maxAgeMs`; absent, a deployment has no revocation authority and streams are left alone. */
5
+ standing?: (claims: RuntimeAccessTokenClaims, maxAgeMs: number) => Promise<RuntimeAccessTokenStanding>;
6
+ /** The bound a request's own check keeps: no answer older than this lets bytes through. */
7
+ boundMs: number;
8
+ /** Consecutive failures to reach the authority a stream survives; `exp` still ends it. */
9
+ graceAttempts: number;
10
+ /** How long before the bound the authority is asked again; `STANDING_REFRESH_LEAD_MS` unless a test shortens the bound. */
11
+ leadMs?: number;
12
+ now?: () => number;
13
+ };
14
+ /**
15
+ * A response whose body stops when its token does.
16
+ *
17
+ * A request is authorized once, when it arrives, but an event stream stays
18
+ * open for hours on that one check. This keeps the answer it flows on inside
19
+ * the bound a new request would get, by asking the authority again shortly
20
+ * before the held answer ages out, and ends the body when the token is
21
+ * revoked or expires. Ending cancels the source, which frees a tunnel
22
+ * stream's slot and tells its host to stop.
23
+ */
24
+ export declare function guardStreamStanding(response: Response, claims: RuntimeAccessTokenClaims, options: StreamStandingOptions): Response;
package/dist/worker.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type WorkspaceRelayDurableObjectNamespace, type WorkspaceRelayDurableObjectRoomOptions, type WorkspaceRelayDurableObjectSocket } from "./cloudflare";
2
2
  import { type WorkspaceRelayTarget } from "./relay-target";
3
- import { type HostGenerationLookup, type RuntimeAccessTokenActiveResult } from "./resolver-clients";
3
+ import { type CachedRevocationLookup, type HostGenerationLookup } from "./resolver-clients";
4
4
  type WorkspaceRelayWorkerBindings = {
5
5
  WORKSPACE_RELAY_ROOM?: WorkspaceRelayDurableObjectNamespace;
6
6
  CLAXEDO_RELAY_RESOLVER_URL?: string;
@@ -32,29 +32,28 @@ type WorkspaceRelayWorkerBindings = {
32
32
  export type WorkspaceRelayWorkerEnv = Record<string, unknown> & WorkspaceRelayWorkerBindings;
33
33
  type ResolverClient = {
34
34
  target(workspaceId: string, hostId: string, routingId?: string): Promise<WorkspaceRelayTarget | undefined>;
35
- revocation(args: {
36
- jti: string;
37
- workspaceId: string;
38
- hostId: string;
39
- }): Promise<RuntimeAccessTokenActiveResult>;
35
+ revocation: CachedRevocationLookup;
40
36
  hostGeneration: HostGenerationLookup;
41
37
  };
42
38
  type ResolverFetch = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
43
39
  export declare function workspaceRelayWorkerResolverUrl(env: WorkspaceRelayWorkerEnv): string;
44
40
  export declare function workspaceRelayWorkerResolverClient(env: WorkspaceRelayWorkerEnv, fetcher?: ResolverFetch): ResolverClient;
45
- export declare function workspaceRelayDurableObjectOptions(env: WorkspaceRelayWorkerEnv): Promise<WorkspaceRelayDurableObjectRoomOptions>;
41
+ /** Everything the room needs from the environment; the Worker adds `report`, which is the object's own. */
42
+ export declare function workspaceRelayDurableObjectOptions(env: WorkspaceRelayWorkerEnv): Promise<Omit<WorkspaceRelayDurableObjectRoomOptions, "report">>;
46
43
  /**
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.
44
+ * The `DurableObjectState` workerd hands a room. Everything is optional because
45
+ * `cloudflare.ts` detects hibernation and alarms: an older runtime, or a
46
+ * harness standing in for one, may provide neither, and the room degrades
47
+ * rather than failing.
54
48
  */
55
49
  export type WorkspaceRelayRoomState = {
50
+ id?: {
51
+ name?: string;
52
+ };
56
53
  acceptWebSocket?: (socket: WorkspaceRelayDurableObjectSocket) => void;
57
54
  getWebSockets?: () => WorkspaceRelayDurableObjectSocket[];
55
+ /** Keeps a report's POST alive past the step that made it; absent in tests that construct the room directly. */
56
+ waitUntil?: (promise: Promise<unknown>) => void;
58
57
  storage?: {
59
58
  getAlarm?: () => Promise<number | null>;
60
59
  setAlarm?: (scheduledTime: number) => Promise<void>;
@@ -66,7 +65,18 @@ export declare class WorkspaceRelayRoom {
66
65
  private env;
67
66
  private room?;
68
67
  private loading?;
68
+ private readonly telemetry;
69
+ private colo?;
70
+ private coloLookup?;
69
71
  constructor(state: WorkspaceRelayRoomState, env: WorkspaceRelayWorkerEnv);
72
+ /** A fault of this room's code, under `system`; the room's own name is a workspace id and is digested by the sink. */
73
+ private report;
74
+ /**
75
+ * A Durable Object has no API for its own location. Cloudflare's trace page
76
+ * names the data center a request left from, which for a subrequest is the
77
+ * object's. The first sampled trace starts the lookup and goes without it.
78
+ */
79
+ private roomColo;
70
80
  private hibernation;
71
81
  /**
72
82
  * The DO alarm surface, used for the hibernation-safe revocation re-check.
@@ -85,8 +95,18 @@ export declare class WorkspaceRelayRoom {
85
95
  * it, so adding another alarm user here stays safe.
86
96
  */
87
97
  alarm(): Promise<void>;
98
+ /**
99
+ * The gateway reaches a workspace's room by one name. A room under any other
100
+ * name keeps the sockets it accepted, and a machine whose tunnel is healthy
101
+ * never dials again, so the workspace reads as offline for as long as this
102
+ * room holds on. Closed, the machine and its browsers dial through the
103
+ * gateway and meet in the room it names.
104
+ */
105
+ private unreachable;
88
106
  }
89
107
  declare const _default: {
90
- fetch(request: Request, env: WorkspaceRelayWorkerEnv): Promise<Response>;
108
+ fetch(request: Request, env: WorkspaceRelayWorkerEnv, context?: {
109
+ waitUntil(promise: Promise<unknown>): void;
110
+ }): Promise<Response>;
91
111
  };
92
112
  export default _default;
package/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "name": "@claxedo/workspace-relay",
36
- "version": "0.10.2",
36
+ "version": "0.10.3",
37
37
  "scripts": {
38
38
  "build": "tsx scripts/build.ts",
39
39
  "prepublishOnly": "npm run build",
@@ -41,8 +41,8 @@
41
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 node scripts/test-workerd.mjs"
42
42
  },
43
43
  "dependencies": {
44
- "@claxedo/helpers": "0.2.2",
45
- "@claxedo/workspace-relay-protocol": "0.10.2",
44
+ "@claxedo/helpers": "0.2.3",
45
+ "@claxedo/workspace-relay-protocol": "0.10.3",
46
46
  "jose": "6.0.11"
47
47
  },
48
48
  "devDependencies": {