@yeaft/webchat-agent 1.0.416 → 1.0.417
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/browser-runtime/chromium.js +192 -0
- package/browser-runtime/cli.js +1 -1
- package/browser-runtime/extension/manifest.json +3 -0
- package/browser-runtime/extension/offscreen.js +201 -25
- package/browser-runtime/extension/popup.js +4 -1
- package/browser-runtime/extension/service-worker.js +47 -7
- package/browser-runtime/extension.js +1 -1
- package/browser-runtime/index.js +3 -0
- package/browser-runtime/local-bridge.js +194 -0
- package/browser-runtime/messages.js +73 -0
- package/browser-runtime/service.js +549 -26
- package/connection/index.js +2 -0
- package/connection/message-router.js +2 -0
- package/index.js +2 -0
- package/local-runtime/server/.env.example +10 -0
- package/local-runtime/server/browser-runtime-routes.js +265 -0
- package/local-runtime/server/client-protocol.js +4 -0
- package/local-runtime/server/config.js +28 -0
- package/local-runtime/server/handlers/agent-browser.js +308 -0
- package/local-runtime/server/handlers/client-browser.js +290 -0
- package/local-runtime/server/ws-agent.js +8 -1
- package/local-runtime/server/ws-client.js +33 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +152 -95
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from 'node:crypto';
|
|
2
|
+
import { CONFIG } from './config.js';
|
|
3
|
+
import { agents, webClients } from './context.js';
|
|
4
|
+
|
|
5
|
+
const MAX_ROUTES = 2048;
|
|
6
|
+
const MAX_PEERS = 4096;
|
|
7
|
+
const MAX_CREATE_REQUESTS = 4096;
|
|
8
|
+
const CREATE_REQUEST_TTL_MS = 10 * 60_000;
|
|
9
|
+
|
|
10
|
+
export const browserRoutes = new Map();
|
|
11
|
+
export const browserPeers = new Map();
|
|
12
|
+
const browserRequests = new Map();
|
|
13
|
+
|
|
14
|
+
function key(agentId, browserSessionId) {
|
|
15
|
+
return `${String(agentId || '')}\0${String(browserSessionId || '')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createKey(agentId, connectionId, requestId) {
|
|
19
|
+
return `${String(agentId || '')}\0${String(connectionId || '')}\0${String(requestId || '')}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scopeUsername({ ownerUserId, agentId, browserSessionId, peerId, connectionGeneration, endpointRole, expiresAt }) {
|
|
23
|
+
const scope = Buffer.from(JSON.stringify({
|
|
24
|
+
ownerUserId,
|
|
25
|
+
agentId,
|
|
26
|
+
browserSessionId,
|
|
27
|
+
peerId,
|
|
28
|
+
connectionGeneration,
|
|
29
|
+
endpointRole,
|
|
30
|
+
credentialId: randomUUID(),
|
|
31
|
+
})).toString('base64url');
|
|
32
|
+
return `${Math.floor(expiresAt / 1000)}:${scope}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function mintBrowserIceServers(scope, config = CONFIG.browserRuntime) {
|
|
36
|
+
const stunServers = config.stunUrls.length > 0 ? [{ urls: [...config.stunUrls] }] : [];
|
|
37
|
+
if (config.turnUrls.length === 0) return stunServers;
|
|
38
|
+
const expiresAt = Date.now() + config.credentialTtlSeconds * 1000;
|
|
39
|
+
const username = scopeUsername({ ...scope, expiresAt });
|
|
40
|
+
const credential = createHmac('sha1', config.turnSecret).update(username).digest('base64');
|
|
41
|
+
return [
|
|
42
|
+
...stunServers,
|
|
43
|
+
{ urls: [...config.turnUrls], username, credential, expiresAt },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function browserServerIdentity(client) {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
ownerUserId: client.userId,
|
|
50
|
+
clientId: client.id,
|
|
51
|
+
webConnectionId: client.connectionId,
|
|
52
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function pruneBrowserRuntimeRoutes(now = Date.now()) {
|
|
57
|
+
for (const [requestKey, request] of browserRequests) {
|
|
58
|
+
if (request.expiresAt <= now || !webClients.has(request.clientId)) browserRequests.delete(requestKey);
|
|
59
|
+
}
|
|
60
|
+
for (const [peerId, peer] of browserPeers) {
|
|
61
|
+
const client = webClients.get(peer.clientId);
|
|
62
|
+
if ((peer.expiresAt != null && peer.expiresAt <= now)
|
|
63
|
+
|| !client || client.connectionId !== peer.webConnectionId) {
|
|
64
|
+
browserPeers.delete(peerId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function registerBrowserCreateRequest({ agentId, client, requestId, digest }) {
|
|
70
|
+
pruneBrowserRuntimeRoutes();
|
|
71
|
+
const requestKey = createKey(agentId, client.connectionId, requestId);
|
|
72
|
+
const existing = browserRequests.get(requestKey);
|
|
73
|
+
if (existing) {
|
|
74
|
+
if (existing.digest !== digest) return { conflict: true, request: existing };
|
|
75
|
+
return { duplicate: true, request: existing };
|
|
76
|
+
}
|
|
77
|
+
if (browserRequests.size >= MAX_CREATE_REQUESTS) return { capacity: true, request: null };
|
|
78
|
+
const request = {
|
|
79
|
+
agentId,
|
|
80
|
+
requestId,
|
|
81
|
+
serverRequestId: randomUUID(),
|
|
82
|
+
clientId: client.id,
|
|
83
|
+
ownerUserId: client.userId,
|
|
84
|
+
webConnectionId: client.connectionId,
|
|
85
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
86
|
+
digest,
|
|
87
|
+
state: 'pending',
|
|
88
|
+
response: null,
|
|
89
|
+
expiresAt: Date.now() + CREATE_REQUEST_TTL_MS,
|
|
90
|
+
};
|
|
91
|
+
browserRequests.set(requestKey, request);
|
|
92
|
+
return { request };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function completeBrowserRequest(agentId, msg, { consume = false } = {}) {
|
|
96
|
+
for (const [requestKey, request] of browserRequests) {
|
|
97
|
+
if (request.agentId !== agentId || request.serverRequestId !== msg.requestId || request.state !== 'pending') continue;
|
|
98
|
+
request.state = msg.type.endsWith('_error') ? 'failed' : 'completed';
|
|
99
|
+
request.response = { ...msg, requestId: request.requestId };
|
|
100
|
+
request.expiresAt = Date.now() + CREATE_REQUEST_TTL_MS;
|
|
101
|
+
if (consume) browserRequests.delete(requestKey);
|
|
102
|
+
return request;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function registerBrowserRequest({ agentId, client, requestId, kind, browserSessionId = null }) {
|
|
108
|
+
pruneBrowserRuntimeRoutes();
|
|
109
|
+
const requestKey = createKey(agentId, client.connectionId, requestId);
|
|
110
|
+
if (browserRequests.has(requestKey) || browserRequests.size >= MAX_CREATE_REQUESTS) return null;
|
|
111
|
+
const request = {
|
|
112
|
+
agentId,
|
|
113
|
+
requestId,
|
|
114
|
+
serverRequestId: randomUUID(),
|
|
115
|
+
kind,
|
|
116
|
+
browserSessionId,
|
|
117
|
+
clientId: client.id,
|
|
118
|
+
ownerUserId: client.userId,
|
|
119
|
+
webConnectionId: client.connectionId,
|
|
120
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
121
|
+
digest: '',
|
|
122
|
+
state: 'pending',
|
|
123
|
+
response: null,
|
|
124
|
+
expiresAt: Date.now() + CREATE_REQUEST_TTL_MS,
|
|
125
|
+
};
|
|
126
|
+
browserRequests.set(requestKey, request);
|
|
127
|
+
return request;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function installBrowserRoute({ agentId, ownerUserId, msg }) {
|
|
131
|
+
if (!msg.browserSessionId) return null;
|
|
132
|
+
const routeKey = key(agentId, msg.browserSessionId);
|
|
133
|
+
const existing = browserRoutes.get(routeKey);
|
|
134
|
+
if (existing && existing.ownerUserId !== ownerUserId) return null;
|
|
135
|
+
if (!existing && browserRoutes.size >= MAX_ROUTES) return null;
|
|
136
|
+
const route = {
|
|
137
|
+
ownerUserId,
|
|
138
|
+
agentId,
|
|
139
|
+
browserSessionId: msg.browserSessionId,
|
|
140
|
+
revision: Number(msg.revision) || 1,
|
|
141
|
+
state: msg.state || 'ready',
|
|
142
|
+
updatedAt: Date.now(),
|
|
143
|
+
};
|
|
144
|
+
browserRoutes.set(routeKey, route);
|
|
145
|
+
return route;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getBrowserRoute(agentId, browserSessionId) {
|
|
149
|
+
return browserRoutes.get(key(agentId, browserSessionId)) || null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function deleteBrowserRoute(agentId, browserSessionId) {
|
|
153
|
+
const routeKey = key(agentId, browserSessionId);
|
|
154
|
+
const deleted = browserRoutes.delete(routeKey);
|
|
155
|
+
for (const [peerId, peer] of browserPeers) {
|
|
156
|
+
if (peer.agentId === agentId && peer.browserSessionId === browserSessionId) browserPeers.delete(peerId);
|
|
157
|
+
}
|
|
158
|
+
return deleted;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function reserveBrowserPeer({ agentId, browserSessionId, client, requestId, connectionGeneration, role = 'viewer' }) {
|
|
162
|
+
pruneBrowserRuntimeRoutes();
|
|
163
|
+
const route = getBrowserRoute(agentId, browserSessionId);
|
|
164
|
+
if (!route || route.ownerUserId !== client.userId || route.state !== 'ready') return { error: 'browser_session_not_found' };
|
|
165
|
+
if (!Number.isSafeInteger(connectionGeneration) || connectionGeneration <= 0) return { error: 'browser_generation_invalid' };
|
|
166
|
+
const duplicate = [...browserPeers.values()].find(peer => (
|
|
167
|
+
peer.agentId === agentId
|
|
168
|
+
&& peer.browserSessionId === browserSessionId
|
|
169
|
+
&& peer.clientId === client.id
|
|
170
|
+
&& peer.webConnectionId === client.connectionId
|
|
171
|
+
&& peer.requestId === requestId
|
|
172
|
+
&& peer.connectionGeneration === connectionGeneration
|
|
173
|
+
));
|
|
174
|
+
if (duplicate) return { peer: duplicate, duplicate: true };
|
|
175
|
+
if (browserPeers.size >= MAX_PEERS) return { error: 'browser_peer_capacity' };
|
|
176
|
+
const peerId = randomUUID();
|
|
177
|
+
const expiresAt = Date.now() + CONFIG.browserRuntime.routeTtlMs;
|
|
178
|
+
const peer = {
|
|
179
|
+
peerId,
|
|
180
|
+
requestId,
|
|
181
|
+
ownerUserId: client.userId,
|
|
182
|
+
agentId,
|
|
183
|
+
browserSessionId,
|
|
184
|
+
clientId: client.id,
|
|
185
|
+
webConnectionId: client.connectionId,
|
|
186
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
187
|
+
connectionGeneration,
|
|
188
|
+
role: role === 'interactive' ? 'interactive' : 'viewer',
|
|
189
|
+
state: 'preparing',
|
|
190
|
+
pendingOffer: null,
|
|
191
|
+
pendingCandidates: [],
|
|
192
|
+
agentCandidateCount: 0,
|
|
193
|
+
webCandidateCount: 0,
|
|
194
|
+
iceTransportPolicy: CONFIG.browserRuntime.iceTransportPolicy,
|
|
195
|
+
expiresAt,
|
|
196
|
+
};
|
|
197
|
+
browserPeers.set(peerId, peer);
|
|
198
|
+
return { peer };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getBrowserPeer(peerId) {
|
|
202
|
+
pruneBrowserRuntimeRoutes();
|
|
203
|
+
return browserPeers.get(String(peerId || '')) || null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function browserPeerMatchesClient(peer, client, message = {}) {
|
|
207
|
+
return !!peer && !!client
|
|
208
|
+
&& peer.ownerUserId === client.userId
|
|
209
|
+
&& peer.clientId === client.id
|
|
210
|
+
&& peer.webConnectionId === client.connectionId
|
|
211
|
+
&& peer.webConnectionGeneration === client.connectionGeneration
|
|
212
|
+
&& peer.agentId === message.agentId
|
|
213
|
+
&& peer.browserSessionId === message.browserSessionId
|
|
214
|
+
&& peer.connectionGeneration === Number(message.connectionGeneration);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function deleteBrowserPeer(peerId) {
|
|
218
|
+
return browserPeers.delete(String(peerId || ''));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function clearBrowserRuntimeForClient(client) {
|
|
222
|
+
if (!client) return [];
|
|
223
|
+
const peers = [];
|
|
224
|
+
for (const [peerId, peer] of browserPeers) {
|
|
225
|
+
if (peer.clientId === client.id && peer.webConnectionId === client.connectionId) {
|
|
226
|
+
browserPeers.delete(peerId);
|
|
227
|
+
peers.push(peer);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const [requestKey, request] of browserRequests) {
|
|
231
|
+
if (request.clientId === client.id && request.webConnectionId === client.connectionId) {
|
|
232
|
+
browserRequests.delete(requestKey);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return peers;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function clearBrowserRuntimeForAgent(agentId) {
|
|
239
|
+
for (const [routeKey, route] of browserRoutes) {
|
|
240
|
+
if (route.agentId === agentId) browserRoutes.delete(routeKey);
|
|
241
|
+
}
|
|
242
|
+
for (const [peerId, peer] of browserPeers) {
|
|
243
|
+
if (peer.agentId === agentId) browserPeers.delete(peerId);
|
|
244
|
+
}
|
|
245
|
+
for (const [requestKey, request] of browserRequests) {
|
|
246
|
+
if (request.agentId === agentId) browserRequests.delete(requestKey);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function browserClientForPeer(peer) {
|
|
251
|
+
const client = webClients.get(peer?.clientId);
|
|
252
|
+
if (!client || client.connectionId !== peer.webConnectionId
|
|
253
|
+
|| client.connectionGeneration !== peer.webConnectionGeneration) return null;
|
|
254
|
+
return client;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function browserAgentForPeer(peer) {
|
|
258
|
+
return agents.get(peer?.agentId) || null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function __testResetBrowserRuntimeRoutes() {
|
|
262
|
+
browserRoutes.clear();
|
|
263
|
+
browserPeers.clear();
|
|
264
|
+
browserRequests.clear();
|
|
265
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const WORKBENCH_ROUTE_PROTOCOL = 1;
|
|
2
|
+
export const BROWSER_RUNTIME_PROTOCOL = 1;
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Apply the explicit browser protocol hello to one Server-owned client record.
|
|
@@ -10,5 +11,8 @@ export function applyClientHello(client, message) {
|
|
|
10
11
|
if (message.workbenchRouteProtocol === WORKBENCH_ROUTE_PROTOCOL) {
|
|
11
12
|
client.workbenchRouteProtocol = WORKBENCH_ROUTE_PROTOCOL;
|
|
12
13
|
}
|
|
14
|
+
if (message.browserRuntimeProtocol === BROWSER_RUNTIME_PROTOCOL) {
|
|
15
|
+
client.browserRuntimeProtocol = BROWSER_RUNTIME_PROTOCOL;
|
|
16
|
+
}
|
|
13
17
|
return true;
|
|
14
18
|
}
|
|
@@ -80,6 +80,10 @@ function loadUsers() {
|
|
|
80
80
|
const DEFAULT_JWT_SECRET = 'default-secret-change-in-production';
|
|
81
81
|
const DEFAULT_AGENT_SECRET = 'agent-shared-secret';
|
|
82
82
|
|
|
83
|
+
function commaList(value) {
|
|
84
|
+
return String(value || '').split(',').map(item => item.trim()).filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
|
|
83
87
|
export const CONFIG = {
|
|
84
88
|
// Server settings
|
|
85
89
|
port: parseInt(process.env.PORT, 10) || 3456,
|
|
@@ -115,6 +119,19 @@ export const CONFIG = {
|
|
|
115
119
|
// Agent authentication (global fallback — per-user agent_secret is preferred)
|
|
116
120
|
agentSecret: process.env.AGENT_SECRET || DEFAULT_AGENT_SECRET,
|
|
117
121
|
|
|
122
|
+
// Browser Runtime stays fail-closed until the Server rollout gate is enabled.
|
|
123
|
+
// TURN credentials use the standard time-limited HMAC username scheme; Web and
|
|
124
|
+
// Agent endpoints receive separately scoped usernames from the route ledger.
|
|
125
|
+
browserRuntime: {
|
|
126
|
+
enabled: process.env.BROWSER_RUNTIME_ENABLED === 'true',
|
|
127
|
+
iceTransportPolicy: process.env.BROWSER_ICE_TRANSPORT_POLICY === 'relay' ? 'relay' : 'all',
|
|
128
|
+
stunUrls: commaList(process.env.BROWSER_STUN_URLS),
|
|
129
|
+
turnUrls: commaList(process.env.BROWSER_TURN_URLS),
|
|
130
|
+
turnSecret: process.env.BROWSER_TURN_SECRET || '',
|
|
131
|
+
credentialTtlSeconds: Math.min(3600, Math.max(60, parseInt(process.env.BROWSER_TURN_TTL_SECONDS, 10) || 600)),
|
|
132
|
+
routeTtlMs: Math.min(60 * 60_000, Math.max(60_000, parseInt(process.env.BROWSER_ROUTE_TTL_MS, 10) || 15 * 60_000)),
|
|
133
|
+
},
|
|
134
|
+
|
|
118
135
|
// A Sandbox is an ordinary yeaft-agent container managed by this Server's Docker daemon.
|
|
119
136
|
// The Server controls only the container lifecycle; Agent behavior stays on the existing wire.
|
|
120
137
|
sandbox: {
|
|
@@ -273,6 +290,17 @@ export function validateProductionConfig() {
|
|
|
273
290
|
if (CONFIG.sandbox.enabled && !/^wss?:\/\//.test(CONFIG.sandbox.serverUrl)) {
|
|
274
291
|
errors.push('SANDBOX_SERVER_URL must be the ws:// or wss:// URL that container Agents use to connect');
|
|
275
292
|
}
|
|
293
|
+
if (CONFIG.browserRuntime.enabled) {
|
|
294
|
+
const invalidIceUrl = [...CONFIG.browserRuntime.stunUrls, ...CONFIG.browserRuntime.turnUrls]
|
|
295
|
+
.find(url => !/^(?:stun|stuns|turn|turns):/i.test(url));
|
|
296
|
+
if (invalidIceUrl) errors.push(`Invalid Browser Runtime ICE URL: ${invalidIceUrl}`);
|
|
297
|
+
if (CONFIG.browserRuntime.turnUrls.length > 0 && !CONFIG.browserRuntime.turnSecret) {
|
|
298
|
+
errors.push('BROWSER_TURN_SECRET is required when BROWSER_TURN_URLS is configured');
|
|
299
|
+
}
|
|
300
|
+
if (CONFIG.browserRuntime.iceTransportPolicy === 'relay' && CONFIG.browserRuntime.turnUrls.length === 0) {
|
|
301
|
+
errors.push('BROWSER_TURN_URLS is required when BROWSER_ICE_TRANSPORT_POLICY=relay');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
276
304
|
|
|
277
305
|
// Check that at least one user with a password exists (in DB or config)
|
|
278
306
|
// Only warn (don't block startup) — allows first-time setup via create-user.js
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { sendToAgent, sendToWebClient } from '../ws-utils.js';
|
|
2
|
+
import {
|
|
3
|
+
browserClientForPeer,
|
|
4
|
+
browserPeers,
|
|
5
|
+
browserRoutes,
|
|
6
|
+
completeBrowserRequest,
|
|
7
|
+
deleteBrowserPeer,
|
|
8
|
+
deleteBrowserRoute,
|
|
9
|
+
getBrowserPeer,
|
|
10
|
+
getBrowserRoute,
|
|
11
|
+
installBrowserRoute,
|
|
12
|
+
} from '../browser-runtime-routes.js';
|
|
13
|
+
|
|
14
|
+
const AGENT_BROWSER_TYPES = new Set([
|
|
15
|
+
'browser_session_created',
|
|
16
|
+
'browser_session_error',
|
|
17
|
+
'browser_session_snapshot',
|
|
18
|
+
'browser_session_list_result',
|
|
19
|
+
'browser_peer_prepared',
|
|
20
|
+
'browser_peer_offer',
|
|
21
|
+
'browser_peer_ice_candidate',
|
|
22
|
+
'browser_peer_state',
|
|
23
|
+
'browser_peer_error',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function clean(value, max = 512) {
|
|
27
|
+
return typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sessionSnapshot(msg) {
|
|
31
|
+
return {
|
|
32
|
+
browserSessionId: clean(msg?.browserSessionId),
|
|
33
|
+
revision: Number(msg?.revision) || 1,
|
|
34
|
+
state: clean(msg?.state, 32) || 'unknown',
|
|
35
|
+
activeUrl: clean(msg?.activeUrl, 4096) || 'about:blank',
|
|
36
|
+
title: clean(msg?.title, 512),
|
|
37
|
+
pageRevision: Number(msg?.pageRevision) || 1,
|
|
38
|
+
captureMode: clean(msg?.captureMode, 32) || null,
|
|
39
|
+
viewport: msg?.viewport && typeof msg.viewport === 'object' ? {
|
|
40
|
+
width: Number(msg.viewport.width) || 0,
|
|
41
|
+
height: Number(msg.viewport.height) || 0,
|
|
42
|
+
deviceScaleFactor: Number(msg.viewport.deviceScaleFactor) || 1,
|
|
43
|
+
} : null,
|
|
44
|
+
viewerCount: Math.max(0, Number(msg?.viewerCount) || 0),
|
|
45
|
+
interactivePeerCount: Math.max(0, Number(msg?.interactivePeerCount) || 0),
|
|
46
|
+
authorizedProducerCount: Math.max(0, Number(msg?.authorizedProducerCount) || 0),
|
|
47
|
+
expiresAt: Number(msg?.expiresAt) || null,
|
|
48
|
+
terminalReason: clean(msg?.terminalReason, 128) || null,
|
|
49
|
+
safeError: clean(msg?.safeError, 500) || null,
|
|
50
|
+
sourceRef: msg?.sourceRef && typeof msg.sourceRef === 'object' ? {
|
|
51
|
+
kind: clean(msg.sourceRef.kind, 32),
|
|
52
|
+
sessionId: clean(msg.sourceRef.sessionId) || undefined,
|
|
53
|
+
conversationId: clean(msg.sourceRef.conversationId) || undefined,
|
|
54
|
+
workItemId: clean(msg.sourceRef.workItemId) || undefined,
|
|
55
|
+
} : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function publicSessionMessage(agentId, msg) {
|
|
60
|
+
if (msg?.type === 'browser_session_error') {
|
|
61
|
+
return {
|
|
62
|
+
type: 'browser_session_error',
|
|
63
|
+
agentId,
|
|
64
|
+
requestId: clean(msg.requestId) || null,
|
|
65
|
+
browserSessionId: clean(msg.browserSessionId) || null,
|
|
66
|
+
code: clean(msg.code, 128) || 'browser_runtime_error',
|
|
67
|
+
safeError: clean(msg.safeError, 500) || null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (msg?.type === 'browser_session_list_result') {
|
|
71
|
+
return {
|
|
72
|
+
type: msg.type,
|
|
73
|
+
agentId,
|
|
74
|
+
requestId: clean(msg.requestId) || null,
|
|
75
|
+
sessions: (Array.isArray(msg.sessions) ? msg.sessions : []).slice(0, 64).map(sessionSnapshot),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
type: clean(msg?.type, 64),
|
|
80
|
+
agentId,
|
|
81
|
+
requestId: clean(msg?.requestId) || null,
|
|
82
|
+
...sessionSnapshot(msg),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function sendCreateResponse(agentId, msg) {
|
|
87
|
+
const request = completeBrowserRequest(agentId, msg);
|
|
88
|
+
if (!request) return true;
|
|
89
|
+
const client = browserClientForPeer({
|
|
90
|
+
clientId: request.clientId,
|
|
91
|
+
webConnectionId: request.webConnectionId,
|
|
92
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
93
|
+
});
|
|
94
|
+
if (!client || client.userId !== request.ownerUserId) return true;
|
|
95
|
+
if (msg.type === 'browser_session_created') {
|
|
96
|
+
const route = installBrowserRoute({ agentId, ownerUserId: request.ownerUserId, msg });
|
|
97
|
+
if (!route) {
|
|
98
|
+
await sendToWebClient(client, {
|
|
99
|
+
type: 'browser_session_error',
|
|
100
|
+
agentId,
|
|
101
|
+
requestId: request.requestId,
|
|
102
|
+
browserSessionId: clean(msg.browserSessionId) || null,
|
|
103
|
+
code: 'browser_route_capacity',
|
|
104
|
+
safeError: 'browser_route_capacity',
|
|
105
|
+
});
|
|
106
|
+
await sendToAgent(agent, {
|
|
107
|
+
type: 'browser_session_close',
|
|
108
|
+
agentId,
|
|
109
|
+
requestId: `server-cleanup-${request.serverRequestId}`,
|
|
110
|
+
browserSessionId: clean(msg.browserSessionId),
|
|
111
|
+
expectedRevision: Number(msg.revision) || 1,
|
|
112
|
+
serverIdentity: {
|
|
113
|
+
ownerUserId: request.ownerUserId,
|
|
114
|
+
clientId: request.clientId,
|
|
115
|
+
webConnectionId: request.webConnectionId,
|
|
116
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
117
|
+
},
|
|
118
|
+
}).catch(() => {});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const publicResponse = publicSessionMessage(agentId, request.response);
|
|
123
|
+
// Create request replay is served from the ledger. Persist only the explicit
|
|
124
|
+
// public projection, never the Agent-originated object.
|
|
125
|
+
request.response = publicResponse;
|
|
126
|
+
await sendToWebClient(client, publicResponse);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Agent-authenticated Browser events routed only through Server-owned ledgers. */
|
|
131
|
+
export async function handleAgentBrowser(agentId, agent, msg) {
|
|
132
|
+
if (!AGENT_BROWSER_TYPES.has(msg?.type)) return false;
|
|
133
|
+
if (msg.type === 'browser_session_created'
|
|
134
|
+
|| (msg.type === 'browser_session_error' && msg.requestId)) {
|
|
135
|
+
return sendCreateResponse(agentId, msg);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (msg.type === 'browser_session_list_result') {
|
|
139
|
+
const request = completeBrowserRequest(agentId, msg, { consume: true });
|
|
140
|
+
if (!request) return true;
|
|
141
|
+
for (const snapshot of Array.isArray(msg.sessions) ? msg.sessions : []) {
|
|
142
|
+
if (!snapshot?.browserSessionId) continue;
|
|
143
|
+
installBrowserRoute({ agentId, ownerUserId: request.ownerUserId, msg: snapshot });
|
|
144
|
+
}
|
|
145
|
+
const client = browserClientForPeer({
|
|
146
|
+
clientId: request.clientId,
|
|
147
|
+
webConnectionId: request.webConnectionId,
|
|
148
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
149
|
+
});
|
|
150
|
+
if (client?.userId === request.ownerUserId) {
|
|
151
|
+
await sendToWebClient(client, publicSessionMessage(agentId, request.response));
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (msg.type === 'browser_session_snapshot' && msg.requestId) {
|
|
157
|
+
const request = completeBrowserRequest(agentId, msg, { consume: true });
|
|
158
|
+
if (!request) return true;
|
|
159
|
+
const client = browserClientForPeer({
|
|
160
|
+
clientId: request.clientId,
|
|
161
|
+
webConnectionId: request.webConnectionId,
|
|
162
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
163
|
+
});
|
|
164
|
+
if (client?.userId === request.ownerUserId) {
|
|
165
|
+
const route = getBrowserRoute(agentId, msg.browserSessionId);
|
|
166
|
+
if (route) {
|
|
167
|
+
route.revision = Number(msg.revision) || route.revision;
|
|
168
|
+
route.state = clean(msg.state, 32) || route.state;
|
|
169
|
+
if (route.state === 'closed' || route.state === 'failed') {
|
|
170
|
+
deleteBrowserRoute(agentId, route.browserSessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await sendToWebClient(client, publicSessionMessage(agentId, request.response));
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (msg.type === 'browser_session_snapshot') {
|
|
179
|
+
const route = getBrowserRoute(agentId, msg.browserSessionId);
|
|
180
|
+
if (!route) return true;
|
|
181
|
+
route.revision = Number(msg.revision) || route.revision;
|
|
182
|
+
route.state = clean(msg.state, 32) || route.state;
|
|
183
|
+
route.updatedAt = Date.now();
|
|
184
|
+
const recipients = [];
|
|
185
|
+
for (const peer of browserPeers.values()) {
|
|
186
|
+
if (peer.agentId !== agentId || peer.browserSessionId !== route.browserSessionId) continue;
|
|
187
|
+
const client = browserClientForPeer(peer);
|
|
188
|
+
if (client && !recipients.includes(client)) recipients.push(client);
|
|
189
|
+
}
|
|
190
|
+
if (route.state === 'closed' || route.state === 'failed') {
|
|
191
|
+
deleteBrowserRoute(agentId, route.browserSessionId);
|
|
192
|
+
}
|
|
193
|
+
const snapshot = publicSessionMessage(agentId, msg);
|
|
194
|
+
for (const client of recipients) await sendToWebClient(client, snapshot);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const peer = getBrowserPeer(msg.peerId);
|
|
199
|
+
if (!peer || peer.agentId !== agentId
|
|
200
|
+
|| peer.browserSessionId !== msg.browserSessionId
|
|
201
|
+
|| peer.connectionGeneration !== Number(msg.connectionGeneration)) return true;
|
|
202
|
+
const route = getBrowserRoute(agentId, peer.browserSessionId);
|
|
203
|
+
if (!route || route.ownerUserId !== peer.ownerUserId) {
|
|
204
|
+
deleteBrowserPeer(peer.peerId);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
const client = browserClientForPeer(peer);
|
|
208
|
+
if (!client || client.userId !== route.ownerUserId) {
|
|
209
|
+
deleteBrowserPeer(peer.peerId);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (msg.type === 'browser_peer_prepared') {
|
|
214
|
+
peer.state = 'prepared';
|
|
215
|
+
await sendToWebClient(client, {
|
|
216
|
+
type: msg.type,
|
|
217
|
+
agentId,
|
|
218
|
+
browserSessionId: peer.browserSessionId,
|
|
219
|
+
peerId: peer.peerId,
|
|
220
|
+
requestId: peer.requestId,
|
|
221
|
+
connectionGeneration: peer.connectionGeneration,
|
|
222
|
+
iceTransportPolicy: peer.iceTransportPolicy,
|
|
223
|
+
iceServers: peer.webIceServers || [],
|
|
224
|
+
role: peer.role,
|
|
225
|
+
});
|
|
226
|
+
if (peer.pendingOffer) {
|
|
227
|
+
await sendToWebClient(client, peer.pendingOffer);
|
|
228
|
+
peer.pendingOffer = null;
|
|
229
|
+
peer.state = 'offered';
|
|
230
|
+
}
|
|
231
|
+
for (const candidate of peer.pendingCandidates.splice(0)) {
|
|
232
|
+
await sendToWebClient(client, candidate);
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (msg.type === 'browser_peer_offer') {
|
|
237
|
+
const description = msg.description;
|
|
238
|
+
if (description?.type !== 'offer' || typeof description.sdp !== 'string' || description.sdp.length > 96 * 1024) {
|
|
239
|
+
deleteBrowserPeer(peer.peerId);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
const offer = {
|
|
243
|
+
type: msg.type,
|
|
244
|
+
agentId,
|
|
245
|
+
browserSessionId: peer.browserSessionId,
|
|
246
|
+
peerId: peer.peerId,
|
|
247
|
+
requestId: peer.requestId,
|
|
248
|
+
connectionGeneration: peer.connectionGeneration,
|
|
249
|
+
description: { type: 'offer', sdp: description.sdp },
|
|
250
|
+
iceServers: peer.webIceServers || [],
|
|
251
|
+
role: peer.role,
|
|
252
|
+
};
|
|
253
|
+
if (peer.state !== 'prepared' && peer.state !== 'offered') {
|
|
254
|
+
peer.pendingOffer = offer;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
peer.state = 'offered';
|
|
258
|
+
await sendToWebClient(client, offer);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (msg.type === 'browser_peer_ice_candidate') {
|
|
262
|
+
if (peer.agentCandidateCount >= 128) return true;
|
|
263
|
+
const raw = msg.candidate;
|
|
264
|
+
if (raw != null && (typeof raw !== 'object'
|
|
265
|
+
|| typeof raw.candidate !== 'string'
|
|
266
|
+
|| raw.candidate.length > 4096)) return true;
|
|
267
|
+
peer.agentCandidateCount += 1;
|
|
268
|
+
const candidate = {
|
|
269
|
+
type: msg.type,
|
|
270
|
+
agentId,
|
|
271
|
+
browserSessionId: peer.browserSessionId,
|
|
272
|
+
peerId: peer.peerId,
|
|
273
|
+
connectionGeneration: peer.connectionGeneration,
|
|
274
|
+
candidate: raw == null ? null : {
|
|
275
|
+
candidate: raw.candidate,
|
|
276
|
+
sdpMid: clean(raw.sdpMid, 256) || null,
|
|
277
|
+
sdpMLineIndex: Number.isInteger(raw.sdpMLineIndex) ? raw.sdpMLineIndex : null,
|
|
278
|
+
usernameFragment: clean(raw.usernameFragment, 256) || null,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
if (peer.state !== 'prepared' && peer.state !== 'offered') {
|
|
282
|
+
if (peer.pendingCandidates.length < 128) peer.pendingCandidates.push(candidate);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
await sendToWebClient(client, candidate);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (msg.type === 'browser_peer_state') {
|
|
289
|
+
peer.state = clean(msg.state, 32) || peer.state;
|
|
290
|
+
if (peer.state === 'connected') peer.expiresAt = null;
|
|
291
|
+
if (['failed', 'disconnected', 'closed'].includes(peer.state)) deleteBrowserPeer(peer.peerId);
|
|
292
|
+
}
|
|
293
|
+
if (msg.type === 'browser_peer_error') deleteBrowserPeer(peer.peerId);
|
|
294
|
+
await sendToWebClient(client, {
|
|
295
|
+
type: msg.type,
|
|
296
|
+
agentId,
|
|
297
|
+
browserSessionId: peer.browserSessionId,
|
|
298
|
+
peerId: peer.peerId,
|
|
299
|
+
connectionGeneration: peer.connectionGeneration,
|
|
300
|
+
requestId: peer.requestId,
|
|
301
|
+
state: clean(msg.state, 32) || null,
|
|
302
|
+
code: clean(msg.code, 128) || null,
|
|
303
|
+
safeError: clean(msg.safeError, 500) || null,
|
|
304
|
+
});
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export { AGENT_BROWSER_TYPES };
|