-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathrelay-config.ts
More file actions
139 lines (124 loc) · 5.28 KB
/
Copy pathrelay-config.ts
File metadata and controls
139 lines (124 loc) · 5.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import crypto from 'node:crypto';
import type { Express, Request, Response } from 'express';
import type { RouteContext } from '../lib/types.js';
import { getDashboardAgentToken, getWriterClient } from '../relaycast-provider-helpers.js';
import { safeUsername } from '../lib/utils.js';
export function registerRelayConfigRoutes(app: Express, ctx: RouteContext): void {
// Allow the workflow runner (or any local caller) to push a Relaycast API key
// into the dashboard without writing any files.
// In cloud deployments (WORKSPACE_TOKEN set), require a valid token.
app.post('/api/relay-config', (req: Request, res: Response) => {
const expectedToken = process.env.WORKSPACE_TOKEN;
if (expectedToken && ctx.mode !== 'standalone') {
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.substring(7)
: null;
if (!token) {
res.status(401).json({ error: 'Unauthorized - missing workspace token' });
return;
}
const tokenBuffer = Buffer.from(token);
const expectedBuffer = Buffer.from(expectedToken);
const isValidToken =
tokenBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(tokenBuffer, expectedBuffer);
if (!isValidToken) {
res.status(401).json({ error: 'Unauthorized - invalid workspace token' });
return;
}
}
const apiKey = typeof req.body?.apiKey === 'string' ? req.body.apiKey.trim() : '';
if (!apiKey) {
res.status(400).json({ ok: false, error: 'Missing "apiKey" field' });
return;
}
ctx.setRelayApiKey(apiKey);
res.json({ ok: true });
});
app.get('/api/relay-config', async (req: Request, res: Response) => {
// In cloud deployments (WORKSPACE_TOKEN set), require a valid token.
// Skip auth check in standalone mode (local development).
const expectedToken = process.env.WORKSPACE_TOKEN;
if (expectedToken && ctx.mode !== 'standalone') {
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.substring(7)
: null;
if (!token) {
res.status(401).json({ error: 'Unauthorized - missing workspace token' });
return;
}
const tokenBuffer = Buffer.from(token);
const expectedBuffer = Buffer.from(expectedToken);
const isValidToken =
tokenBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(tokenBuffer, expectedBuffer);
if (!isValidToken) {
res.status(401).json({ error: 'Unauthorized - invalid workspace token' });
return;
}
}
// When refresh=true, clear cached agent token so we re-register and get
// a fresh token. This handles cases where the token was rotated externally
// (e.g. by another process calling registerOrRotate for the same agent).
const forceRefresh = req.query.refresh === 'true';
if (forceRefresh) {
ctx.clearCachedAgentToken();
}
const config = ctx.resolveRelaycastConfig();
if (!config) {
res.status(503).json({
success: false,
error: 'Relaycast credentials not configured. Set RELAY_API_KEY or POST /api/relay-config.',
});
return;
}
// refresh=true should force a fresh registerOrRotate even when an older
// token was cached earlier in this process. The new token remains cached
// in memory so future requests reuse it without any file persistence.
let agentToken = forceRefresh ? undefined : config.agentToken;
let agentName = config.agentName ?? safeUsername();
if (!agentToken) {
try {
const registered = await getDashboardAgentToken({ ...config, agentToken: undefined }, agentName);
agentToken = registered.token;
agentName = registered.name;
// Persist the token so subsequent calls reuse it instead of rotating
// (which would invalidate the frontend's WebSocket connection).
ctx.setRelayAgentIdentity(agentToken, agentName);
} catch (err) {
res.status(503).json({
success: false,
error: `Failed to auto-register dashboard agent: ${err instanceof Error ? err.message : String(err)}`,
});
return;
}
}
// Best-effort: ensure the dashboard agent has joined default channels so the
// client-side @relaycast/react hooks (which auth with the agent token) can
// read channel messages. New agents auto-join #general on registration, but
// token rotation via registerOrRotate does not re-join.
// Fire-and-forget to avoid adding latency to the config response.
const defaultChannels = ['general'];
const configWithToken: typeof config = { ...config, agentToken, agentName };
getWriterClient(configWithToken, agentName, ctx.dataDir)
.then(async (writer) => {
for (const channel of defaultChannels) {
await writer.channels.join(channel).catch(() => {});
}
})
.catch(() => {});
res.json({
success: true,
baseUrl: config.baseUrl,
apiKey: config.apiKey,
agentToken,
agentName,
channels: defaultChannels,
// WebSocket auth uses the stable workspace key so agent token rotation
// does not disconnect the dashboard's realtime connection.
wsToken: config.apiKey,
});
});
}