camofox-browser 2.1.1 → 2.4.1
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 +126 -0
- package/README.md +304 -33
- 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 +428 -53
- 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 +244 -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 +19 -3
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +248 -65
- 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 +84 -2
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +349 -47
- 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 +162 -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,23 @@ 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
|
+
session = existingSessions[0][1];
|
|
151
190
|
}
|
|
152
191
|
await session.context.addCookies(sanitized);
|
|
153
192
|
const result = { ok: true, userId: String(userId), count: sanitized.length };
|
|
@@ -232,12 +271,53 @@ router.get('/presets', (_req, res) => {
|
|
|
232
271
|
});
|
|
233
272
|
// Create new tab
|
|
234
273
|
router.post('/tabs', async (req, res) => {
|
|
274
|
+
let createUserId;
|
|
275
|
+
let isFirstCreator = false;
|
|
276
|
+
let stagedGeneration;
|
|
235
277
|
try {
|
|
278
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
279
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
280
|
+
}
|
|
281
|
+
// Record activity at START to prevent idle cleanup race during tab creation
|
|
282
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
236
283
|
const { userId, sessionKey, listItemId, url, preset, locale, timezoneId, geolocation, viewport } = req.body;
|
|
284
|
+
const { proxy, proxyProfile, geoMode } = req.body;
|
|
237
285
|
const resolvedSessionKey = sessionKey || listItemId;
|
|
238
286
|
if (!userId || !resolvedSessionKey) {
|
|
239
287
|
return res.status(400).json({ error: 'userId and sessionKey required' });
|
|
240
288
|
}
|
|
289
|
+
createUserId = String(userId);
|
|
290
|
+
// Check for session profile conflict (proxy/geo drift)
|
|
291
|
+
if (proxy || proxyProfile || geoMode) {
|
|
292
|
+
const { resolveSessionProfileInput, getConfiguredServerProxy, loadProxyProfiles } = await Promise.resolve().then(() => __importStar(require('../utils/proxy-profiles')));
|
|
293
|
+
const { establishSessionProfile } = await Promise.resolve().then(() => __importStar(require('../services/session')));
|
|
294
|
+
const profileInput = {
|
|
295
|
+
preset,
|
|
296
|
+
locale,
|
|
297
|
+
timezoneId,
|
|
298
|
+
geolocation: geolocation,
|
|
299
|
+
viewport: viewport,
|
|
300
|
+
proxy: proxy,
|
|
301
|
+
proxyProfile,
|
|
302
|
+
geoMode,
|
|
303
|
+
};
|
|
304
|
+
const deps = {
|
|
305
|
+
serverProxy: getConfiguredServerProxy(CONFIG.proxy),
|
|
306
|
+
proxyProfiles: loadProxyProfiles(CONFIG.proxyProfilesFile),
|
|
307
|
+
};
|
|
308
|
+
try {
|
|
309
|
+
const resolvedProfileBase = resolveSessionProfileInput(profileInput, deps);
|
|
310
|
+
const resolvedProfile = { ...resolvedProfileBase, sessionKey: resolvedSessionKey };
|
|
311
|
+
establishSessionProfile(userId, resolvedSessionKey, resolvedProfile);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
315
|
+
if (message === 'Session profile conflict') {
|
|
316
|
+
return res.status(409).json({ error: 'Session profile conflict' });
|
|
317
|
+
}
|
|
318
|
+
return res.status(400).json({ error: message });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
241
321
|
let contextOverrides = null;
|
|
242
322
|
try {
|
|
243
323
|
contextOverrides = (0, presets_1.resolveContextOptions)({ preset, locale, timezoneId, geolocation, viewport });
|
|
@@ -251,40 +331,125 @@ router.post('/tabs', async (req, res) => {
|
|
|
251
331
|
if (validationError)
|
|
252
332
|
return res.status(400).json({ error: validationError });
|
|
253
333
|
}
|
|
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
334
|
if (url) {
|
|
269
|
-
const urlErr = (0, tab_1.
|
|
335
|
+
const urlErr = await (0, tab_1.validateNavigationUrl)(url, {
|
|
336
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
337
|
+
});
|
|
270
338
|
if (urlErr)
|
|
271
339
|
return res.status(400).json({ error: urlErr });
|
|
272
|
-
|
|
273
|
-
|
|
340
|
+
}
|
|
341
|
+
const requestedProfile = (0, session_1.createCanonicalProfile)(contextOverrides);
|
|
342
|
+
const requestHash = requestedProfile.hash;
|
|
343
|
+
const MAX_CANONICAL_RETRIES = 3;
|
|
344
|
+
for (let attempt = 0; attempt < MAX_CANONICAL_RETRIES; attempt++) {
|
|
345
|
+
const existingProfile = (0, session_1.getCanonicalProfile)(userId);
|
|
346
|
+
if (existingProfile) {
|
|
347
|
+
if (contextOverrides === null) {
|
|
348
|
+
contextOverrides = existingProfile.resolvedOverrides;
|
|
349
|
+
}
|
|
350
|
+
else if (requestHash !== existingProfile.hash) {
|
|
351
|
+
(0, logging_1.log)('warn', 'canonical profile conflict', { userId: String(userId) });
|
|
352
|
+
return res.status(409).json({
|
|
353
|
+
error: 'Context override conflict',
|
|
354
|
+
message: 'A canonical profile already exists for this user with different overrides. Close the session first to reconfigure.',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
contextOverrides = existingProfile.resolvedOverrides;
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
const mutex = (0, session_1.acquireFirstCreateMutex)(userId);
|
|
363
|
+
if (mutex.acquired) {
|
|
364
|
+
isFirstCreator = true;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
await mutex.wait;
|
|
368
|
+
}
|
|
369
|
+
if (!(0, session_1.getCanonicalProfile)(userId) && !isFirstCreator) {
|
|
370
|
+
(0, logging_1.log)('error', 'canonical profile acquisition failed after retries', { userId: String(userId) });
|
|
371
|
+
return res.status(503).json({ error: 'Could not acquire canonical profile, try again' });
|
|
372
|
+
}
|
|
373
|
+
const sessionMapKey = (0, session_1.getSessionMapKey)(userId, contextOverrides);
|
|
374
|
+
let tabId;
|
|
375
|
+
let pageUrl;
|
|
376
|
+
if (isFirstCreator) {
|
|
377
|
+
const staged = await (0, session_1.createStagedSession)(userId, contextOverrides);
|
|
378
|
+
stagedGeneration = staged.generation;
|
|
379
|
+
const { session, generation } = staged;
|
|
380
|
+
const page = await session.context.newPage();
|
|
381
|
+
tabId = node_crypto_1.default.randomUUID();
|
|
382
|
+
page.__camofox_tabId = tabId;
|
|
383
|
+
const tabState = await (0, tab_1.createTabState)(page);
|
|
384
|
+
(0, download_1.registerDownloadListener)(tabId, String(userId), page);
|
|
385
|
+
(0, download_1.markDownloadsStaged)(tabId);
|
|
386
|
+
if (url) {
|
|
387
|
+
await (0, tab_1.navigateWithSafetyGuard)(page, url, {
|
|
388
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
389
|
+
waitUntil: 'domcontentloaded',
|
|
390
|
+
timeout: 30000,
|
|
391
|
+
});
|
|
392
|
+
tabState.visitedUrls.add(url);
|
|
393
|
+
}
|
|
394
|
+
const committed = (0, session_1.commitStagedFirstUse)(userId, session, contextOverrides, {
|
|
395
|
+
tabId,
|
|
396
|
+
sessionMapKey,
|
|
397
|
+
sessionKey: resolvedSessionKey,
|
|
398
|
+
tabState,
|
|
399
|
+
}, generation);
|
|
400
|
+
if (!committed) {
|
|
401
|
+
await (0, session_1.rollbackStagedFirstUse)(createUserId ?? userId, generation).catch(() => { });
|
|
402
|
+
return res.status(409).json({ error: 'Session closed during creation' });
|
|
403
|
+
}
|
|
404
|
+
(0, download_1.commitStagedDownloads)(tabId);
|
|
405
|
+
pageUrl = page.url();
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const session = await (0, session_1.getSession)(userId, contextOverrides);
|
|
409
|
+
const totalTabs = (0, session_1.countTotalTabsForSessions)([[sessionMapKey, session]]);
|
|
410
|
+
if (totalTabs >= session_1.MAX_TABS_PER_SESSION) {
|
|
411
|
+
return res.status(429).json({ error: 'Maximum tabs per session reached' });
|
|
412
|
+
}
|
|
413
|
+
const group = (0, session_1.getTabGroup)(session, resolvedSessionKey);
|
|
414
|
+
const page = await session.context.newPage();
|
|
415
|
+
tabId = node_crypto_1.default.randomUUID();
|
|
416
|
+
page.__camofox_tabId = tabId;
|
|
417
|
+
const tabState = await (0, tab_1.createTabState)(page);
|
|
418
|
+
group.set(tabId, tabState);
|
|
419
|
+
(0, session_1.indexTab)(tabId, sessionMapKey);
|
|
420
|
+
(0, download_1.registerDownloadListener)(tabId, String(userId), page);
|
|
421
|
+
if (url) {
|
|
422
|
+
await (0, tab_1.navigateWithSafetyGuard)(page, url, {
|
|
423
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
424
|
+
waitUntil: 'domcontentloaded',
|
|
425
|
+
timeout: 30000,
|
|
426
|
+
});
|
|
427
|
+
tabState.visitedUrls.add(url);
|
|
428
|
+
}
|
|
429
|
+
pageUrl = page.url();
|
|
274
430
|
}
|
|
275
431
|
(0, logging_1.log)('info', 'tab created', {
|
|
276
432
|
reqId: req.reqId,
|
|
277
433
|
tabId,
|
|
278
434
|
userId,
|
|
279
435
|
sessionKey: resolvedSessionKey,
|
|
280
|
-
url:
|
|
436
|
+
url: pageUrl,
|
|
281
437
|
});
|
|
282
|
-
|
|
438
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
439
|
+
return res.json({ tabId, url: pageUrl });
|
|
283
440
|
}
|
|
284
441
|
catch (err) {
|
|
442
|
+
if (isFirstCreator && createUserId) {
|
|
443
|
+
if (stagedGeneration) {
|
|
444
|
+
await (0, session_1.rollbackStagedFirstUse)(createUserId, stagedGeneration).catch(() => { });
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
(0, session_1.rollbackCanonicalMutex)(createUserId);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
285
450
|
const message = err instanceof Error ? err.message : String(err);
|
|
286
451
|
(0, logging_1.log)('error', 'tab create failed', { reqId: req.reqId, error: message });
|
|
287
|
-
return res.status(
|
|
452
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
288
453
|
}
|
|
289
454
|
});
|
|
290
455
|
// GET /tabs - List all tabs (OpenClaw expects this)
|
|
@@ -320,6 +485,9 @@ router.get('/tabs', async (req, res) => {
|
|
|
320
485
|
router.post('/tabs/:tabId/navigate', async (req, res) => {
|
|
321
486
|
const tabId = req.params.tabId;
|
|
322
487
|
try {
|
|
488
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
489
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
490
|
+
}
|
|
323
491
|
const { userId, url, macro, query } = req.body;
|
|
324
492
|
if (!userId)
|
|
325
493
|
return res.status(400).json({ error: 'userId required' });
|
|
@@ -336,24 +504,30 @@ router.post('/tabs/:tabId/navigate', async (req, res) => {
|
|
|
336
504
|
}
|
|
337
505
|
if (!targetUrl)
|
|
338
506
|
return { status: 400, body: { error: 'url or macro required' } };
|
|
339
|
-
const urlErr = (0, tab_1.
|
|
507
|
+
const urlErr = await (0, tab_1.validateNavigationUrl)(targetUrl, {
|
|
508
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
509
|
+
});
|
|
340
510
|
if (urlErr)
|
|
341
511
|
return { status: 400, body: { error: urlErr } };
|
|
342
|
-
await tabState.page
|
|
512
|
+
await (0, tab_1.navigateWithSafetyGuard)(tabState.page, targetUrl, {
|
|
513
|
+
allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
|
|
514
|
+
waitUntil: 'domcontentloaded',
|
|
515
|
+
timeout: 30000,
|
|
516
|
+
});
|
|
343
517
|
tabState.visitedUrls.add(targetUrl);
|
|
344
518
|
tabState.refs = await (0, tab_1.buildRefs)(tabState.page);
|
|
345
519
|
return { status: 200, body: { ok: true, url: tabState.page.url() } };
|
|
346
520
|
}), CONFIG.handlerTimeoutMs, 'navigate'));
|
|
347
521
|
if (result.status !== 200)
|
|
348
522
|
return res.status(result.status).json(result.body);
|
|
523
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
349
524
|
(0, logging_1.log)('info', 'navigated', { reqId: req.reqId, tabId, url: result.body.url });
|
|
350
525
|
return res.json(result.body);
|
|
351
526
|
}
|
|
352
527
|
catch (err) {
|
|
353
528
|
const message = err instanceof Error ? err.message : String(err);
|
|
354
529
|
(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) });
|
|
530
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
357
531
|
}
|
|
358
532
|
});
|
|
359
533
|
// Snapshot
|
|
@@ -368,12 +542,15 @@ router.get('/tabs/:tabId/snapshot', async (req, res) => {
|
|
|
368
542
|
return res.status(404).json({ error: 'Tab not found' });
|
|
369
543
|
const { tabState } = found;
|
|
370
544
|
tabState.toolCalls++;
|
|
371
|
-
const
|
|
545
|
+
const rawOffset = Number(req.query.offset);
|
|
546
|
+
const offset = Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : 0;
|
|
547
|
+
const raw = await (0, session_1.withUserLimit)(String(userId), CONFIG.maxConcurrentPerUser, () => (0, tab_1.withTimeout)((0, tab_1.snapshotTab)(tabState), CONFIG.handlerTimeoutMs, 'snapshot'));
|
|
548
|
+
const result = (0, tab_1.buildSnapshotPayload)(raw, offset);
|
|
372
549
|
(0, logging_1.log)('info', 'snapshot', {
|
|
373
550
|
reqId: req.reqId,
|
|
374
551
|
tabId,
|
|
375
552
|
url: result.url,
|
|
376
|
-
snapshotLen: result.snapshot
|
|
553
|
+
snapshotLen: result.snapshot.length,
|
|
377
554
|
refsCount: result.refsCount,
|
|
378
555
|
});
|
|
379
556
|
return res.json(result);
|
|
@@ -381,30 +558,37 @@ router.get('/tabs/:tabId/snapshot', async (req, res) => {
|
|
|
381
558
|
catch (err) {
|
|
382
559
|
const message = err instanceof Error ? err.message : String(err);
|
|
383
560
|
(0, logging_1.log)('error', 'snapshot failed', { reqId: req.reqId, tabId: req.params.tabId, error: message });
|
|
384
|
-
return res.status(
|
|
561
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
385
562
|
}
|
|
386
563
|
});
|
|
387
564
|
// Wait for page ready
|
|
388
565
|
router.post('/tabs/:tabId/wait', async (req, res) => {
|
|
389
566
|
try {
|
|
567
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
568
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
569
|
+
}
|
|
390
570
|
const { userId, timeout = 10000, waitForNetwork = true } = req.body;
|
|
391
571
|
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
392
572
|
if (!found)
|
|
393
573
|
return res.status(404).json({ error: 'Tab not found' });
|
|
394
574
|
const { tabState } = found;
|
|
395
575
|
const ready = await (0, tab_1.waitForPageReady)(tabState.page, { timeout, waitForNetwork });
|
|
576
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
396
577
|
return res.json({ ok: true, ready });
|
|
397
578
|
}
|
|
398
579
|
catch (err) {
|
|
399
580
|
const message = err instanceof Error ? err.message : String(err);
|
|
400
581
|
(0, logging_1.log)('error', 'wait failed', { reqId: req.reqId, error: message });
|
|
401
|
-
return res.status(
|
|
582
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
402
583
|
}
|
|
403
584
|
});
|
|
404
585
|
// Click
|
|
405
586
|
router.post('/tabs/:tabId/click', async (req, res) => {
|
|
406
587
|
const tabId = req.params.tabId;
|
|
407
588
|
try {
|
|
589
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
590
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
591
|
+
}
|
|
408
592
|
const { userId, ref, selector } = req.body;
|
|
409
593
|
if (!userId)
|
|
410
594
|
return res.status(400).json({ error: 'userId required' });
|
|
@@ -425,21 +609,22 @@ router.post('/tabs/:tabId/click', async (req, res) => {
|
|
|
425
609
|
}));
|
|
426
610
|
}
|
|
427
611
|
(0, logging_1.log)('info', 'clicked', { reqId: req.reqId, tabId, url: result.url });
|
|
612
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
428
613
|
return res.json(responseObj);
|
|
429
614
|
}
|
|
430
615
|
catch (err) {
|
|
431
|
-
const statusCode = err?.statusCode;
|
|
432
616
|
const message = err instanceof Error ? err.message : String(err);
|
|
433
617
|
(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) });
|
|
618
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
437
619
|
}
|
|
438
620
|
});
|
|
439
621
|
// Type
|
|
440
622
|
router.post('/tabs/:tabId/type', async (req, res) => {
|
|
441
623
|
const tabId = req.params.tabId;
|
|
442
624
|
try {
|
|
625
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
626
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
627
|
+
}
|
|
443
628
|
const { userId, ref, selector, text } = req.body;
|
|
444
629
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
445
630
|
if (!found)
|
|
@@ -448,21 +633,22 @@ router.post('/tabs/:tabId/type', async (req, res) => {
|
|
|
448
633
|
tabState.toolCalls++;
|
|
449
634
|
const textValue = String(text ?? '');
|
|
450
635
|
const result = await (0, tab_1.withTimeout)((0, tab_1.typeTab)(tabId, tabState, { ref, selector, text: textValue }), (0, tab_1.calculateTypeTimeoutMs)(textValue), 'type');
|
|
636
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
451
637
|
return res.json(result);
|
|
452
638
|
}
|
|
453
639
|
catch (err) {
|
|
454
|
-
const statusCode = err?.statusCode;
|
|
455
640
|
const message = err instanceof Error ? err.message : String(err);
|
|
456
641
|
(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) });
|
|
642
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
460
643
|
}
|
|
461
644
|
});
|
|
462
645
|
// Press key
|
|
463
646
|
router.post('/tabs/:tabId/press', async (req, res) => {
|
|
464
647
|
const tabId = req.params.tabId;
|
|
465
648
|
try {
|
|
649
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
650
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
651
|
+
}
|
|
466
652
|
const { userId, key } = req.body;
|
|
467
653
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
468
654
|
if (!found)
|
|
@@ -470,38 +656,44 @@ router.post('/tabs/:tabId/press', async (req, res) => {
|
|
|
470
656
|
const { tabState } = found;
|
|
471
657
|
tabState.toolCalls++;
|
|
472
658
|
await (0, tab_1.withTimeout)((0, tab_1.pressTab)(tabId, tabState, String(key ?? '')), CONFIG.handlerTimeoutMs, 'press');
|
|
659
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
473
660
|
return res.json({ ok: true });
|
|
474
661
|
}
|
|
475
662
|
catch (err) {
|
|
476
663
|
const message = err instanceof Error ? err.message : String(err);
|
|
477
664
|
(0, logging_1.log)('error', 'press failed', { reqId: req.reqId, error: message });
|
|
478
|
-
return res.status(
|
|
665
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
479
666
|
}
|
|
480
667
|
});
|
|
481
668
|
// Scroll
|
|
482
669
|
router.post('/tabs/:tabId/scroll', async (req, res) => {
|
|
483
670
|
try {
|
|
671
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
672
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
673
|
+
}
|
|
484
674
|
const { userId, direction = 'down', amount = 500 } = req.body;
|
|
485
675
|
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
486
676
|
if (!found)
|
|
487
677
|
return res.status(404).json({ error: 'Tab not found' });
|
|
488
678
|
const { tabState } = found;
|
|
489
679
|
tabState.toolCalls++;
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return res.json({ ok: true });
|
|
680
|
+
const result = await (0, tab_1.withTimeout)((0, tab_1.scrollTab)(tabState, { direction, amount }), CONFIG.handlerTimeoutMs, 'scroll');
|
|
681
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
682
|
+
return res.json(result);
|
|
494
683
|
}
|
|
495
684
|
catch (err) {
|
|
496
685
|
const message = err instanceof Error ? err.message : String(err);
|
|
497
686
|
(0, logging_1.log)('error', 'scroll failed', { reqId: req.reqId, error: message });
|
|
498
|
-
return res.status(
|
|
687
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
499
688
|
}
|
|
500
689
|
});
|
|
501
690
|
// Scroll element (selector or ref)
|
|
502
691
|
router.post('/tabs/:tabId/scroll-element', async (req, res) => {
|
|
503
692
|
const tabId = req.params.tabId;
|
|
504
693
|
try {
|
|
694
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
695
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
696
|
+
}
|
|
505
697
|
const { userId, selector, ref, deltaX, deltaY, scrollTo } = req.body;
|
|
506
698
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
507
699
|
if (!found)
|
|
@@ -509,15 +701,13 @@ router.post('/tabs/:tabId/scroll-element', async (req, res) => {
|
|
|
509
701
|
const { tabState } = found;
|
|
510
702
|
tabState.toolCalls++;
|
|
511
703
|
const result = await (0, tab_1.scrollElementTab)(tabId, tabState, { selector, ref, deltaX, deltaY, scrollTo });
|
|
704
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
512
705
|
return res.json(result);
|
|
513
706
|
}
|
|
514
707
|
catch (err) {
|
|
515
|
-
const statusCode = err?.statusCode;
|
|
516
708
|
const message = err instanceof Error ? err.message : String(err);
|
|
517
709
|
(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) });
|
|
710
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
521
711
|
}
|
|
522
712
|
});
|
|
523
713
|
// Evaluate JS (API key optional)
|
|
@@ -540,12 +730,13 @@ router.post('/tabs/:tabId/evaluate', express_1.default.json({ limit: '64kb' }),
|
|
|
540
730
|
const { tabState } = found;
|
|
541
731
|
tabState.toolCalls++;
|
|
542
732
|
const result = await (0, tab_1.evaluateTab)(tabId, tabState, { expression, timeout });
|
|
733
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
543
734
|
return res.json(result);
|
|
544
735
|
}
|
|
545
736
|
catch (err) {
|
|
546
737
|
const message = err instanceof Error ? err.message : String(err);
|
|
547
738
|
(0, logging_1.log)('error', 'evaluate failed', { reqId: req.reqId, tabId, error: message });
|
|
548
|
-
return res.status(
|
|
739
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
549
740
|
}
|
|
550
741
|
});
|
|
551
742
|
// Evaluate JS extended (API key optional)
|
|
@@ -586,6 +777,7 @@ router.post('/tabs/:tabId/evaluate-extended', express_1.default.json({ limit: '6
|
|
|
586
777
|
const outerTimeout = effectiveTimeout + 10000;
|
|
587
778
|
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
779
|
if (result.ok) {
|
|
780
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
589
781
|
return res.json(result);
|
|
590
782
|
}
|
|
591
783
|
if (result.errorType === 'timeout') {
|
|
@@ -596,6 +788,10 @@ router.post('/tabs/:tabId/evaluate-extended', express_1.default.json({ limit: '6
|
|
|
596
788
|
catch (err) {
|
|
597
789
|
const message = err instanceof Error ? err.message : String(err);
|
|
598
790
|
(0, logging_1.log)('error', 'evaluate-extended failed', { reqId: req.reqId, tabId, error: message });
|
|
791
|
+
const status = getRouteErrorStatus(err);
|
|
792
|
+
if (status === 400) {
|
|
793
|
+
return res.status(400).json({ ok: false, error: (0, errors_1.safeError)(err), errorType: 'js_error' });
|
|
794
|
+
}
|
|
599
795
|
if (message.includes('timed out') || message.includes('Timeout')) {
|
|
600
796
|
return res.status(408).json({ ok: false, error: message, errorType: 'timeout' });
|
|
601
797
|
}
|
|
@@ -611,6 +807,9 @@ router.post('/tabs/:tabId/evaluate-extended', express_1.default.json({ limit: '6
|
|
|
611
807
|
router.post('/tabs/:tabId/back', async (req, res) => {
|
|
612
808
|
const tabId = req.params.tabId;
|
|
613
809
|
try {
|
|
810
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
811
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
812
|
+
}
|
|
614
813
|
const { userId } = req.body;
|
|
615
814
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
616
815
|
if (!found)
|
|
@@ -618,18 +817,22 @@ router.post('/tabs/:tabId/back', async (req, res) => {
|
|
|
618
817
|
const { tabState } = found;
|
|
619
818
|
tabState.toolCalls++;
|
|
620
819
|
const result = await (0, tab_1.withTimeout)((0, tab_1.backTab)(tabId, tabState), CONFIG.handlerTimeoutMs, 'back');
|
|
820
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
621
821
|
return res.json(result);
|
|
622
822
|
}
|
|
623
823
|
catch (err) {
|
|
624
824
|
const message = err instanceof Error ? err.message : String(err);
|
|
625
825
|
(0, logging_1.log)('error', 'back failed', { reqId: req.reqId, error: message });
|
|
626
|
-
return res.status(
|
|
826
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
627
827
|
}
|
|
628
828
|
});
|
|
629
829
|
// Forward
|
|
630
830
|
router.post('/tabs/:tabId/forward', async (req, res) => {
|
|
631
831
|
const tabId = req.params.tabId;
|
|
632
832
|
try {
|
|
833
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
834
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
835
|
+
}
|
|
633
836
|
const { userId } = req.body;
|
|
634
837
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
635
838
|
if (!found)
|
|
@@ -637,18 +840,22 @@ router.post('/tabs/:tabId/forward', async (req, res) => {
|
|
|
637
840
|
const { tabState } = found;
|
|
638
841
|
tabState.toolCalls++;
|
|
639
842
|
const result = await (0, tab_1.withTimeout)((0, tab_1.forwardTab)(tabId, tabState), CONFIG.handlerTimeoutMs, 'forward');
|
|
843
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
640
844
|
return res.json(result);
|
|
641
845
|
}
|
|
642
846
|
catch (err) {
|
|
643
847
|
const message = err instanceof Error ? err.message : String(err);
|
|
644
848
|
(0, logging_1.log)('error', 'forward failed', { reqId: req.reqId, error: message });
|
|
645
|
-
return res.status(
|
|
849
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
646
850
|
}
|
|
647
851
|
});
|
|
648
852
|
// Refresh
|
|
649
853
|
router.post('/tabs/:tabId/refresh', async (req, res) => {
|
|
650
854
|
const tabId = req.params.tabId;
|
|
651
855
|
try {
|
|
856
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
857
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
858
|
+
}
|
|
652
859
|
const { userId } = req.body;
|
|
653
860
|
const found = (0, session_1.findTabById)(tabId, userId);
|
|
654
861
|
if (!found)
|
|
@@ -656,12 +863,13 @@ router.post('/tabs/:tabId/refresh', async (req, res) => {
|
|
|
656
863
|
const { tabState } = found;
|
|
657
864
|
tabState.toolCalls++;
|
|
658
865
|
const result = await (0, tab_1.withTimeout)((0, tab_1.refreshTab)(tabId, tabState), CONFIG.handlerTimeoutMs, 'refresh');
|
|
866
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
659
867
|
return res.json(result);
|
|
660
868
|
}
|
|
661
869
|
catch (err) {
|
|
662
870
|
const message = err instanceof Error ? err.message : String(err);
|
|
663
871
|
(0, logging_1.log)('error', 'refresh failed', { reqId: req.reqId, error: message });
|
|
664
|
-
return res.status(
|
|
872
|
+
return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
|
|
665
873
|
}
|
|
666
874
|
});
|
|
667
875
|
// Get links
|
|
@@ -708,6 +916,54 @@ router.get('/tabs/:tabId/screenshot', async (req, res) => {
|
|
|
708
916
|
return res.status(500).json({ error: (0, errors_1.safeError)(err) });
|
|
709
917
|
}
|
|
710
918
|
});
|
|
919
|
+
// Images
|
|
920
|
+
router.get('/tabs/:tabId/images', async (req, res) => {
|
|
921
|
+
try {
|
|
922
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
923
|
+
return res.status(403).json({ ok: false, error: 'Forbidden' });
|
|
924
|
+
}
|
|
925
|
+
const userId = req.query.userId;
|
|
926
|
+
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
927
|
+
if (!found) {
|
|
928
|
+
(0, logging_1.log)('warn', 'images: tab not found', { reqId: req.reqId, tabId: req.params.tabId, userId });
|
|
929
|
+
return res.status(404).json({ error: 'Tab not found' });
|
|
930
|
+
}
|
|
931
|
+
const selector = typeof req.query.selector === 'string' && req.query.selector ? req.query.selector : undefined;
|
|
932
|
+
const extensions = Array.isArray(req.query.extensions)
|
|
933
|
+
? req.query.extensions.map((value) => String(value)).filter(Boolean)
|
|
934
|
+
: typeof req.query.extensions === 'string' && req.query.extensions
|
|
935
|
+
? req.query.extensions
|
|
936
|
+
.split(',')
|
|
937
|
+
.map((value) => value.trim())
|
|
938
|
+
.filter(Boolean)
|
|
939
|
+
: undefined;
|
|
940
|
+
const resolveBlobs = String(req.query.resolveBlobs || '').toLowerCase() === 'true';
|
|
941
|
+
const triggerLazyLoad = String(req.query.triggerLazyLoad || '').toLowerCase() === 'true';
|
|
942
|
+
const { tabState } = found;
|
|
943
|
+
tabState.toolCalls++;
|
|
944
|
+
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, {
|
|
945
|
+
selector,
|
|
946
|
+
extensions,
|
|
947
|
+
resolveBlobs,
|
|
948
|
+
triggerLazyLoad,
|
|
949
|
+
})), CONFIG.handlerTimeoutMs, 'images'));
|
|
950
|
+
return res.json({
|
|
951
|
+
ok: result.ok,
|
|
952
|
+
container: result.container,
|
|
953
|
+
images: result.resources.images,
|
|
954
|
+
totals: {
|
|
955
|
+
images: result.totals.images,
|
|
956
|
+
total: result.totals.images,
|
|
957
|
+
},
|
|
958
|
+
metadata: result.metadata,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
catch (err) {
|
|
962
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
963
|
+
(0, logging_1.log)('error', 'images failed', { reqId: req.reqId, error: message });
|
|
964
|
+
return res.status(500).json({ error: (0, errors_1.safeError)(err) });
|
|
965
|
+
}
|
|
966
|
+
});
|
|
711
967
|
// Stats
|
|
712
968
|
router.get('/tabs/:tabId/stats', async (req, res) => {
|
|
713
969
|
try {
|
|
@@ -735,6 +991,9 @@ router.get('/tabs/:tabId/stats', async (req, res) => {
|
|
|
735
991
|
// Close tab
|
|
736
992
|
router.delete('/tabs/:tabId', async (req, res) => {
|
|
737
993
|
try {
|
|
994
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
995
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
996
|
+
}
|
|
738
997
|
const { userId } = req.body;
|
|
739
998
|
const found = (0, session_1.findTabById)(req.params.tabId, userId);
|
|
740
999
|
if (found) {
|
|
@@ -746,6 +1005,7 @@ router.delete('/tabs/:tabId', async (req, res) => {
|
|
|
746
1005
|
}
|
|
747
1006
|
(0, logging_1.log)('info', 'tab closed', { reqId: req.reqId, tabId: req.params.tabId, userId });
|
|
748
1007
|
}
|
|
1008
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
749
1009
|
return res.json({ ok: true });
|
|
750
1010
|
}
|
|
751
1011
|
catch (err) {
|
|
@@ -757,6 +1017,9 @@ router.delete('/tabs/:tabId', async (req, res) => {
|
|
|
757
1017
|
// Close tab group
|
|
758
1018
|
router.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
759
1019
|
try {
|
|
1020
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1021
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1022
|
+
}
|
|
760
1023
|
const { userId } = req.body;
|
|
761
1024
|
const sessionsForUser = (0, session_1.getSessionsForUser)(userId);
|
|
762
1025
|
for (const [sessionKey, session] of sessionsForUser) {
|
|
@@ -775,6 +1038,7 @@ router.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
|
775
1038
|
sessionKey,
|
|
776
1039
|
});
|
|
777
1040
|
}
|
|
1041
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
778
1042
|
return res.json({ ok: true });
|
|
779
1043
|
}
|
|
780
1044
|
catch (err) {
|
|
@@ -786,10 +1050,14 @@ router.delete('/tabs/group/:listItemId', async (req, res) => {
|
|
|
786
1050
|
// Close session
|
|
787
1051
|
router.delete('/sessions/:userId', async (req, res) => {
|
|
788
1052
|
try {
|
|
1053
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1054
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1055
|
+
}
|
|
789
1056
|
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
790
1057
|
await (0, session_1.closeSessionsForUser)(userId);
|
|
791
1058
|
// Ensure downloads are cleaned even if the session was already partially removed.
|
|
792
1059
|
(0, download_1.cleanupUserDownloads)(userId);
|
|
1060
|
+
lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
|
|
793
1061
|
return res.json({ ok: true });
|
|
794
1062
|
}
|
|
795
1063
|
catch (err) {
|
|
@@ -801,6 +1069,9 @@ router.delete('/sessions/:userId', async (req, res) => {
|
|
|
801
1069
|
// Toggle display mode (headless/headed/virtual) for a user session
|
|
802
1070
|
router.post('/sessions/:userId/toggle-display', async (req, res) => {
|
|
803
1071
|
try {
|
|
1072
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1073
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1074
|
+
}
|
|
804
1075
|
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
805
1076
|
const { headless } = req.body ?? {};
|
|
806
1077
|
if (typeof headless !== 'boolean' && headless !== 'virtual') {
|
|
@@ -952,6 +1223,9 @@ router.get('/downloads/:downloadId/content', async (req, res) => {
|
|
|
952
1223
|
// Downloads: delete
|
|
953
1224
|
router.delete('/downloads/:downloadId', async (req, res) => {
|
|
954
1225
|
try {
|
|
1226
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1227
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1228
|
+
}
|
|
955
1229
|
const userId = req.body?.userId || req.query.userId;
|
|
956
1230
|
if (!userId)
|
|
957
1231
|
return res.status(400).json({ ok: false, error: 'userId required' });
|
|
@@ -969,6 +1243,9 @@ router.delete('/downloads/:downloadId', async (req, res) => {
|
|
|
969
1243
|
// Extract resources from a scoped container
|
|
970
1244
|
router.post('/tabs/:tabId/extract-resources', async (req, res) => {
|
|
971
1245
|
try {
|
|
1246
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1247
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1248
|
+
}
|
|
972
1249
|
const { tabId } = req.params;
|
|
973
1250
|
const { userId, selector, types, extensions, resolveBlobs, triggerLazyLoad } = req.body;
|
|
974
1251
|
if (!userId)
|
|
@@ -994,9 +1271,50 @@ router.post('/tabs/:tabId/extract-resources', async (req, res) => {
|
|
|
994
1271
|
res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
995
1272
|
}
|
|
996
1273
|
});
|
|
1274
|
+
router.post('/tabs/:tabId/extract-structured', async (req, res) => {
|
|
1275
|
+
try {
|
|
1276
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1277
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1278
|
+
}
|
|
1279
|
+
const { tabId } = req.params;
|
|
1280
|
+
const { userId, schema } = req.body;
|
|
1281
|
+
if (!userId || typeof userId !== 'string') {
|
|
1282
|
+
return res.status(400).json({ ok: false, error: 'userId required' });
|
|
1283
|
+
}
|
|
1284
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
1285
|
+
return res.status(400).json({ ok: false, error: 'schema must be an object' });
|
|
1286
|
+
}
|
|
1287
|
+
const found = (0, session_1.findTabById)(tabId, userId);
|
|
1288
|
+
if (!found)
|
|
1289
|
+
return res.status(404).json({ ok: false, error: 'Tab not found' });
|
|
1290
|
+
const { tabState } = found;
|
|
1291
|
+
tabState.toolCalls++;
|
|
1292
|
+
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'));
|
|
1293
|
+
return res.json(result);
|
|
1294
|
+
}
|
|
1295
|
+
catch (err) {
|
|
1296
|
+
if (err instanceof structured_extractor_1.StructuredExtractSchemaError) {
|
|
1297
|
+
return res.status(err.statusCode).json({ ok: false, error: err.message });
|
|
1298
|
+
}
|
|
1299
|
+
if (err instanceof structured_extractor_1.StructuredExtractRuntimeError) {
|
|
1300
|
+
return res.status(err.statusCode).json({
|
|
1301
|
+
ok: false,
|
|
1302
|
+
error: 'Structured extraction failed',
|
|
1303
|
+
fieldPath: err.fieldPath,
|
|
1304
|
+
reason: err.reason,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1308
|
+
(0, logging_1.log)('error', 'structured extract failed', { error: message });
|
|
1309
|
+
return res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
997
1312
|
// Batch download from a scoped container
|
|
998
1313
|
router.post('/tabs/:tabId/batch-download', async (req, res) => {
|
|
999
1314
|
try {
|
|
1315
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1316
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1317
|
+
}
|
|
1000
1318
|
const { tabId } = req.params;
|
|
1001
1319
|
const body = req.body;
|
|
1002
1320
|
const userId = body?.userId;
|
|
@@ -1093,12 +1411,13 @@ router.post('/tabs/:tabId/trace/stop', async (req, res) => {
|
|
|
1093
1411
|
return res.json({
|
|
1094
1412
|
ok: true,
|
|
1095
1413
|
path: result.path,
|
|
1414
|
+
filename: result.path ? (0, node_path_1.basename)(result.path) : undefined,
|
|
1096
1415
|
size: result.size,
|
|
1097
1416
|
alreadyStopped: true,
|
|
1098
1417
|
message: 'Trace was already stopped by chunk stop',
|
|
1099
1418
|
});
|
|
1100
1419
|
}
|
|
1101
|
-
return res.json({ ok: true, ...result });
|
|
1420
|
+
return res.json({ ok: true, ...result, filename: (0, node_path_1.basename)(result.path) });
|
|
1102
1421
|
}
|
|
1103
1422
|
catch (err) {
|
|
1104
1423
|
const status = getTracingErrorStatus(err);
|
|
@@ -1143,7 +1462,7 @@ router.post('/tabs/:tabId/trace/chunk/stop', async (req, res) => {
|
|
|
1143
1462
|
if (!tab)
|
|
1144
1463
|
return res.status(404).json({ ok: false, error: 'Tab not found' });
|
|
1145
1464
|
const result = await (0, tracing_1.stopTracingChunk)(userId, tab.page.context(), path);
|
|
1146
|
-
return res.json({ ok: true, ...result });
|
|
1465
|
+
return res.json({ ok: true, ...result, filename: (0, node_path_1.basename)(result.path) });
|
|
1147
1466
|
}
|
|
1148
1467
|
catch (err) {
|
|
1149
1468
|
const status = getTracingErrorStatus(err);
|
|
@@ -1170,6 +1489,62 @@ router.get('/tabs/:tabId/trace/status', async (req, res) => {
|
|
|
1170
1489
|
return res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1171
1490
|
}
|
|
1172
1491
|
});
|
|
1492
|
+
router.get('/sessions/:userId/traces', async (req, res) => {
|
|
1493
|
+
try {
|
|
1494
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1495
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1496
|
+
}
|
|
1497
|
+
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
1498
|
+
return res.json({ ok: true, traces: (0, tracing_1.listTraceArtifacts)(userId) });
|
|
1499
|
+
}
|
|
1500
|
+
catch (err) {
|
|
1501
|
+
return res.status(500).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
router.get('/sessions/:userId/traces/:filename', async (req, res) => {
|
|
1505
|
+
try {
|
|
1506
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1507
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1508
|
+
}
|
|
1509
|
+
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
1510
|
+
const filename = req.params.filename;
|
|
1511
|
+
const filePath = (0, tracing_1.resolveTraceArtifactPath)(userId, filename);
|
|
1512
|
+
const safeName = filename.replace(/[\r\n\0"]/g, '');
|
|
1513
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
1514
|
+
res.setHeader('Content-Disposition', `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(safeName)}`);
|
|
1515
|
+
const stream = node_fs_1.default.createReadStream(filePath);
|
|
1516
|
+
stream.on('error', (streamErr) => {
|
|
1517
|
+
if (!res.headersSent) {
|
|
1518
|
+
const status = getTraceArtifactErrorStatus(streamErr);
|
|
1519
|
+
res.removeHeader('Content-Disposition');
|
|
1520
|
+
res.removeHeader('Content-Type');
|
|
1521
|
+
res.status(status).json({ ok: false, error: (0, errors_1.safeError)(streamErr) });
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
res.destroy();
|
|
1525
|
+
});
|
|
1526
|
+
stream.pipe(res);
|
|
1527
|
+
}
|
|
1528
|
+
catch (err) {
|
|
1529
|
+
const status = getTraceArtifactErrorStatus(err);
|
|
1530
|
+
return res.status(status).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
router.delete('/sessions/:userId/traces/:filename', async (req, res) => {
|
|
1534
|
+
try {
|
|
1535
|
+
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|
|
1536
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
1537
|
+
}
|
|
1538
|
+
const userId = (0, session_1.normalizeUserId)(req.params.userId);
|
|
1539
|
+
const filename = req.params.filename;
|
|
1540
|
+
(0, tracing_1.deleteTraceArtifact)(userId, filename);
|
|
1541
|
+
return res.json({ ok: true });
|
|
1542
|
+
}
|
|
1543
|
+
catch (err) {
|
|
1544
|
+
const status = getTraceArtifactErrorStatus(err);
|
|
1545
|
+
return res.status(status).json({ ok: false, error: (0, errors_1.safeError)(err) });
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1173
1548
|
router.get('/tabs/:tabId/console', async (req, res) => {
|
|
1174
1549
|
try {
|
|
1175
1550
|
if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
|