Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions packages/dashboard-server/src/lib/send-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,19 @@ export interface SendStrategy {
* Used when running in proxy mode with a broker URL configured.
*/
export class BrokerSendStrategy implements SendStrategy {
constructor(private brokerUrl: string) {}
constructor(private brokerUrl: string, private brokerApiKey?: string) {}

async send(request: SendRequest): Promise<SendOutcome> {
try {
const headers: Record<string, string> = {
'content-type': 'application/json',
};
if (this.brokerApiKey) {
headers['x-api-key'] = this.brokerApiKey;
}
const upstream = await fetch(`${this.brokerUrl}/api/send`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
headers,
body: JSON.stringify({
to: request.to,
message: request.message,
Expand Down Expand Up @@ -159,6 +163,7 @@ export class DirectSendStrategy implements SendStrategy {
export interface CreateSendStrategyOptions {
brokerProxyEnabled: boolean;
brokerUrl?: string;
brokerApiKey?: string;
relaycastConfig?: RelaycastConfig | null;
dataDir: string;
}
Expand All @@ -171,7 +176,7 @@ export interface CreateSendStrategyOptions {
*/
export function createSendStrategy(opts: CreateSendStrategyOptions): SendStrategy | null {
if (opts.brokerProxyEnabled && opts.brokerUrl) {
return new BrokerSendStrategy(opts.brokerUrl);
return new BrokerSendStrategy(opts.brokerUrl, opts.brokerApiKey);
}

if (opts.relaycastConfig) {
Expand Down
9 changes: 7 additions & 2 deletions packages/dashboard-server/src/lib/spawned-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,12 @@ export function createSpawnedAgentsCaches(opts: {
relayUrl: string | undefined;
dataDir: string;
verbose: boolean;
brokerApiKey?: string;
}): {
getSpawnedAgents: () => Promise<{ names: Set<string> | null; agents: SpawnedAgentSummary[] | null }>;
getLocalAgentNames: () => Set<string> | null;
} {
const { brokerProxyEnabled, relayUrl, dataDir, verbose } = opts;
const { brokerProxyEnabled, relayUrl, dataDir, verbose, brokerApiKey } = opts;

let spawnedAgentsCache: { expiresAt: number; names: Set<string> | null; agents: SpawnedAgentSummary[] | null } = {
expiresAt: 0,
Expand All @@ -305,9 +306,13 @@ export function createSpawnedAgentsCaches(opts: {
}

try {
const headers: Record<string, string> = { Accept: 'application/json' };
if (brokerApiKey) {
headers['x-api-key'] = brokerApiKey;
}
const response = await fetch(`${relayUrl}/api/spawned`, {
method: 'GET',
headers: { Accept: 'application/json' },
headers,
});

if (!response.ok) {
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard-server/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export interface RouteContext {
verbose: boolean;
relayUrl: string | undefined;
brokerProxyEnabled: boolean;
brokerApiKey: string | undefined;
resolveRelaycastConfig: () => RelaycastConfig | null;
setRelayApiKey: (apiKey: string) => void;
setRelayAgentIdentity: (token: string, name: string) => void;
Expand Down
4 changes: 4 additions & 0 deletions packages/dashboard-server/src/proxy-server.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Broker API key not passed to the hybrid WebSocket connection to the broker

The PR adds brokerApiKey authentication to all HTTP requests to the broker (send, spawned, proxy routes), but the handleHybridWebSocket function in packages/dashboard-server/src/websocket/standalone.ts:144 creates a new WebSocket(brokerWsUrl) connection to the broker without including the x-api-key header. When RELAY_BROKER_API_KEY is configured, the broker presumably requires it on all endpoints. The WebSocket connection will fail to authenticate, triggering the fallback to snapshot polling and losing real-time broker event streaming.

Affected call site and WebSocket creation

The call at packages/dashboard-server/src/proxy-server.ts:521 passes only (ws, getRelaycastSnapshot, relayUrl, verbose)brokerApiKey is never provided. Inside handleHybridWebSocket at packages/dashboard-server/src/websocket/standalone.ts:144, the WebSocket is created with no headers:

const bws = new WebSocket(brokerWsUrl);

The Node.js ws library supports passing headers via new WebSocket(url, { headers: { 'x-api-key': key } }), so this should be straightforward to fix.

(Refers to line 521)

Prompt for agents
The handleHybridWebSocket function in packages/dashboard-server/src/websocket/standalone.ts needs to accept an optional brokerApiKey parameter and pass it as an x-api-key header when creating the broker WebSocket connection at line 144. The call site in packages/dashboard-server/src/proxy-server.ts:521 needs to pass the brokerApiKey value.

1. Update handleHybridWebSocket signature to add brokerApiKey?: string parameter
2. At line 144 in standalone.ts, change: const bws = new WebSocket(brokerWsUrl); to: const bws = new WebSocket(brokerWsUrl, brokerApiKey ? { headers: { 'x-api-key': brokerApiKey } } : undefined);
3. Update the call at proxy-server.ts:521 to pass brokerApiKey: handleHybridWebSocket(ws, getRelaycastSnapshot, relayUrl, verbose, brokerApiKey)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,13 @@ export function createServer(options: DashboardServerOptions = {}): DashboardSer
inMemoryAgentName = undefined;
clearRegistrationCache();
};
const brokerApiKey = process.env.RELAY_BROKER_API_KEY?.trim() || undefined;
const { getSpawnedAgents, getLocalAgentNames } = createSpawnedAgentsCaches({
brokerProxyEnabled,
relayUrl,
dataDir,
verbose,
brokerApiKey,
});

const getRelaycastSnapshot = async (): Promise<DashboardSnapshot> => {
Expand Down Expand Up @@ -333,6 +335,7 @@ export function createServer(options: DashboardServerOptions = {}): DashboardSer
const strategy: SendStrategy | null = createSendStrategy({
brokerProxyEnabled,
brokerUrl: relayUrl,
brokerApiKey,
relaycastConfig: config,
dataDir,
});
Expand Down Expand Up @@ -394,6 +397,7 @@ export function createServer(options: DashboardServerOptions = {}): DashboardSer
verbose,
relayUrl,
brokerProxyEnabled,
brokerApiKey,
resolveRelaycastConfig,
setRelayApiKey,
setRelayAgentIdentity,
Expand Down
8 changes: 8 additions & 0 deletions packages/dashboard-server/src/routes/broker-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export function registerBrokerProxyRoutes(app: Express, ctx: RouteContext): void
const headers: Record<string, string> = {
'content-type': 'application/json',
};
if (ctx.brokerApiKey) {
headers['x-api-key'] = ctx.brokerApiKey;
}
const workspaceId = req.header('x-workspace-id');
if (workspaceId) {
headers['x-workspace-id'] = workspaceId;
Expand Down Expand Up @@ -74,6 +77,11 @@ export function registerBrokerProxyRoutes(app: Express, ctx: RouteContext): void
ws: false,
logger: ctx.verbose ? console : undefined,
on: {
proxyReq: (proxyReq) => {
if (ctx.brokerApiKey) {
proxyReq.setHeader('x-api-key', ctx.brokerApiKey);
}
},
error: (err, _req, res) => {
console.error('[dashboard] Broker proxy error:', (err as Error).message);
if (res && 'writeHead' in res && typeof res.writeHead === 'function') {
Expand Down
Loading