@agent-relay/dashboard 2.0.92 → 2.0.93

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.
Files changed (65) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{5518-6d77237eefc8d5ae.js → 5518-3b96a248632a79c0.js} +1 -1
  3. package/out/about.html +1 -1
  4. package/out/about.txt +1 -1
  5. package/out/app/onboarding.html +1 -1
  6. package/out/app/onboarding.txt +1 -1
  7. package/out/app.html +1 -1
  8. package/out/app.txt +2 -2
  9. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +1 -1
  10. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  11. package/out/blog/let-them-cook-multi-agent-orchestration.html +1 -1
  12. package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
  13. package/out/blog.html +1 -1
  14. package/out/blog.txt +1 -1
  15. package/out/careers.html +1 -1
  16. package/out/careers.txt +1 -1
  17. package/out/changelog.html +1 -1
  18. package/out/changelog.txt +1 -1
  19. package/out/cloud/link.html +1 -1
  20. package/out/cloud/link.txt +1 -1
  21. package/out/complete-profile.html +1 -1
  22. package/out/complete-profile.txt +1 -1
  23. package/out/connect-repos.html +1 -1
  24. package/out/connect-repos.txt +1 -1
  25. package/out/contact.html +1 -1
  26. package/out/contact.txt +1 -1
  27. package/out/dev/cli-tools.html +1 -1
  28. package/out/dev/cli-tools.txt +1 -1
  29. package/out/dev/log-viewer.html +1 -1
  30. package/out/dev/log-viewer.txt +1 -1
  31. package/out/docs.html +1 -1
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +1 -1
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +1 -1
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +1 -1
  41. package/out/pricing.html +1 -1
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +1 -1
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +1 -1
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +1 -1
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +1 -1
  58. package/out/terms.txt +1 -1
  59. package/package.json +1 -1
  60. package/src/components/hooks/useOrchestrator.test.ts +3 -3
  61. package/src/providers/RelayConfigProvider.integration.test.tsx +549 -0
  62. package/src/providers/RelayConfigProvider.test.tsx +299 -0
  63. package/src/providers/RelayConfigProvider.tsx +198 -16
  64. /package/out/_next/static/{5cqIVzlh9DbJT28EbNrcC → 9CykW6n4dJn75_XoFVN_X}/_buildManifest.js +0 -0
  65. /package/out/_next/static/{5cqIVzlh9DbJT28EbNrcC → 9CykW6n4dJn75_XoFVN_X}/_ssgManifest.js +0 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import React from 'react';
6
+ import { act, cleanup, render } from '@testing-library/react';
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
+
9
+ type MockRelayProviderProps = {
10
+ apiKey: string;
11
+ agentToken: string;
12
+ wsToken?: string;
13
+ baseUrl?: string;
14
+ channels?: string[];
15
+ children?: React.ReactNode;
16
+ };
17
+
18
+ const relayProviderCalls: MockRelayProviderProps[] = [];
19
+
20
+ vi.mock('@relaycast/react', () => ({
21
+ RelayProvider: ({ children, ...props }: MockRelayProviderProps) => {
22
+ relayProviderCalls.push(props);
23
+ return <>{children}</>;
24
+ },
25
+ }));
26
+
27
+ import { RelayConfigProvider } from './RelayConfigProvider';
28
+
29
+ interface RelayConfigPayload {
30
+ success: boolean;
31
+ baseUrl: string;
32
+ apiKey: string;
33
+ agentToken: string;
34
+ agentName: string;
35
+ channels: string[];
36
+ wsToken?: string;
37
+ }
38
+
39
+ interface QueuedFetchResponse {
40
+ url: string;
41
+ status?: number;
42
+ payload?: unknown;
43
+ }
44
+
45
+ function makeConfig(agentToken: string, overrides: Partial<RelayConfigPayload> = {}): RelayConfigPayload {
46
+ return {
47
+ success: true,
48
+ baseUrl: 'https://api.relaycast.dev',
49
+ apiKey: 'rk_test',
50
+ agentToken,
51
+ agentName: 'relay-dashboard',
52
+ channels: ['general'],
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ async function flushPromises(): Promise<void> {
58
+ await act(async () => {
59
+ await Promise.resolve();
60
+ await Promise.resolve();
61
+ });
62
+ }
63
+
64
+ class MockBroadcastChannel {
65
+ static channels = new Map<string, Set<MockBroadcastChannel>>();
66
+
67
+ name: string;
68
+ onmessage: ((event: MessageEvent) => void) | null = null;
69
+
70
+ constructor(name: string) {
71
+ this.name = name;
72
+ const peers = MockBroadcastChannel.channels.get(name) ?? new Set<MockBroadcastChannel>();
73
+ peers.add(this);
74
+ MockBroadcastChannel.channels.set(name, peers);
75
+ }
76
+
77
+ postMessage(data: unknown): void {
78
+ const peers = MockBroadcastChannel.channels.get(this.name);
79
+ if (!peers) return;
80
+
81
+ for (const peer of peers) {
82
+ if (peer === this) continue;
83
+ peer.onmessage?.({ data } as MessageEvent);
84
+ }
85
+ }
86
+
87
+ close(): void {
88
+ const peers = MockBroadcastChannel.channels.get(this.name);
89
+ peers?.delete(this);
90
+ if (peers && peers.size === 0) {
91
+ MockBroadcastChannel.channels.delete(this.name);
92
+ }
93
+ }
94
+ }
95
+
96
+ describe('RelayConfigProvider', () => {
97
+ beforeEach(() => {
98
+ relayProviderCalls.length = 0;
99
+ MockBroadcastChannel.channels.clear();
100
+ });
101
+
102
+ afterEach(() => {
103
+ cleanup();
104
+ vi.unstubAllGlobals();
105
+ });
106
+
107
+ it('passes the stable wsToken to RelayProvider', async () => {
108
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => ({
109
+ status: 200,
110
+ ok: true,
111
+ json: async () => {
112
+ expect(String(input)).toBe('/api/relay-config');
113
+ return makeConfig('agt_old', { wsToken: 'rk_ws' });
114
+ },
115
+ }));
116
+
117
+ vi.stubGlobal('fetch', fetchMock);
118
+
119
+ render(
120
+ <RelayConfigProvider>
121
+ <div>dashboard</div>
122
+ </RelayConfigProvider>,
123
+ );
124
+
125
+ await flushPromises();
126
+
127
+ expect(relayProviderCalls.at(-1)).toMatchObject({
128
+ apiKey: 'rk_test',
129
+ agentToken: 'agt_old',
130
+ wsToken: 'rk_ws',
131
+ });
132
+ });
133
+
134
+ it('refreshes a stale agent token on Relaycast 401 and retries once with the new token', async () => {
135
+ const queuedResponses: QueuedFetchResponse[] = [
136
+ {
137
+ url: '/api/relay-config',
138
+ payload: makeConfig('agt_old', { wsToken: 'rk_test' }),
139
+ },
140
+ {
141
+ url: 'https://api.relaycast.dev/v1/channels',
142
+ status: 401,
143
+ payload: { ok: false, error: { code: 'unauthorized', message: 'stale token' } },
144
+ },
145
+ {
146
+ url: '/api/relay-config?refresh=true',
147
+ payload: makeConfig('agt_new', { wsToken: 'rk_test' }),
148
+ },
149
+ {
150
+ url: 'https://api.relaycast.dev/v1/channels',
151
+ payload: { ok: true, data: [] },
152
+ },
153
+ ];
154
+
155
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
156
+ const next = queuedResponses.shift();
157
+ if (!next) {
158
+ throw new Error(`Unexpected fetch: ${String(input)}`);
159
+ }
160
+
161
+ expect(String(input)).toBe(next.url);
162
+ if (next.url === 'https://api.relaycast.dev/v1/channels') {
163
+ const headers = new Headers(init?.headers);
164
+ const authHeader = headers.get('Authorization');
165
+ if (next.status === 401) {
166
+ expect(authHeader).toBe('Bearer agt_old');
167
+ } else {
168
+ expect(authHeader).toBe('Bearer agt_new');
169
+ }
170
+ }
171
+
172
+ const status = next.status ?? 200;
173
+ return {
174
+ status,
175
+ ok: status >= 200 && status < 300,
176
+ json: async () => next.payload,
177
+ };
178
+ });
179
+
180
+ vi.stubGlobal('fetch', fetchMock);
181
+
182
+ render(
183
+ <RelayConfigProvider>
184
+ <div>dashboard</div>
185
+ </RelayConfigProvider>,
186
+ );
187
+
188
+ await flushPromises();
189
+
190
+ let response: Response;
191
+ await act(async () => {
192
+ response = await globalThis.fetch('https://api.relaycast.dev/v1/channels', {
193
+ headers: {
194
+ Authorization: 'Bearer agt_old',
195
+ },
196
+ });
197
+ await Promise.resolve();
198
+ });
199
+ await flushPromises();
200
+
201
+ expect(response!.status).toBe(200);
202
+ expect(relayProviderCalls.at(-1)).toMatchObject({
203
+ agentToken: 'agt_new',
204
+ wsToken: 'rk_test',
205
+ });
206
+ expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([
207
+ '/api/relay-config',
208
+ 'https://api.relaycast.dev/v1/channels',
209
+ '/api/relay-config?refresh=true',
210
+ 'https://api.relaycast.dev/v1/channels',
211
+ ]);
212
+ });
213
+
214
+ it('retries with the current broadcasted token before forcing another refresh', async () => {
215
+ const queuedResponses: QueuedFetchResponse[] = [
216
+ {
217
+ url: '/api/relay-config',
218
+ payload: makeConfig('agt_old', { wsToken: 'rk_test' }),
219
+ },
220
+ {
221
+ url: 'https://api.relaycast.dev/v1/channels',
222
+ status: 401,
223
+ payload: { ok: false, error: { code: 'unauthorized', message: 'stale token' } },
224
+ },
225
+ {
226
+ url: 'https://api.relaycast.dev/v1/channels',
227
+ payload: { ok: true, data: [] },
228
+ },
229
+ ];
230
+
231
+ const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
232
+ const next = queuedResponses.shift();
233
+ if (!next) {
234
+ throw new Error(`Unexpected fetch: ${String(input)}`);
235
+ }
236
+
237
+ expect(String(input)).toBe(next.url);
238
+ if (next.url === 'https://api.relaycast.dev/v1/channels') {
239
+ const headers = new Headers(init?.headers);
240
+ const authHeader = headers.get('Authorization');
241
+ if (next.status === 401) {
242
+ expect(authHeader).toBe('Bearer agt_old');
243
+ } else {
244
+ expect(authHeader).toBe('Bearer agt_new');
245
+ }
246
+ }
247
+
248
+ const status = next.status ?? 200;
249
+ return {
250
+ status,
251
+ ok: status >= 200 && status < 300,
252
+ json: async () => next.payload,
253
+ };
254
+ });
255
+
256
+ vi.stubGlobal('fetch', fetchMock);
257
+ vi.stubGlobal('BroadcastChannel', MockBroadcastChannel);
258
+
259
+ render(
260
+ <RelayConfigProvider>
261
+ <div>dashboard</div>
262
+ </RelayConfigProvider>,
263
+ );
264
+
265
+ await flushPromises();
266
+
267
+ const peer = new MockBroadcastChannel('relay-dashboard:relay-config');
268
+ await act(async () => {
269
+ peer.postMessage({
270
+ type: 'relay-config-refreshed',
271
+ config: makeConfig('agt_new', { wsToken: 'rk_test' }),
272
+ });
273
+ await Promise.resolve();
274
+ });
275
+
276
+ let response: Response;
277
+ await act(async () => {
278
+ response = await globalThis.fetch('https://api.relaycast.dev/v1/channels', {
279
+ headers: {
280
+ Authorization: 'Bearer agt_old',
281
+ },
282
+ });
283
+ await Promise.resolve();
284
+ });
285
+ await flushPromises();
286
+
287
+ expect(response!.status).toBe(200);
288
+ expect(relayProviderCalls.at(-1)).toMatchObject({
289
+ agentToken: 'agt_new',
290
+ wsToken: 'rk_test',
291
+ });
292
+ expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([
293
+ '/api/relay-config',
294
+ 'https://api.relaycast.dev/v1/channels',
295
+ 'https://api.relaycast.dev/v1/channels',
296
+ ]);
297
+ peer.close();
298
+ });
299
+ });
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
3
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { RelayProvider } from '@relaycast/react';
5
5
 
6
6
  interface RelayConfigResponse {
@@ -10,6 +10,8 @@ interface RelayConfigResponse {
10
10
  agentToken?: string;
11
11
  agentName?: string | null;
12
12
  channels?: string[];
13
+ /** Workspace API key for WebSocket auth (stable across agent token rotation). */
14
+ wsToken?: string;
13
15
  }
14
16
 
15
17
  export interface RelayConfigProviderProps {
@@ -34,37 +36,214 @@ export function useRelayConfigStatus(): RelayConfigStatus {
34
36
 
35
37
  /** Default channels the dashboard agent should subscribe to via WebSocket */
36
38
  const DEFAULT_CHANNELS = ['general'];
39
+ const RELAY_CONFIG_CHANNEL = 'relay-dashboard:relay-config';
40
+
41
+ interface RelayConfigBroadcastMessage {
42
+ type: 'relay-config-refreshed';
43
+ config: RelayConfigResponse;
44
+ }
45
+
46
+ async function fetchRelayConfig(refresh = false): Promise<RelayConfigResponse | null> {
47
+ const url = refresh ? '/api/relay-config?refresh=true' : '/api/relay-config';
48
+ const response = await fetch(url, { credentials: 'include' });
49
+ if (!response.ok) return null;
50
+ const payload = await response.json() as RelayConfigResponse;
51
+ if (!hasUsableRelayConfig(payload)) return null;
52
+ return payload;
53
+ }
54
+
55
+ function hasUsableRelayConfig(payload: RelayConfigResponse | null | undefined): payload is RelayConfigResponse {
56
+ return Boolean(payload?.success && payload.baseUrl && payload.apiKey && payload.agentToken);
57
+ }
58
+
59
+ function sameRelayConfig(current: RelayConfigResponse | null, next: RelayConfigResponse): boolean {
60
+ if (!current) return false;
61
+
62
+ const currentChannels = current.channels ?? [];
63
+ const nextChannels = next.channels ?? [];
64
+ if (currentChannels.length !== nextChannels.length) return false;
65
+ if (currentChannels.some((channel, index) => channel !== nextChannels[index])) return false;
66
+
67
+ return current.baseUrl === next.baseUrl
68
+ && current.apiKey === next.apiKey
69
+ && current.agentToken === next.agentToken
70
+ && current.wsToken === next.wsToken
71
+ && current.agentName === next.agentName;
72
+ }
73
+
74
+ function isRelaycastUrl(url: string, baseUrl: string): boolean {
75
+ return url.startsWith(`${baseUrl.replace(/\/+$/, '')}/`);
76
+ }
77
+
78
+ function extractRequestUrl(input: RequestInfo | URL): string {
79
+ if (typeof input === 'string') return input;
80
+ if (input instanceof URL) return input.toString();
81
+ return input.url;
82
+ }
83
+
84
+ function buildHeaders(input: RequestInfo | URL, init?: RequestInit): Headers {
85
+ const headers = new Headers(input instanceof Request ? input.headers : undefined);
86
+ if (init?.headers) {
87
+ new Headers(init.headers).forEach((value, key) => {
88
+ headers.set(key, value);
89
+ });
90
+ }
91
+ return headers;
92
+ }
93
+
94
+ function getBearerToken(headers: Headers): string | null {
95
+ const authorization = headers.get('Authorization');
96
+ if (!authorization?.startsWith('Bearer ')) return null;
97
+ return authorization.slice(7);
98
+ }
99
+
100
+ function withBearerToken(init: RequestInit | undefined, token: string): RequestInit {
101
+ const headers = new Headers(init?.headers);
102
+ headers.set('Authorization', `Bearer ${token}`);
103
+ return {
104
+ ...init,
105
+ headers,
106
+ };
107
+ }
37
108
 
38
109
  export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
39
110
  const [config, setConfig] = useState<RelayConfigResponse | null>(null);
40
111
  const [loaded, setLoaded] = useState(false);
112
+ const broadcastChannelRef = useRef<BroadcastChannel | null>(null);
113
+ const configRef = useRef<RelayConfigResponse | null>(null);
114
+ const refreshPromiseRef = useRef<Promise<RelayConfigResponse | null> | null>(null);
115
+
116
+ const applyRelayConfig = useCallback((nextConfig: RelayConfigResponse, options?: { broadcast?: boolean }) => {
117
+ if (!hasUsableRelayConfig(nextConfig)) return;
118
+
119
+ configRef.current = nextConfig;
120
+ setConfig((current) => (sameRelayConfig(current, nextConfig) ? current : nextConfig));
121
+ setLoaded(true);
122
+
123
+ if (options?.broadcast) {
124
+ try {
125
+ const message: RelayConfigBroadcastMessage = {
126
+ type: 'relay-config-refreshed',
127
+ config: nextConfig,
128
+ };
129
+ broadcastChannelRef.current?.postMessage(message);
130
+ } catch {
131
+ // BroadcastChannel is best-effort only.
132
+ }
133
+ }
134
+ }, []);
135
+
136
+ const refreshAgentToken = useCallback(async (): Promise<RelayConfigResponse | null> => {
137
+ if (refreshPromiseRef.current) return refreshPromiseRef.current;
138
+
139
+ const promise = fetchRelayConfig(true)
140
+ .then((payload) => {
141
+ if (payload) {
142
+ applyRelayConfig(payload, { broadcast: true });
143
+ }
144
+ return payload;
145
+ })
146
+ .catch(() => null)
147
+ .finally(() => {
148
+ if (refreshPromiseRef.current === promise) {
149
+ refreshPromiseRef.current = null;
150
+ }
151
+ });
152
+
153
+ refreshPromiseRef.current = promise;
154
+ return promise;
155
+ }, [applyRelayConfig]);
156
+
157
+ useEffect(() => {
158
+ if (typeof BroadcastChannel !== 'function') return;
159
+
160
+ const channel = new BroadcastChannel(RELAY_CONFIG_CHANNEL);
161
+ broadcastChannelRef.current = channel;
162
+ channel.onmessage = (event: MessageEvent<RelayConfigBroadcastMessage>) => {
163
+ if (event.data?.type !== 'relay-config-refreshed') return;
164
+ applyRelayConfig(event.data.config);
165
+ };
166
+
167
+ return () => {
168
+ if (broadcastChannelRef.current === channel) {
169
+ broadcastChannelRef.current = null;
170
+ }
171
+ channel.close();
172
+ };
173
+ }, [applyRelayConfig]);
41
174
 
42
175
  useEffect(() => {
43
176
  let cancelled = false;
44
177
 
45
- void fetch('/api/relay-config', { credentials: 'include' })
46
- .then(async (response) => {
47
- if (!response.ok) return null;
48
- return response.json() as Promise<RelayConfigResponse>;
49
- })
178
+ void fetchRelayConfig()
50
179
  .then((payload) => {
51
- if (cancelled || !payload?.success) return;
52
- if (!payload.baseUrl || !payload.apiKey || !payload.agentToken) return;
53
- setConfig(payload);
54
- })
55
- .catch(() => {
56
- // No relay-config is a valid local fallback.
180
+ if (cancelled) return;
181
+ if (payload) applyRelayConfig(payload);
57
182
  })
183
+ .catch(() => {})
58
184
  .finally(() => {
59
- if (!cancelled) {
60
- setLoaded(true);
61
- }
185
+ if (!cancelled) setLoaded(true);
62
186
  });
63
187
 
64
188
  return () => {
65
189
  cancelled = true;
66
190
  };
67
- }, []);
191
+ }, [applyRelayConfig]);
192
+
193
+ useEffect(() => {
194
+ configRef.current = config;
195
+ }, [config]);
196
+
197
+ useEffect(() => {
198
+ if (typeof globalThis.fetch !== 'function') return;
199
+
200
+ const originalFetch = globalThis.fetch.bind(globalThis);
201
+ const interceptedFetch: typeof globalThis.fetch = async (input, init) => {
202
+ const response = await originalFetch(input, init);
203
+ if (response.status !== 401 && response.status !== 403) {
204
+ return response;
205
+ }
206
+
207
+ const currentConfig = configRef.current;
208
+ if (!hasUsableRelayConfig(currentConfig)) {
209
+ return response;
210
+ }
211
+
212
+ const url = extractRequestUrl(input);
213
+ if (!isRelaycastUrl(url, currentConfig.baseUrl!)) {
214
+ return response;
215
+ }
216
+
217
+ const headers = buildHeaders(input, init);
218
+ const requestToken = getBearerToken(headers);
219
+ if (!requestToken || requestToken === currentConfig.apiKey) {
220
+ return response;
221
+ }
222
+
223
+ const retryWithToken = async (token: string): Promise<Response> => {
224
+ return originalFetch(url, withBearerToken(init, token));
225
+ };
226
+
227
+ if (requestToken !== currentConfig.agentToken) {
228
+ return retryWithToken(currentConfig.agentToken!);
229
+ }
230
+
231
+ const refreshed = await refreshAgentToken();
232
+ if (!hasUsableRelayConfig(refreshed) || refreshed.agentToken === requestToken) {
233
+ return response;
234
+ }
235
+
236
+ return retryWithToken(refreshed.agentToken!);
237
+ };
238
+
239
+ globalThis.fetch = interceptedFetch;
240
+
241
+ return () => {
242
+ if (globalThis.fetch === interceptedFetch) {
243
+ globalThis.fetch = originalFetch;
244
+ }
245
+ };
246
+ }, [refreshAgentToken]);
68
247
 
69
248
  const configured = Boolean(config?.baseUrl && config.apiKey && config.agentToken);
70
249
  const providerConfig = useMemo(() => {
@@ -73,6 +252,7 @@ export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
73
252
  baseUrl: config!.baseUrl!,
74
253
  apiKey: config!.apiKey!,
75
254
  agentToken: config!.agentToken!,
255
+ wsToken: config!.wsToken ?? config!.apiKey!,
76
256
  };
77
257
  }
78
258
 
@@ -80,6 +260,7 @@ export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
80
260
  baseUrl: typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1',
81
261
  apiKey: '__relay_disabled__',
82
262
  agentToken: '__relay_disabled__',
263
+ wsToken: '__relay_disabled__',
83
264
  };
84
265
  }, [configured, config]);
85
266
 
@@ -99,6 +280,7 @@ export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
99
280
  baseUrl={providerConfig.baseUrl}
100
281
  apiKey={providerConfig.apiKey}
101
282
  agentToken={providerConfig.agentToken}
283
+ wsToken={providerConfig.wsToken}
102
284
  channels={channels}
103
285
  >
104
286
  {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}