camofox-browser 2.1.1 → 2.4.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.
- package/CHANGELOG.md +150 -0
- package/README.md +310 -34
- package/dist/src/cli/commands/content.d.ts.map +1 -1
- package/dist/src/cli/commands/content.js +37 -0
- package/dist/src/cli/commands/content.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +21 -4
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/interaction.d.ts.map +1 -1
- package/dist/src/cli/commands/interaction.js +5 -14
- package/dist/src/cli/commands/interaction.js.map +1 -1
- package/dist/src/cli/commands/navigation.d.ts.map +1 -1
- package/dist/src/cli/commands/navigation.js +12 -6
- package/dist/src/cli/commands/navigation.js.map +1 -1
- package/dist/src/cli/commands/server.d.ts.map +1 -1
- package/dist/src/cli/commands/server.js +9 -3
- package/dist/src/cli/commands/server.js.map +1 -1
- package/dist/src/cli/commands/session.d.ts.map +1 -1
- package/dist/src/cli/commands/session.js +23 -5
- package/dist/src/cli/commands/session.js.map +1 -1
- package/dist/src/cli/server/manager.d.ts +1 -0
- package/dist/src/cli/server/manager.d.ts.map +1 -1
- package/dist/src/cli/server/manager.js +7 -12
- package/dist/src/cli/server/manager.js.map +1 -1
- package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
- package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
- package/dist/src/middleware/lifecycle-activity.js +21 -0
- package/dist/src/middleware/lifecycle-activity.js.map +1 -0
- package/dist/src/openapi/spec.d.ts +4 -0
- package/dist/src/openapi/spec.d.ts.map +1 -0
- package/dist/src/openapi/spec.js +730 -0
- package/dist/src/openapi/spec.js.map +1 -0
- package/dist/src/routes/core.d.ts.map +1 -1
- package/dist/src/routes/core.js +545 -58
- package/dist/src/routes/core.js.map +1 -1
- package/dist/src/routes/docs.d.ts +3 -0
- package/dist/src/routes/docs.d.ts.map +1 -0
- package/dist/src/routes/docs.js +23 -0
- package/dist/src/routes/docs.js.map +1 -0
- package/dist/src/routes/openclaw.d.ts.map +1 -1
- package/dist/src/routes/openclaw.js +317 -90
- package/dist/src/routes/openclaw.js.map +1 -1
- package/dist/src/server.js +55 -4
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/context-pool.d.ts +21 -4
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +290 -71
- package/dist/src/services/context-pool.js.map +1 -1
- package/dist/src/services/download.d.ts +2 -0
- package/dist/src/services/download.d.ts.map +1 -1
- package/dist/src/services/download.js +110 -80
- package/dist/src/services/download.js.map +1 -1
- package/dist/src/services/lifecycle-controller.d.ts +40 -0
- package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
- package/dist/src/services/lifecycle-controller.js +106 -0
- package/dist/src/services/lifecycle-controller.js.map +1 -0
- package/dist/src/services/resource-extractor.d.ts +1 -0
- package/dist/src/services/resource-extractor.d.ts.map +1 -1
- package/dist/src/services/resource-extractor.js +7 -0
- package/dist/src/services/resource-extractor.js.map +1 -1
- package/dist/src/services/session.d.ts +109 -4
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +622 -64
- package/dist/src/services/session.js.map +1 -1
- package/dist/src/services/structured-extractor.d.ts +39 -0
- package/dist/src/services/structured-extractor.d.ts.map +1 -0
- package/dist/src/services/structured-extractor.js +487 -0
- package/dist/src/services/structured-extractor.js.map +1 -0
- package/dist/src/services/tab.d.ts +30 -3
- package/dist/src/services/tab.d.ts.map +1 -1
- package/dist/src/services/tab.js +872 -124
- package/dist/src/services/tab.js.map +1 -1
- package/dist/src/services/tracing.d.ts +7 -0
- package/dist/src/services/tracing.d.ts.map +1 -1
- package/dist/src/services/tracing.js +200 -19
- package/dist/src/services/tracing.js.map +1 -1
- package/dist/src/services/vnc.d.ts.map +1 -1
- package/dist/src/services/vnc.js +5 -3
- package/dist/src/services/vnc.js.map +1 -1
- package/dist/src/services/youtube.js +1 -1
- package/dist/src/services/youtube.js.map +1 -1
- package/dist/src/types.d.ts +71 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/config.d.ts +79 -3
- package/dist/src/utils/config.d.ts.map +1 -1
- package/dist/src/utils/config.js +145 -3
- package/dist/src/utils/config.js.map +1 -1
- package/dist/src/utils/presets.d.ts.map +1 -1
- package/dist/src/utils/presets.js +3 -1
- package/dist/src/utils/presets.js.map +1 -1
- package/dist/src/utils/proxy-profiles.d.ts +18 -0
- package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
- package/dist/src/utils/proxy-profiles.js +197 -0
- package/dist/src/utils/proxy-profiles.js.map +1 -0
- package/dist/src/utils/sidecar-version.d.ts +12 -0
- package/dist/src/utils/sidecar-version.d.ts.map +1 -0
- package/dist/src/utils/sidecar-version.js +63 -0
- package/dist/src/utils/sidecar-version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/openclaw.plugin.json +39 -0
- package/package.json +16 -4
- package/plugin.ts +949 -0
package/dist/src/routes/core.js
CHANGED
|
@@ -47,11 +47,13 @@ const rate_limit_1 = require("../middleware/rate-limit");
|
|
|
47
47
|
const config_1 = require("../utils/config");
|
|
48
48
|
const presets_1 = require("../utils/presets");
|
|
49
49
|
const context_pool_1 = require("../services/context-pool");
|
|
50
|
+
const lifecycle_controller_1 = require("../services/lifecycle-controller");
|
|
50
51
|
const vnc_1 = require("../services/vnc");
|
|
51
52
|
const session_1 = require("../services/session");
|
|
52
53
|
const tab_1 = require("../services/tab");
|
|
53
54
|
const download_1 = require("../services/download");
|
|
54
55
|
const resource_extractor_1 = require("../services/resource-extractor");
|
|
56
|
+
const structured_extractor_1 = require("../services/structured-extractor");
|
|
55
57
|
const batch_downloader_1 = require("../services/batch-downloader");
|
|
56
58
|
const tracing_1 = require("../services/tracing");
|
|
57
59
|
const CONFIG = (0, config_1.loadConfig)();
|
|
@@ -82,6 +84,27 @@ function getTracingErrorStatus(err) {
|
|
|
82
84
|
return 409;
|
|
83
85
|
if (message.includes('No active chunk'))
|
|
84
86
|
return 400;
|
|
87
|
+
if (message.includes('Invalid trace output path'))
|
|
88
|
+
return 400;
|
|
89
|
+
return 500;
|
|
90
|
+
}
|
|
91
|
+
function getTraceArtifactErrorStatus(err) {
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
if (message.includes('Invalid trace filename'))
|
|
94
|
+
return 400;
|
|
95
|
+
if (message.includes('does not belong to this user'))
|
|
96
|
+
return 404;
|
|
97
|
+
if (typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT')
|
|
98
|
+
return 404;
|
|
99
|
+
return 500;
|
|
100
|
+
}
|
|
101
|
+
function getRouteErrorStatus(err) {
|
|
102
|
+
if (typeof err === 'object' && err !== null && 'statusCode' in err && typeof err.statusCode === 'number') {
|
|
103
|
+
return err.statusCode;
|
|
104
|
+
}
|
|
105
|
+
if (typeof err === 'object' && err !== null && 'status' in err && typeof err.status === 'number') {
|
|
106
|
+
return err.status;
|
|
107
|
+
}
|
|
85
108
|
return 500;
|
|
86
109
|
}
|
|
87
110
|
// Import cookies into a user's browser context (Playwright cookies format)
|
|
@@ -147,7 +170,33 @@ router.post('/sessions/:userId/cookies', express_1.default.json({ limit: '512kb'
|
|
|
147
170
|
session = found.session;
|
|
148
171
|
}
|
|
149
172
|
else {
|
|
150
|
-
|
|
173
|
+
const canonical = (0, session_1.getCanonicalProfile)(userId);
|
|
174
|
+
if (!canonical) {
|
|
175
|
+
(0, logging_1.log)('warn', 'cookie import rejected: no canonical profile', { userId: String(userId) });
|
|
176
|
+
return res.status(409).json({
|
|
177
|
+
error: 'No canonical profile',
|
|
178
|
+
message: 'Cannot import cookies without an established canonical profile. Create a tab via POST /tabs first.',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const existingSessions = (0, session_1.getSessionsForUser)(userId);
|
|
182
|
+
if (existingSessions.length === 0) {
|
|
183
|
+
(0, logging_1.log)('warn', 'cookie import rejected: no active session', { userId: String(userId) });
|
|
184
|
+
return res.status(409).json({
|
|
185
|
+
error: 'No active session',
|
|
186
|
+
message: 'Cannot import cookies without an active session. Create a tab via POST /tabs first.',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (existingSessions.length > 1) {
|
|
190
|
+
(0, logging_1.log)('warn', 'cookie import rejected: ambiguous active sessions', {
|
|
191
|
+
userId: String(userId),
|
|
192
|
+
sessionCount: existingSessions.length,
|
|
193
|
+
});
|
|
194
|
+
return res.status(409).json({
|
|
195
|
+
error: 'Ambiguous active sessions',
|
|
196
|
+
message: 'Multiple active browser contexts exist for this user. Provide tabId to import cookies into a specific context.',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
session = existingSessions[0][1];
|
|
151
200
|
}
|
|
152
201
|
await session.context.addCookies(sanitized);
|
|
153
202
|
const result = { ok: true, userId: String(userId), count: sanitized.length };
|
|
@@ -232,12 +281,55 @@ router.get('/presets', (_req, res) => {
|
|
|
232
281
|
});
|
|
233
282
|
// Create new tab
|
|
234
283
|
router.post('/tabs', async (req, res) => {
|
|
284
|
+
let createUserId;
|
|
285
|
+
let createSessionKey;
|
|
286
|
+
let createdSessionProfile = false;
|
|
287
|
+
let createdSessionProfileSignature;
|
|
288
|
+
let createdDefaultSessionProfileClaim = false;
|
|
289
|
+
let releaseSessionProfileCreate;
|
|
290
|
+
let isFirstCreator = false;
|
|
291
|
+
let stagedGeneration;
|
|
235
292
|
try {
|
|
293
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
294
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
295
|
+
}
|
|
296
|
+
// Record activity at START to prevent idle cleanup race during tab creation
|
|
297
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
236
298
|
const { userId, sessionKey, listItemId, url, preset, locale, timezoneId, geolocation, viewport } = req.body;
|
|
299
|
+
const { proxy, proxyProfile, geoMode } = req.body;
|
|
237
300
|
const resolvedSessionKey = sessionKey || listItemId;
|
|
238
301
|
if (!userId || !resolvedSessionKey) {
|
|
239
302
|
return res.status(400).json({ error: 'userId and sessionKey required' });
|
|
240
303
|
}
|
|
304
|
+
createUserId = String(userId);
|
|
305
|
+
createSessionKey = String(resolvedSessionKey);
|
|
306
|
+
// Check for session profile conflict (proxy/geo drift)
|
|
307
|
+
let resolvedSessionProfile;
|
|
308
|
+
if (proxy || proxyProfile || geoMode) {
|
|
309
|
+
const { resolveSessionProfileInput, getConfiguredServerProxy, loadProxyProfiles } = await Promise.resolve().then(() => __importStar(require('../utils/proxy-profiles')));
|
|
310
|
+
const profileInput = {
|
|
311
|
+
preset,
|
|
312
|
+
locale,
|
|
313
|
+
timezoneId,
|
|
314
|
+
geolocation: geolocation,
|
|
315
|
+
viewport: viewport,
|
|
316
|
+
proxy: proxy,
|
|
317
|
+
proxyProfile,
|
|
318
|
+
geoMode,
|
|
319
|
+
};
|
|
320
|
+
const deps = {
|
|
321
|
+
serverProxy: getConfiguredServerProxy(CONFIG.proxy),
|
|
322
|
+
proxyProfiles: loadProxyProfiles(CONFIG.proxyProfilesFile),
|
|
323
|
+
};
|
|
324
|
+
try {
|
|
325
|
+
const resolvedProfileBase = resolveSessionProfileInput(profileInput, deps);
|
|
326
|
+
resolvedSessionProfile = { ...resolvedProfileBase, sessionKey: resolvedSessionKey };
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
330
|
+
return res.status(400).json({ error: message });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
241
333
|
let contextOverrides = null;
|
|
242
334
|
try {
|
|
243
335
|
contextOverrides = (0, presets_1.resolveContextOptions)({ preset, locale, timezoneId, geolocation, viewport });
|
|
@@ -251,40 +343,203 @@ router.post('/tabs', async (req, res) => {
|
|
|
251
343
|
if (validationError)
|
|
252
344
|
return res.status(400).json({ error: validationError });
|
|
253
345
|
}
|
|
254
|
-
const sessionMapKey = (0, session_1.getSessionMapKey)(userId, contextOverrides);
|
|
255
|
-
const session = await (0, session_1.getSession)(userId, contextOverrides);
|
|
256
|
-
const totalTabs = (0, session_1.countTotalTabsForSessions)([[sessionMapKey, session]]);
|
|
257
|
-
if (totalTabs >= session_1.MAX_TABS_PER_SESSION) {
|
|
258
|
-
return res.status(429).json({ error: 'Maximum tabs per session reached' });
|
|
259
|
-
}
|
|
260
|
-
const group = (0, session_1.getTabGroup)(session, resolvedSessionKey);
|
|
261
|
-
const page = await session.context.newPage();
|
|
262
|
-
const tabId = node_crypto_1.default.randomUUID();
|
|
263
|
-
page.__camofox_tabId = tabId;
|
|
264
|
-
const tabState = (0, tab_1.createTabState)(page);
|
|
265
|
-
group.set(tabId, tabState);
|
|
266
|
-
(0, session_1.indexTab)(tabId, sessionMapKey);
|
|
267
|
-
(0, download_1.registerDownloadListener)(tabId, String(userId), page);
|
|
268
346
|
if (url) {
|
|
269
|
-
const urlErr = (0, tab_1.
|
|
347
|
+
const urlErr = await (0, tab_1.validateNavigationUrl)(url, {
|
|
348
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
349
|
+
});
|
|
270
350
|
if (urlErr)
|
|
271
351
|
return res.status(400).json({ error: urlErr });
|
|
272
|
-
|
|
273
|
-
|
|
352
|
+
}
|
|
353
|
+
const requestedProfile = (0, session_1.createCanonicalProfile)(contextOverrides);
|
|
354
|
+
const requestHash = requestedProfile.hash;
|
|
355
|
+
const MAX_CANONICAL_RETRIES = 3;
|
|
356
|
+
for (let attempt = 0; attempt < MAX_CANONICAL_RETRIES; attempt++) {
|
|
357
|
+
const existingProfile = (0, session_1.getCanonicalProfile)(userId);
|
|
358
|
+
if (existingProfile) {
|
|
359
|
+
if (contextOverrides === null) {
|
|
360
|
+
contextOverrides = existingProfile.resolvedOverrides;
|
|
361
|
+
}
|
|
362
|
+
else if (requestHash !== existingProfile.hash) {
|
|
363
|
+
(0, logging_1.log)('warn', 'canonical profile conflict', { userId: String(userId) });
|
|
364
|
+
return res.status(409).json({
|
|
365
|
+
error: 'Context override conflict',
|
|
366
|
+
message: 'A canonical profile already exists for this user with different overrides. Close the session first to reconfigure.',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
contextOverrides = existingProfile.resolvedOverrides;
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
const mutex = (0, session_1.acquireFirstCreateMutex)(userId);
|
|
375
|
+
if (mutex.acquired) {
|
|
376
|
+
isFirstCreator = true;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
await mutex.wait;
|
|
380
|
+
}
|
|
381
|
+
if (!(0, session_1.getCanonicalProfile)(userId) && !isFirstCreator) {
|
|
382
|
+
(0, logging_1.log)('error', 'canonical profile acquisition failed after retries', { userId: String(userId) });
|
|
383
|
+
return res.status(503).json({ error: 'Could not acquire canonical profile, try again' });
|
|
384
|
+
}
|
|
385
|
+
if (resolvedSessionProfile) {
|
|
386
|
+
while (true) {
|
|
387
|
+
await (0, session_1.waitForSessionProfileCreate)(userId, resolvedSessionKey);
|
|
388
|
+
const existingProfile = (0, session_1.getEstablishedSessionProfile)(userId, resolvedSessionKey);
|
|
389
|
+
if (existingProfile) {
|
|
390
|
+
if (existingProfile.signature !== resolvedSessionProfile.signature) {
|
|
391
|
+
return res.status(409).json({ error: 'Session profile conflict' });
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if ((0, session_1.hasDefaultSessionProfileRuntime)(userId, resolvedSessionKey)) {
|
|
396
|
+
return res.status(409).json({ error: 'Session profile conflict' });
|
|
397
|
+
}
|
|
398
|
+
const mutex = (0, session_1.acquireSessionProfileCreateMutex)(userId, resolvedSessionKey, resolvedSessionProfile.signature);
|
|
399
|
+
if (mutex.acquired) {
|
|
400
|
+
releaseSessionProfileCreate = mutex.release;
|
|
401
|
+
try {
|
|
402
|
+
(0, session_1.establishSessionProfile)(userId, resolvedSessionKey, resolvedSessionProfile);
|
|
403
|
+
createdSessionProfile = true;
|
|
404
|
+
createdSessionProfileSignature = resolvedSessionProfile.signature;
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
releaseSessionProfileCreate(false);
|
|
408
|
+
releaseSessionProfileCreate = undefined;
|
|
409
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
410
|
+
if (message === 'Session profile conflict') {
|
|
411
|
+
return res.status(409).json({ error: 'Session profile conflict' });
|
|
412
|
+
}
|
|
413
|
+
return res.status(400).json({ error: message });
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
await mutex.wait;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
await (0, session_1.waitForSessionProfileCreate)(userId, resolvedSessionKey);
|
|
422
|
+
if (!(0, session_1.getEstablishedSessionProfile)(userId, resolvedSessionKey)) {
|
|
423
|
+
createdDefaultSessionProfileClaim = (0, session_1.claimDefaultSessionProfileRuntime)(userId, resolvedSessionKey);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const establishedProfile = (0, session_1.getEstablishedSessionProfile)(userId, resolvedSessionKey);
|
|
427
|
+
const sessionMapKey = establishedProfile
|
|
428
|
+
? (0, session_1.getSessionMapKey)(userId, resolvedSessionKey, establishedProfile.signature)
|
|
429
|
+
: (0, session_1.getSessionMapKey)(userId, contextOverrides);
|
|
430
|
+
let tabId;
|
|
431
|
+
let pageUrl;
|
|
432
|
+
if (isFirstCreator) {
|
|
433
|
+
const staged = await (0, session_1.createStagedSession)(userId, contextOverrides, resolvedSessionKey);
|
|
434
|
+
stagedGeneration = staged.generation;
|
|
435
|
+
const { session, generation } = staged;
|
|
436
|
+
const page = await session.context.newPage();
|
|
437
|
+
tabId = node_crypto_1.default.randomUUID();
|
|
438
|
+
page.__camofox_tabId = tabId;
|
|
439
|
+
const tabState = await (0, tab_1.createTabState)(page);
|
|
440
|
+
(0, download_1.registerDownloadListener)(tabId, String(userId), page);
|
|
441
|
+
(0, download_1.markDownloadsStaged)(tabId);
|
|
442
|
+
if (url) {
|
|
443
|
+
await (0, tab_1.navigateWithSafetyGuard)(page, url, {
|
|
444
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
445
|
+
waitUntil: 'domcontentloaded',
|
|
446
|
+
timeout: 30000,
|
|
447
|
+
});
|
|
448
|
+
tabState.visitedUrls.add(url);
|
|
449
|
+
}
|
|
450
|
+
const committed = (0, session_1.commitStagedFirstUse)(userId, session, contextOverrides, {
|
|
451
|
+
tabId,
|
|
452
|
+
sessionMapKey,
|
|
453
|
+
sessionKey: resolvedSessionKey,
|
|
454
|
+
tabState,
|
|
455
|
+
}, generation);
|
|
456
|
+
if (!committed) {
|
|
457
|
+
await (0, session_1.rollbackStagedFirstUse)(createUserId ?? userId, generation).catch(() => { });
|
|
458
|
+
if (createdSessionProfile && createdSessionProfileSignature) {
|
|
459
|
+
await (0, session_1.rollbackSessionProfileRuntime)(userId, resolvedSessionKey, createdSessionProfileSignature);
|
|
460
|
+
}
|
|
461
|
+
if (createdDefaultSessionProfileClaim) {
|
|
462
|
+
(0, session_1.clearDefaultSessionProfileClaim)(userId, resolvedSessionKey);
|
|
463
|
+
createdDefaultSessionProfileClaim = false;
|
|
464
|
+
}
|
|
465
|
+
releaseSessionProfileCreate?.(false);
|
|
466
|
+
releaseSessionProfileCreate = undefined;
|
|
467
|
+
return res.status(409).json({ error: 'Session closed during creation' });
|
|
468
|
+
}
|
|
469
|
+
(0, download_1.commitStagedDownloads)(tabId);
|
|
470
|
+
pageUrl = page.url();
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
const session = await (0, session_1.getSession)(userId, contextOverrides, resolvedSessionKey);
|
|
474
|
+
const totalTabs = (0, session_1.countTotalTabsForSessions)([[sessionMapKey, session]]);
|
|
475
|
+
if (totalTabs >= session_1.MAX_TABS_PER_SESSION) {
|
|
476
|
+
if (createdSessionProfile && createdSessionProfileSignature) {
|
|
477
|
+
await (0, session_1.rollbackSessionProfileRuntime)(userId, resolvedSessionKey, createdSessionProfileSignature);
|
|
478
|
+
}
|
|
479
|
+
if (createdDefaultSessionProfileClaim) {
|
|
480
|
+
(0, session_1.clearDefaultSessionProfileClaim)(userId, resolvedSessionKey);
|
|
481
|
+
createdDefaultSessionProfileClaim = false;
|
|
482
|
+
}
|
|
483
|
+
releaseSessionProfileCreate?.(false);
|
|
484
|
+
releaseSessionProfileCreate = undefined;
|
|
485
|
+
return res.status(429).json({ error: 'Maximum tabs per session reached' });
|
|
486
|
+
}
|
|
487
|
+
const group = (0, session_1.getTabGroup)(session, resolvedSessionKey);
|
|
488
|
+
const page = await session.context.newPage();
|
|
489
|
+
tabId = node_crypto_1.default.randomUUID();
|
|
490
|
+
page.__camofox_tabId = tabId;
|
|
491
|
+
const tabState = await (0, tab_1.createTabState)(page);
|
|
492
|
+
group.set(tabId, tabState);
|
|
493
|
+
(0, session_1.indexTab)(tabId, sessionMapKey);
|
|
494
|
+
(0, download_1.registerDownloadListener)(tabId, String(userId), page);
|
|
495
|
+
if (url) {
|
|
496
|
+
await (0, tab_1.navigateWithSafetyGuard)(page, url, {
|
|
497
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
498
|
+
waitUntil: 'domcontentloaded',
|
|
499
|
+
timeout: 30000,
|
|
500
|
+
});
|
|
501
|
+
tabState.visitedUrls.add(url);
|
|
502
|
+
}
|
|
503
|
+
pageUrl = page.url();
|
|
274
504
|
}
|
|
275
505
|
(0, logging_1.log)('info', 'tab created', {
|
|
276
506
|
reqId: req.reqId,
|
|
277
507
|
tabId,
|
|
278
508
|
userId,
|
|
279
509
|
sessionKey: resolvedSessionKey,
|
|
280
|
-
url:
|
|
510
|
+
url: pageUrl,
|
|
281
511
|
});
|
|
282
|
-
|
|
512
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
513
|
+
releaseSessionProfileCreate?.(true);
|
|
514
|
+
releaseSessionProfileCreate = undefined;
|
|
515
|
+
createdSessionProfile = false;
|
|
516
|
+
return res.json({ tabId, url: pageUrl });
|
|
283
517
|
}
|
|
284
518
|
catch (err) {
|
|
519
|
+
if (isFirstCreator && createUserId) {
|
|
520
|
+
if (stagedGeneration) {
|
|
521
|
+
await (0, session_1.rollbackStagedFirstUse)(createUserId, stagedGeneration).catch(() => { });
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
(0, session_1.rollbackCanonicalMutex)(createUserId);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (createdSessionProfile && createUserId && createSessionKey) {
|
|
528
|
+
if (createdSessionProfileSignature) {
|
|
529
|
+
await (0, session_1.rollbackSessionProfileRuntime)(createUserId, createSessionKey, createdSessionProfileSignature);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
(0, session_1.clearSessionProfile)(createUserId, createSessionKey);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (createdDefaultSessionProfileClaim && createUserId && createSessionKey) {
|
|
536
|
+
(0, session_1.clearDefaultSessionProfileClaim)(createUserId, createSessionKey);
|
|
537
|
+
}
|
|
538
|
+
releaseSessionProfileCreate?.(false);
|
|
539
|
+
releaseSessionProfileCreate = undefined;
|
|
285
540
|
const message = err instanceof Error ? err.message : String(err);
|
|
286
541
|
(0, logging_1.log)('error', 'tab create failed', { reqId: req.reqId, error: message });
|
|
287
|
-
return res.status(
|
|
542
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
288
543
|
}
|
|
289
544
|
});
|
|
290
545
|
// GET /tabs - List all tabs (OpenClaw expects this)
|
|
@@ -320,6 +575,9 @@ router.get('/tabs', async (req, res) => {
|
|
|
320
575
|
router.post('/tabs/:tabId/navigate', async (req, res) => {
|
|
321
576
|
const tabId = req.params.tabId;
|
|
322
577
|
try {
|
|
578
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
579
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
580
|
+
}
|
|
323
581
|
const { userId, url, macro, query } = req.body;
|
|
324
582
|
if (!userId)
|
|
325
583
|
return res.status(400).json({ error: 'userId required' });
|
|
@@ -336,24 +594,30 @@ router.post('/tabs/:tabId/navigate', async (req, res) => {
|
|
|
336
594
|
}
|
|
337
595
|
if (!targetUrl)
|
|
338
596
|
return { status: 400, body: { error: 'url or macro required' } };
|
|
339
|
-
const urlErr = (0, tab_1.
|
|
597
|
+
const urlErr = await (0, tab_1.validateNavigationUrl)(targetUrl, {
|
|
598
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
599
|
+
});
|
|
340
600
|
if (urlErr)
|
|
341
601
|
return { status: 400, body: { error: urlErr } };
|
|
342
|
-
await tabState.page
|
|
602
|
+
await (0, tab_1.navigateWithSafetyGuard)(tabState.page, targetUrl, {
|
|
603
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
604
|
+
waitUntil: 'domcontentloaded',
|
|
605
|
+
timeout: 30000,
|
|
606
|
+
});
|
|
343
607
|
tabState.visitedUrls.add(targetUrl);
|
|
344
608
|
tabState.refs = await (0, tab_1.buildRefs)(tabState.page);
|
|
345
609
|
return { status: 200, body: { ok: true, url: tabState.page.url() } };
|
|
346
610
|
}), CONFIG.handlerTimeoutMs, 'navigate'));
|
|
347
611
|
if (result.status !== 200)
|
|
348
612
|
return res.status(result.status).json(result.body);
|
|
613
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
349
614
|
(0, logging_1.log)('info', 'navigated', { reqId: req.reqId, tabId, url: result.body.url });
|
|
350
615
|
return res.json(result.body);
|
|
351
616
|
}
|
|
352
617
|
catch (err) {
|
|
353
618
|
const message = err instanceof Error ? err.message : String(err);
|
|
354
619
|
(0, logging_1.log)('error', 'navigate failed', { reqId: req.reqId, tabId, error: message });
|
|
355
|
-
|
|
356
|
-
return res.status(status).json({ error: (0, errors_1.safeError)(err) });
|
|
620
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
357
621
|
}
|
|
358
622
|
});
|
|
359
623
|
// Snapshot
|
|
@@ -368,12 +632,15 @@ router.get('/tabs/:tabId/snapshot', async (req, res) => {
|
|
|
368
632
|
return res.status(404).json({ error: 'Tab not found' });
|
|
369
633
|
const { tabState } = found;
|
|
370
634
|
tabState.toolCalls++;
|
|
371
|
-
const
|
|
635
|
+
const rawOffset = Number(req.query.offset);
|
|
636
|
+
const offset = Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : 0;
|
|
637
|
+
const raw = await (0, session_1.withUserLimit)(String(userId), CONFIG.maxConcurrentPerUser, () => (0, tab_1.withTimeout)((0, tab_1.snapshotTab)(tabState), CONFIG.handlerTimeoutMs, 'snapshot'));
|
|
638
|
+
const result = (0, tab_1.buildSnapshotPayload)(raw, offset);
|
|
372
639
|
(0, logging_1.log)('info', 'snapshot', {
|
|
373
640
|
reqId: req.reqId,
|
|
374
641
|
tabId,
|
|
375
642
|
url: result.url,
|
|
376
|
-
snapshotLen: result.snapshot
|
|
643
|
+
snapshotLen: result.snapshot.length,
|
|
377
644
|
refsCount: result.refsCount,
|
|
378
645
|
});
|
|
379
646
|
return res.json(result);
|
|
@@ -381,30 +648,37 @@ router.get('/tabs/:tabId/snapshot', async (req, res) => {
|
|
|
381
648
|
catch (err) {
|
|
382
649
|
const message = err instanceof Error ? err.message : String(err);
|
|
383
650
|
(0, logging_1.log)('error', 'snapshot failed', { reqId: req.reqId, tabId: req.params.tabId, error: message });
|
|
384
|
-
return res.status(
|
|
651
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
385
652
|
}
|
|
386
653
|
});
|
|
387
654
|
// Wait for page ready
|
|
388
655
|
router.post('/tabs/:tabId/wait', async (req, res) => {
|
|
389
656
|
try {
|
|
657
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
658
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
659
|
+
}
|
|
390
660
|
const { userId, timeout = 10000, waitForNetwork = true } = req.body;
|
|
391
661
|
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
392
662
|
if (!found)
|
|
393
663
|
return res.status(404).json({ error: 'Tab not found' });
|
|
394
664
|
const { tabState } = found;
|
|
395
665
|
const ready = await (0, tab_1.waitForPageReady)(tabState.page, { timeout, waitForNetwork });
|
|
666
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
396
667
|
return res.json({ ok: true, ready });
|
|
397
668
|
}
|
|
398
669
|
catch (err) {
|
|
399
670
|
const message = err instanceof Error ? err.message : String(err);
|
|
400
671
|
(0, logging_1.log)('error', 'wait failed', { reqId: req.reqId, error: message });
|
|
401
|
-
return res.status(
|
|
672
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
402
673
|
}
|
|
403
674
|
});
|
|
404
675
|
// Click
|
|
405
676
|
router.post('/tabs/:tabId/click', async (req, res) => {
|
|
406
677
|
const tabId = req.params.tabId;
|
|
407
678
|
try {
|
|
679
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
680
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
681
|
+
}
|
|
408
682
|
const { userId, ref, selector } = req.body;
|
|
409
683
|
if (!userId)
|
|
410
684
|
return res.status(400).json({ error: 'userId required' });
|
|
@@ -425,21 +699,22 @@ router.post('/tabs/:tabId/click', async (req, res) => {
|
|
|
425
699
|
}));
|
|
426
700
|
}
|
|
427
701
|
(0, logging_1.log)('info', 'clicked', { reqId: req.reqId, tabId, url: result.url });
|
|
702
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
428
703
|
return res.json(responseObj);
|
|
429
704
|
}
|
|
430
705
|
catch (err) {
|
|
431
|
-
const statusCode = err?.statusCode;
|
|
432
706
|
const message = err instanceof Error ? err.message : String(err);
|
|
433
707
|
(0, logging_1.log)('error', 'click failed', { reqId: req.reqId, tabId, error: message });
|
|
434
|
-
|
|
435
|
-
return res.status(400).json({ error: message });
|
|
436
|
-
return res.status(500).json({ error: (0, errors_1.safeError)(err) });
|
|
708
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
437
709
|
}
|
|
438
710
|
});
|
|
439
711
|
// Type
|
|
440
712
|
router.post('/tabs/:tabId/type', async (req, res) => {
|
|
441
713
|
const tabId = req.params.tabId;
|
|
442
714
|
try {
|
|
715
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
716
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
717
|
+
}
|
|
443
718
|
const { userId, ref, selector, text } = req.body;
|
|
444
719
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
445
720
|
if (!found)
|
|
@@ -448,21 +723,22 @@ router.post('/tabs/:tabId/type', async (req, res) => {
|
|
|
448
723
|
tabState.toolCalls++;
|
|
449
724
|
const textValue = String(text ?? '');
|
|
450
725
|
const result = await (0, tab_1.withTimeout)((0, tab_1.typeTab)(tabId, tabState, { ref, selector, text: textValue }), (0, tab_1.calculateTypeTimeoutMs)(textValue), 'type');
|
|
726
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
451
727
|
return res.json(result);
|
|
452
728
|
}
|
|
453
729
|
catch (err) {
|
|
454
|
-
const statusCode = err?.statusCode;
|
|
455
730
|
const message = err instanceof Error ? err.message : String(err);
|
|
456
731
|
(0, logging_1.log)('error', 'type failed', { reqId: req.reqId, error: message });
|
|
457
|
-
|
|
458
|
-
return res.status(400).json({ error: message });
|
|
459
|
-
return res.status(500).json({ error: (0, errors_1.safeError)(err) });
|
|
732
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
460
733
|
}
|
|
461
734
|
});
|
|
462
735
|
// Press key
|
|
463
736
|
router.post('/tabs/:tabId/press', async (req, res) => {
|
|
464
737
|
const tabId = req.params.tabId;
|
|
465
738
|
try {
|
|
739
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
740
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
741
|
+
}
|
|
466
742
|
const { userId, key } = req.body;
|
|
467
743
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
468
744
|
if (!found)
|
|
@@ -470,38 +746,44 @@ router.post('/tabs/:tabId/press', async (req, res) => {
|
|
|
470
746
|
const { tabState } = found;
|
|
471
747
|
tabState.toolCalls++;
|
|
472
748
|
await (0, tab_1.withTimeout)((0, tab_1.pressTab)(tabId, tabState, String(key ?? '')), CONFIG.handlerTimeoutMs, 'press');
|
|
749
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
473
750
|
return res.json({ ok: true });
|
|
474
751
|
}
|
|
475
752
|
catch (err) {
|
|
476
753
|
const message = err instanceof Error ? err.message : String(err);
|
|
477
754
|
(0, logging_1.log)('error', 'press failed', { reqId: req.reqId, error: message });
|
|
478
|
-
return res.status(
|
|
755
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
479
756
|
}
|
|
480
757
|
});
|
|
481
758
|
// Scroll
|
|
482
759
|
router.post('/tabs/:tabId/scroll', async (req, res) => {
|
|
483
760
|
try {
|
|
761
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
762
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
763
|
+
}
|
|
484
764
|
const { userId, direction = 'down', amount = 500 } = req.body;
|
|
485
765
|
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
486
766
|
if (!found)
|
|
487
767
|
return res.status(404).json({ error: 'Tab not found' });
|
|
488
768
|
const { tabState } = found;
|
|
489
769
|
tabState.toolCalls++;
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return res.json({ ok: true });
|
|
770
|
+
const result = await (0, tab_1.withTimeout)((0, tab_1.scrollTab)(tabState, { direction, amount }), CONFIG.handlerTimeoutMs, 'scroll');
|
|
771
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
772
|
+
return res.json(result);
|
|
494
773
|
}
|
|
495
774
|
catch (err) {
|
|
496
775
|
const message = err instanceof Error ? err.message : String(err);
|
|
497
776
|
(0, logging_1.log)('error', 'scroll failed', { reqId: req.reqId, error: message });
|
|
498
|
-
return res.status(
|
|
777
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
499
778
|
}
|
|
500
779
|
});
|
|
501
780
|
// Scroll element (selector or ref)
|
|
502
781
|
router.post('/tabs/:tabId/scroll-element', async (req, res) => {
|
|
503
782
|
const tabId = req.params.tabId;
|
|
504
783
|
try {
|
|
784
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
785
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
786
|
+
}
|
|
505
787
|
const { userId, selector, ref, deltaX, deltaY, scrollTo } = req.body;
|
|
506
788
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
507
789
|
if (!found)
|
|
@@ -509,15 +791,13 @@ router.post('/tabs/:tabId/scroll-element', async (req, res) => {
|
|
|
509
791
|
const { tabState } = found;
|
|
510
792
|
tabState.toolCalls++;
|
|
511
793
|
const result = await (0, tab_1.scrollElementTab)(tabId, tabState, { selector, ref, deltaX, deltaY, scrollTo });
|
|
794
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
512
795
|
return res.json(result);
|
|
513
796
|
}
|
|
514
797
|
catch (err) {
|
|
515
|
-
const statusCode = err?.statusCode;
|
|
516
798
|
const message = err instanceof Error ? err.message : String(err);
|
|
517
799
|
(0, logging_1.log)('error', 'scroll-element failed', { reqId: req.reqId, tabId, error: message });
|
|
518
|
-
|
|
519
|
-
return res.status(400).json({ error: message });
|
|
520
|
-
return res.status(500).json({ error: (0, errors_1.safeError)(err) });
|
|
800
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
521
801
|
}
|
|
522
802
|
});
|
|
523
803
|
// Evaluate JS (API key optional)
|
|
@@ -540,12 +820,13 @@ router.post('/tabs/:tabId/evaluate', express_1.default.json({ limit: '64kb' }),
|
|
|
540
820
|
const { tabState } = found;
|
|
541
821
|
tabState.toolCalls++;
|
|
542
822
|
const result = await (0, tab_1.evaluateTab)(tabId, tabState, { expression, timeout });
|
|
823
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
543
824
|
return res.json(result);
|
|
544
825
|
}
|
|
545
826
|
catch (err) {
|
|
546
827
|
const message = err instanceof Error ? err.message : String(err);
|
|
547
828
|
(0, logging_1.log)('error', 'evaluate failed', { reqId: req.reqId, tabId, error: message });
|
|
548
|
-
return res.status(
|
|
829
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
549
830
|
}
|
|
550
831
|
});
|
|
551
832
|
// Evaluate JS extended (API key optional)
|
|
@@ -586,6 +867,7 @@ router.post('/tabs/:tabId/evaluate-extended', express_1.default.json({ limit: '6
|
|
|
586
867
|
const outerTimeout = effectiveTimeout + 10000;
|
|
587
868
|
const result = await (0, session_1.withUserLimit)(String(userId), CONFIG.maxConcurrentPerUser, async () => (0, tab_1.withTimeout)((0, tab_1.evaluateTabExtended)(tabId, tabState, { expression, timeout: effectiveTimeout }), outerTimeout, `Evaluate-extended timed out after ${outerTimeout}ms`));
|
|
588
869
|
if (result.ok) {
|
|
870
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
589
871
|
return res.json(result);
|
|
590
872
|
}
|
|
591
873
|
if (result.errorType === 'timeout') {
|
|
@@ -596,6 +878,10 @@ router.post('/tabs/:tabId/evaluate-extended', express_1.default.json({ limit: '6
|
|
|
596
878
|
catch (err) {
|
|
597
879
|
const message = err instanceof Error ? err.message : String(err);
|
|
598
880
|
(0, logging_1.log)('error', 'evaluate-extended failed', { reqId: req.reqId, tabId, error: message });
|
|
881
|
+
const status = getRouteErrorStatus(err);
|
|
882
|
+
if (status === 400) {
|
|
883
|
+
return res.status(400).json({ ok: false, error: (0, errors_1.safeError)(err), errorType: 'js_error' });
|
|
884
|
+
}
|
|
599
885
|
if (message.includes('timed out') || message.includes('Timeout')) {
|
|
600
886
|
return res.status(408).json({ ok: false, error: message, errorType: 'timeout' });
|
|
601
887
|
}
|
|
@@ -611,6 +897,9 @@ router.post('/tabs/:tabId/evaluate-extended', express_1.default.json({ limit: '6
|
|
|
611
897
|
router.post('/tabs/:tabId/back', async (req, res) => {
|
|
612
898
|
const tabId = req.params.tabId;
|
|
613
899
|
try {
|
|
900
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
901
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
902
|
+
}
|
|
614
903
|
const { userId } = req.body;
|
|
615
904
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
616
905
|
if (!found)
|
|
@@ -618,18 +907,22 @@ router.post('/tabs/:tabId/back', async (req, res) => {
|
|
|
618
907
|
const { tabState } = found;
|
|
619
908
|
tabState.toolCalls++;
|
|
620
909
|
const result = await (0, tab_1.withTimeout)((0, tab_1.backTab)(tabId, tabState), CONFIG.handlerTimeoutMs, 'back');
|
|
910
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
621
911
|
return res.json(result);
|
|
622
912
|
}
|
|
623
913
|
catch (err) {
|
|
624
914
|
const message = err instanceof Error ? err.message : String(err);
|
|
625
915
|
(0, logging_1.log)('error', 'back failed', { reqId: req.reqId, error: message });
|
|
626
|
-
return res.status(
|
|
916
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
627
917
|
}
|
|
628
918
|
});
|
|
629
919
|
// Forward
|
|
630
920
|
router.post('/tabs/:tabId/forward', async (req, res) => {
|
|
631
921
|
const tabId = req.params.tabId;
|
|
632
922
|
try {
|
|
923
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
924
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
925
|
+
}
|
|
633
926
|
const { userId } = req.body;
|
|
634
927
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
635
928
|
if (!found)
|
|
@@ -637,18 +930,22 @@ router.post('/tabs/:tabId/forward', async (req, res) => {
|
|
|
637
930
|
const { tabState } = found;
|
|
638
931
|
tabState.toolCalls++;
|
|
639
932
|
const result = await (0, tab_1.withTimeout)((0, tab_1.forwardTab)(tabId, tabState), CONFIG.handlerTimeoutMs, 'forward');
|
|
933
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
640
934
|
return res.json(result);
|
|
641
935
|
}
|
|
642
936
|
catch (err) {
|
|
643
937
|
const message = err instanceof Error ? err.message : String(err);
|
|
644
938
|
(0, logging_1.log)('error', 'forward failed', { reqId: req.reqId, error: message });
|
|
645
|
-
return res.status(
|
|
939
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
646
940
|
}
|
|
647
941
|
});
|
|
648
942
|
// Refresh
|
|
649
943
|
router.post('/tabs/:tabId/refresh', async (req, res) => {
|
|
650
944
|
const tabId = req.params.tabId;
|
|
651
945
|
try {
|
|
946
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
947
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
948
|
+
}
|
|
652
949
|
const { userId } = req.body;
|
|
653
950
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
654
951
|
if (!found)
|
|
@@ -656,12 +953,13 @@ router.post('/tabs/:tabId/refresh', async (req, res) => {
|
|
|
656
953
|
const { tabState } = found;
|
|
657
954
|
tabState.toolCalls++;
|
|
658
955
|
const result = await (0, tab_1.withTimeout)((0, tab_1.refreshTab)(tabId, tabState), CONFIG.handlerTimeoutMs, 'refresh');
|
|
956
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
659
957
|
return res.json(result);
|
|
660
958
|
}
|
|
661
959
|
catch (err) {
|
|
662
960
|
const message = err instanceof Error ? err.message : String(err);
|
|
663
961
|
(0, logging_1.log)('error', 'refresh failed', { reqId: req.reqId, error: message });
|
|
664
|
-
return res.status(
|
|
962
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
665
963
|
}
|
|
666
964
|
});
|
|
667
965
|
// Get links
|
|
@@ -708,6 +1006,54 @@ router.get('/tabs/:tabId/screenshot', async (req, res) => {
|
|
|
708
1006
|
return res.status(500).json({ error: (0, errors_1.safeError)(err) });
|
|
709
1007
|
}
|
|
710
1008
|
});
|
|
1009
|
+
// Images
|
|
1010
|
+
router.get('/tabs/:tabId/images', async (req, res) => {
|
|
1011
|
+
try {
|
|
1012
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1013
|
+
return res.status(403).json({ ok: false, error: 'Forbidden' });
|
|
1014
|
+
}
|
|
1015
|
+
const userId = req.query.userId;
|
|
1016
|
+
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
1017
|
+
if (!found) {
|
|
1018
|
+
(0, logging_1.log)('warn', 'images: tab not found', { reqId: req.reqId, tabId: req.params.tabId, userId });
|
|
1019
|
+
return res.status(404).json({ error: 'Tab not found' });
|
|
1020
|
+
}
|
|
1021
|
+
const selector = typeof req.query.selector === 'string' && req.query.selector ? req.query.selector : undefined;
|
|
1022
|
+
const extensions = Array.isArray(req.query.extensions)
|
|
1023
|
+
? req.query.extensions.map((value) => String(value)).filter(Boolean)
|
|
1024
|
+
: typeof req.query.extensions === 'string' && req.query.extensions
|
|
1025
|
+
? req.query.extensions
|
|
1026
|
+
.split(',')
|
|
1027
|
+
.map((value) => value.trim())
|
|
1028
|
+
.filter(Boolean)
|
|
1029
|
+
: undefined;
|
|
1030
|
+
const resolveBlobs = String(req.query.resolveBlobs || '').toLowerCase() === 'true';
|
|
1031
|
+
const triggerLazyLoad = String(req.query.triggerLazyLoad || '').toLowerCase() === 'true';
|
|
1032
|
+
const { tabState } = found;
|
|
1033
|
+
tabState.toolCalls++;
|
|
1034
|
+
const result = await (0, session_1.withUserLimit)(String(userId), CONFIG.maxConcurrentPerUser, () => (0, tab_1.withTimeout)((0, tab_1.withTabLock)(req.params.tabId, async () => (0, resource_extractor_1.extractImages)(tabState.page, {
|
|
1035
|
+
selector,
|
|
1036
|
+
extensions,
|
|
1037
|
+
resolveBlobs,
|
|
1038
|
+
triggerLazyLoad,
|
|
1039
|
+
})), CONFIG.handlerTimeoutMs, 'images'));
|
|
1040
|
+
return res.json({
|
|
1041
|
+
ok: result.ok,
|
|
1042
|
+
container: result.container,
|
|
1043
|
+
images: result.resources.images,
|
|
1044
|
+
totals: {
|
|
1045
|
+
images: result.totals.images,
|
|
1046
|
+
total: result.totals.images,
|
|
1047
|
+
},
|
|
1048
|
+
metadata: result.metadata,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1053
|
+
(0, logging_1.log)('error', 'images failed', { reqId: req.reqId, error: message });
|
|
1054
|
+
return res.status(500).json({ error: (0, errors_1.safeError)(err) });
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
711
1057
|
// Stats
|
|
712
1058
|
router.get('/tabs/:tabId/stats', async (req, res) => {
|
|
713
1059
|
try {
|
|
@@ -735,6 +1081,9 @@ router.get('/tabs/:tabId/stats', async (req, res) => {
|
|
|
735
1081
|
// Close tab
|
|
736
1082
|
router.delete('/tabs/:tabId', async (req, res) => {
|
|
737
1083
|
try {
|
|
1084
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1085
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1086
|
+
}
|
|
738
1087
|
const { userId } = req.body;
|
|
739
1088
|
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
740
1089
|
if (found) {
|
|
@@ -746,6 +1095,7 @@ router.delete('/tabs/:tabId', async (req, res) => {
|
|
|
746
1095
|
}
|
|
747
1096
|
(0, logging_1.log)('info', 'tab closed', { reqId: req.reqId, tabId: req.params.tabId, userId });
|
|
748
1097
|
}
|
|
1098
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
749
1099
|
return res.json({ ok: true });
|
|
750
1100
|
}
|
|
751
1101
|
catch (err) {
|
|
@@ -757,6 +1107,9 @@ router.delete('/tabs/:tabId', async (req, res) => {
|
|
|
757
1107
|
// Close tab group
|
|
758
1108
|
router.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
759
1109
|
try {
|
|
1110
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1111
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1112
|
+
}
|
|
760
1113
|
const { userId } = req.body;
|
|
761
1114
|
const sessionsForUser = (0, session_1.getSessionsForUser)(userId);
|
|
762
1115
|
for (const [sessionKey, session] of sessionsForUser) {
|
|
@@ -775,6 +1128,7 @@ router.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
|
775
1128
|
sessionKey,
|
|
776
1129
|
});
|
|
777
1130
|
}
|
|
1131
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
778
1132
|
return res.json({ ok: true });
|
|
779
1133
|
}
|
|
780
1134
|
catch (err) {
|
|
@@ -786,10 +1140,14 @@ router.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
|
786
1140
|
// Close session
|
|
787
1141
|
router.delete('/sessions/:userId', async (req, res) => {
|
|
788
1142
|
try {
|
|
1143
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1144
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1145
|
+
}
|
|
789
1146
|
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
790
1147
|
await (0, session_1.closeSessionsForUser)(userId);
|
|
791
1148
|
// Ensure downloads are cleaned even if the session was already partially removed.
|
|
792
1149
|
(0, download_1.cleanupUserDownloads)(userId);
|
|
1150
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
793
1151
|
return res.json({ ok: true });
|
|
794
1152
|
}
|
|
795
1153
|
catch (err) {
|
|
@@ -801,6 +1159,9 @@ router.delete('/sessions/:userId', async (req, res) => {
|
|
|
801
1159
|
// Toggle display mode (headless/headed/virtual) for a user session
|
|
802
1160
|
router.post('/sessions/:userId/toggle-display', async (req, res) => {
|
|
803
1161
|
try {
|
|
1162
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1163
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1164
|
+
}
|
|
804
1165
|
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
805
1166
|
const { headless } = req.body ?? {};
|
|
806
1167
|
if (typeof headless !== 'boolean' && headless !== 'virtual') {
|
|
@@ -808,14 +1169,28 @@ router.post('/sessions/:userId/toggle-display', async (req, res) => {
|
|
|
808
1169
|
error: 'headless must be a boolean or "virtual"',
|
|
809
1170
|
});
|
|
810
1171
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1172
|
+
const existingSessions = (0, session_1.getSessionsForUser)(userId);
|
|
1173
|
+
const prewarmProfileKey = existingSessions.length === 1 ? existingSessions[0][0] : undefined;
|
|
1174
|
+
const prewarmEntry = prewarmProfileKey ? context_pool_1.contextPool.getEntry(prewarmProfileKey) : undefined;
|
|
1175
|
+
const prewarmLaunchSettings = prewarmProfileKey && !prewarmEntry ? (0, session_1.getSessionProfileLaunchSettings)(userId, prewarmProfileKey) : undefined;
|
|
1176
|
+
const prewarmOptions = prewarmEntry ? prewarmEntry.seedOptions : prewarmLaunchSettings?.contextOverrides ?? undefined;
|
|
1177
|
+
const prewarmProxy = prewarmEntry ? prewarmEntry.proxyConfig : prewarmLaunchSettings?.proxy ?? null;
|
|
1178
|
+
let tabsInvalidated = false;
|
|
814
1179
|
let vncUrl;
|
|
815
1180
|
if (headless === true) {
|
|
1181
|
+
if (prewarmProfileKey) {
|
|
1182
|
+
// Existing tabs become invalid after context restart/close.
|
|
1183
|
+
await (0, session_1.closeSessionsForUser)(userId, { clearProfiles: false });
|
|
1184
|
+
tabsInvalidated = true;
|
|
1185
|
+
}
|
|
1186
|
+
context_pool_1.contextPool.setHeadlessOverride(userId, headless);
|
|
816
1187
|
await (0, vnc_1.stopVnc)(userId).catch(() => { });
|
|
817
1188
|
}
|
|
818
|
-
else {
|
|
1189
|
+
else if (prewarmProfileKey) {
|
|
1190
|
+
// Existing tabs become invalid after context restart.
|
|
1191
|
+
await (0, session_1.closeSessionsForUser)(userId, { clearProfiles: false });
|
|
1192
|
+
tabsInvalidated = true;
|
|
1193
|
+
await context_pool_1.contextPool.restartContext(userId, headless, prewarmProfileKey, prewarmOptions, prewarmProxy);
|
|
819
1194
|
const displayNum = (0, context_pool_1.getDisplayForUser)(userId);
|
|
820
1195
|
if (displayNum) {
|
|
821
1196
|
try {
|
|
@@ -828,13 +1203,21 @@ router.post('/sessions/:userId/toggle-display', async (req, res) => {
|
|
|
828
1203
|
}
|
|
829
1204
|
}
|
|
830
1205
|
}
|
|
1206
|
+
else {
|
|
1207
|
+
context_pool_1.contextPool.setHeadlessOverride(userId, headless);
|
|
1208
|
+
}
|
|
831
1209
|
const modeLabel = headless === false ? 'headed mode' : headless === 'virtual' ? 'virtual display mode' : 'headless mode';
|
|
832
1210
|
return res.json({
|
|
833
1211
|
ok: true,
|
|
834
1212
|
headless,
|
|
1213
|
+
tabsInvalidated,
|
|
835
1214
|
...(vncUrl
|
|
836
1215
|
? { vncUrl, message: 'Browser visible via VNC' }
|
|
837
|
-
: {
|
|
1216
|
+
: {
|
|
1217
|
+
message: tabsInvalidated
|
|
1218
|
+
? `Display mode updated to ${modeLabel}. Previous tabs invalidated — create new tabs.`
|
|
1219
|
+
: `Display mode override saved as ${modeLabel}. Existing tabs preserved; new contexts use the requested mode.`,
|
|
1220
|
+
}),
|
|
838
1221
|
userId,
|
|
839
1222
|
});
|
|
840
1223
|
}
|
|
@@ -952,6 +1335,9 @@ router.get('/downloads/:downloadId/content', async (req, res) => {
|
|
|
952
1335
|
// Downloads: delete
|
|
953
1336
|
router.delete('/downloads/:downloadId', async (req, res) => {
|
|
954
1337
|
try {
|
|
1338
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1339
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1340
|
+
}
|
|
955
1341
|
const userId = req.body?.userId || req.query.userId;
|
|
956
1342
|
if (!userId)
|
|
957
1343
|
return res.status(400).json({ ok: false, error: 'userId required' });
|
|
@@ -969,6 +1355,9 @@ router.delete('/downloads/:downloadId', async (req, res) => {
|
|
|
969
1355
|
// Extract resources from a scoped container
|
|
970
1356
|
router.post('/tabs/:tabId/extract-resources', async (req, res) => {
|
|
971
1357
|
try {
|
|
1358
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1359
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1360
|
+
}
|
|
972
1361
|
const { tabId } = req.params;
|
|
973
1362
|
const { userId, selector, types, extensions, resolveBlobs, triggerLazyLoad } = req.body;
|
|
974
1363
|
if (!userId)
|
|
@@ -994,9 +1383,50 @@ router.post('/tabs/:tabId/extract-resources', async (req, res) => {
|
|
|
994
1383
|
res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
995
1384
|
}
|
|
996
1385
|
});
|
|
1386
|
+
router.post('/tabs/:tabId/extract-structured', async (req, res) => {
|
|
1387
|
+
try {
|
|
1388
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1389
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1390
|
+
}
|
|
1391
|
+
const { tabId } = req.params;
|
|
1392
|
+
const { userId, schema } = req.body;
|
|
1393
|
+
if (!userId || typeof userId !== 'string') {
|
|
1394
|
+
return res.status(400).json({ ok: false, error: 'userId required' });
|
|
1395
|
+
}
|
|
1396
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
1397
|
+
return res.status(400).json({ ok: false, error: 'schema must be an object' });
|
|
1398
|
+
}
|
|
1399
|
+
const found = (0, session_1.findTabById)(tabId, userId);
|
|
1400
|
+
if (!found)
|
|
1401
|
+
return res.status(404).json({ ok: false, error: 'Tab not found' });
|
|
1402
|
+
const { tabState } = found;
|
|
1403
|
+
tabState.toolCalls++;
|
|
1404
|
+
const result = await (0, session_1.withUserLimit)(String(userId), CONFIG.maxConcurrentPerUser, () => (0, tab_1.withTimeout)((0, tab_1.withTabLock)(tabId, async () => (0, structured_extractor_1.extractStructuredData)(tabState.page, schema)), CONFIG.handlerTimeoutMs, 'extract-structured'));
|
|
1405
|
+
return res.json(result);
|
|
1406
|
+
}
|
|
1407
|
+
catch (err) {
|
|
1408
|
+
if (err instanceof structured_extractor_1.StructuredExtractSchemaError) {
|
|
1409
|
+
return res.status(err.statusCode).json({ ok: false, error: err.message });
|
|
1410
|
+
}
|
|
1411
|
+
if (err instanceof structured_extractor_1.StructuredExtractRuntimeError) {
|
|
1412
|
+
return res.status(err.statusCode).json({
|
|
1413
|
+
ok: false,
|
|
1414
|
+
error: 'Structured extraction failed',
|
|
1415
|
+
fieldPath: err.fieldPath,
|
|
1416
|
+
reason: err.reason,
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1420
|
+
(0, logging_1.log)('error', 'structured extract failed', { error: message });
|
|
1421
|
+
return res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
997
1424
|
// Batch download from a scoped container
|
|
998
1425
|
router.post('/tabs/:tabId/batch-download', async (req, res) => {
|
|
999
1426
|
try {
|
|
1427
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1428
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1429
|
+
}
|
|
1000
1430
|
const { tabId } = req.params;
|
|
1001
1431
|
const body = req.body;
|
|
1002
1432
|
const userId = body?.userId;
|
|
@@ -1093,12 +1523,13 @@ router.post('/tabs/:tabId/trace/stop', async (req, res) => {
|
|
|
1093
1523
|
return res.json({
|
|
1094
1524
|
ok: true,
|
|
1095
1525
|
path: result.path,
|
|
1526
|
+
filename: result.path ? (0, node_path_1.basename)(result.path) : undefined,
|
|
1096
1527
|
size: result.size,
|
|
1097
1528
|
alreadyStopped: true,
|
|
1098
1529
|
message: 'Trace was already stopped by chunk stop',
|
|
1099
1530
|
});
|
|
1100
1531
|
}
|
|
1101
|
-
return res.json({ ok: true, ...result });
|
|
1532
|
+
return res.json({ ok: true, ...result, filename: (0, node_path_1.basename)(result.path) });
|
|
1102
1533
|
}
|
|
1103
1534
|
catch (err) {
|
|
1104
1535
|
const status = getTracingErrorStatus(err);
|
|
@@ -1143,7 +1574,7 @@ router.post('/tabs/:tabId/trace/chunk/stop', async (req, res) => {
|
|
|
1143
1574
|
if (!tab)
|
|
1144
1575
|
return res.status(404).json({ ok: false, error: 'Tab not found' });
|
|
1145
1576
|
const result = await (0, tracing_1.stopTracingChunk)(userId, tab.page.context(), path);
|
|
1146
|
-
return res.json({ ok: true, ...result });
|
|
1577
|
+
return res.json({ ok: true, ...result, filename: (0, node_path_1.basename)(result.path) });
|
|
1147
1578
|
}
|
|
1148
1579
|
catch (err) {
|
|
1149
1580
|
const status = getTracingErrorStatus(err);
|
|
@@ -1170,6 +1601,62 @@ router.get('/tabs/:tabId/trace/status', async (req, res) => {
|
|
|
1170
1601
|
return res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1171
1602
|
}
|
|
1172
1603
|
});
|
|
1604
|
+
router.get('/sessions/:userId/traces', async (req, res) => {
|
|
1605
|
+
try {
|
|
1606
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1607
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1608
|
+
}
|
|
1609
|
+
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
1610
|
+
return res.json({ ok: true, traces: (0, tracing_1.listTraceArtifacts)(userId) });
|
|
1611
|
+
}
|
|
1612
|
+
catch (err) {
|
|
1613
|
+
return res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
router.get('/sessions/:userId/traces/:filename', async (req, res) => {
|
|
1617
|
+
try {
|
|
1618
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1619
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1620
|
+
}
|
|
1621
|
+
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
1622
|
+
const filename = req.params.filename;
|
|
1623
|
+
const filePath = (0, tracing_1.resolveTraceArtifactPath)(userId, filename);
|
|
1624
|
+
const safeName = filename.replace(/[\r\n\0"]/g, '');
|
|
1625
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
1626
|
+
res.setHeader('Content-Disposition', `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(safeName)}`);
|
|
1627
|
+
const stream = node_fs_1.default.createReadStream(filePath);
|
|
1628
|
+
stream.on('error', (streamErr) => {
|
|
1629
|
+
if (!res.headersSent) {
|
|
1630
|
+
const status = getTraceArtifactErrorStatus(streamErr);
|
|
1631
|
+
res.removeHeader('Content-Disposition');
|
|
1632
|
+
res.removeHeader('Content-Type');
|
|
1633
|
+
res.status(status).json({ ok: false, error: (0, errors_1.safeError)(streamErr) });
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
res.destroy();
|
|
1637
|
+
});
|
|
1638
|
+
stream.pipe(res);
|
|
1639
|
+
}
|
|
1640
|
+
catch (err) {
|
|
1641
|
+
const status = getTraceArtifactErrorStatus(err);
|
|
1642
|
+
return res.status(status).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
router.delete('/sessions/:userId/traces/:filename', async (req, res) => {
|
|
1646
|
+
try {
|
|
1647
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1648
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1649
|
+
}
|
|
1650
|
+
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
1651
|
+
const filename = req.params.filename;
|
|
1652
|
+
(0, tracing_1.deleteTraceArtifact)(userId, filename);
|
|
1653
|
+
return res.json({ ok: true });
|
|
1654
|
+
}
|
|
1655
|
+
catch (err) {
|
|
1656
|
+
const status = getTraceArtifactErrorStatus(err);
|
|
1657
|
+
return res.status(status).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1173
1660
|
router.get('/tabs/:tabId/console', async (req, res) => {
|
|
1174
1661
|
try {
|
|
1175
1662
|
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|