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.
Files changed (102) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +310 -34
  3. package/dist/src/cli/commands/content.d.ts.map +1 -1
  4. package/dist/src/cli/commands/content.js +37 -0
  5. package/dist/src/cli/commands/content.js.map +1 -1
  6. package/dist/src/cli/commands/core.d.ts.map +1 -1
  7. package/dist/src/cli/commands/core.js +21 -4
  8. package/dist/src/cli/commands/core.js.map +1 -1
  9. package/dist/src/cli/commands/interaction.d.ts.map +1 -1
  10. package/dist/src/cli/commands/interaction.js +5 -14
  11. package/dist/src/cli/commands/interaction.js.map +1 -1
  12. package/dist/src/cli/commands/navigation.d.ts.map +1 -1
  13. package/dist/src/cli/commands/navigation.js +12 -6
  14. package/dist/src/cli/commands/navigation.js.map +1 -1
  15. package/dist/src/cli/commands/server.d.ts.map +1 -1
  16. package/dist/src/cli/commands/server.js +9 -3
  17. package/dist/src/cli/commands/server.js.map +1 -1
  18. package/dist/src/cli/commands/session.d.ts.map +1 -1
  19. package/dist/src/cli/commands/session.js +23 -5
  20. package/dist/src/cli/commands/session.js.map +1 -1
  21. package/dist/src/cli/server/manager.d.ts +1 -0
  22. package/dist/src/cli/server/manager.d.ts.map +1 -1
  23. package/dist/src/cli/server/manager.js +7 -12
  24. package/dist/src/cli/server/manager.js.map +1 -1
  25. package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
  26. package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
  27. package/dist/src/middleware/lifecycle-activity.js +21 -0
  28. package/dist/src/middleware/lifecycle-activity.js.map +1 -0
  29. package/dist/src/openapi/spec.d.ts +4 -0
  30. package/dist/src/openapi/spec.d.ts.map +1 -0
  31. package/dist/src/openapi/spec.js +730 -0
  32. package/dist/src/openapi/spec.js.map +1 -0
  33. package/dist/src/routes/core.d.ts.map +1 -1
  34. package/dist/src/routes/core.js +545 -58
  35. package/dist/src/routes/core.js.map +1 -1
  36. package/dist/src/routes/docs.d.ts +3 -0
  37. package/dist/src/routes/docs.d.ts.map +1 -0
  38. package/dist/src/routes/docs.js +23 -0
  39. package/dist/src/routes/docs.js.map +1 -0
  40. package/dist/src/routes/openclaw.d.ts.map +1 -1
  41. package/dist/src/routes/openclaw.js +317 -90
  42. package/dist/src/routes/openclaw.js.map +1 -1
  43. package/dist/src/server.js +55 -4
  44. package/dist/src/server.js.map +1 -1
  45. package/dist/src/services/context-pool.d.ts +21 -4
  46. package/dist/src/services/context-pool.d.ts.map +1 -1
  47. package/dist/src/services/context-pool.js +290 -71
  48. package/dist/src/services/context-pool.js.map +1 -1
  49. package/dist/src/services/download.d.ts +2 -0
  50. package/dist/src/services/download.d.ts.map +1 -1
  51. package/dist/src/services/download.js +110 -80
  52. package/dist/src/services/download.js.map +1 -1
  53. package/dist/src/services/lifecycle-controller.d.ts +40 -0
  54. package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
  55. package/dist/src/services/lifecycle-controller.js +106 -0
  56. package/dist/src/services/lifecycle-controller.js.map +1 -0
  57. package/dist/src/services/resource-extractor.d.ts +1 -0
  58. package/dist/src/services/resource-extractor.d.ts.map +1 -1
  59. package/dist/src/services/resource-extractor.js +7 -0
  60. package/dist/src/services/resource-extractor.js.map +1 -1
  61. package/dist/src/services/session.d.ts +109 -4
  62. package/dist/src/services/session.d.ts.map +1 -1
  63. package/dist/src/services/session.js +622 -64
  64. package/dist/src/services/session.js.map +1 -1
  65. package/dist/src/services/structured-extractor.d.ts +39 -0
  66. package/dist/src/services/structured-extractor.d.ts.map +1 -0
  67. package/dist/src/services/structured-extractor.js +487 -0
  68. package/dist/src/services/structured-extractor.js.map +1 -0
  69. package/dist/src/services/tab.d.ts +30 -3
  70. package/dist/src/services/tab.d.ts.map +1 -1
  71. package/dist/src/services/tab.js +872 -124
  72. package/dist/src/services/tab.js.map +1 -1
  73. package/dist/src/services/tracing.d.ts +7 -0
  74. package/dist/src/services/tracing.d.ts.map +1 -1
  75. package/dist/src/services/tracing.js +200 -19
  76. package/dist/src/services/tracing.js.map +1 -1
  77. package/dist/src/services/vnc.d.ts.map +1 -1
  78. package/dist/src/services/vnc.js +5 -3
  79. package/dist/src/services/vnc.js.map +1 -1
  80. package/dist/src/services/youtube.js +1 -1
  81. package/dist/src/services/youtube.js.map +1 -1
  82. package/dist/src/types.d.ts +71 -1
  83. package/dist/src/types.d.ts.map +1 -1
  84. package/dist/src/utils/config.d.ts +79 -3
  85. package/dist/src/utils/config.d.ts.map +1 -1
  86. package/dist/src/utils/config.js +145 -3
  87. package/dist/src/utils/config.js.map +1 -1
  88. package/dist/src/utils/presets.d.ts.map +1 -1
  89. package/dist/src/utils/presets.js +3 -1
  90. package/dist/src/utils/presets.js.map +1 -1
  91. package/dist/src/utils/proxy-profiles.d.ts +18 -0
  92. package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
  93. package/dist/src/utils/proxy-profiles.js +197 -0
  94. package/dist/src/utils/proxy-profiles.js.map +1 -0
  95. package/dist/src/utils/sidecar-version.d.ts +12 -0
  96. package/dist/src/utils/sidecar-version.d.ts.map +1 -0
  97. package/dist/src/utils/sidecar-version.js +63 -0
  98. package/dist/src/utils/sidecar-version.js.map +1 -0
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/openclaw.plugin.json +39 -0
  101. package/package.json +16 -4
  102. package/plugin.ts +949 -0
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -10,9 +43,12 @@ const express_1 = require("express");
10
43
  const errors_1 = require("../middleware/errors");
11
44
  const logging_1 = require("../middleware/logging");
12
45
  const auth_1 = require("../middleware/auth");
46
+ const structured_extractor_1 = require("../services/structured-extractor");
13
47
  const config_1 = require("../utils/config");
14
48
  const browser_1 = require("../services/browser");
15
49
  const context_pool_1 = require("../services/context-pool");
50
+ const lifecycle_controller_1 = require("../services/lifecycle-controller");
51
+ const download_1 = require("../services/download");
16
52
  const session_1 = require("../services/session");
17
53
  const tab_1 = require("../services/tab");
18
54
  const CONFIG = (0, config_1.loadConfig)();
@@ -26,6 +62,15 @@ const PKG_VERSION = (() => {
26
62
  return pkg.version;
27
63
  })();
28
64
  const router = (0, express_1.Router)();
65
+ function getRouteErrorStatus(err) {
66
+ if (typeof err === 'object' && err !== null && 'statusCode' in err && typeof err.statusCode === 'number') {
67
+ return err.statusCode;
68
+ }
69
+ if (typeof err === 'object' && err !== null && 'status' in err && typeof err.status === 'number') {
70
+ return err.status;
71
+ }
72
+ return 500;
73
+ }
29
74
  function isLoadState(value) {
30
75
  return value === 'load' || value === 'domcontentloaded' || value === 'networkidle';
31
76
  }
@@ -61,32 +106,137 @@ router.get('/', async (_req, res) => {
61
106
  });
62
107
  // POST /tabs/open - Open tab (alias for POST /tabs, OpenClaw format)
63
108
  router.post('/tabs/open', async (req, res) => {
109
+ let profileUserId;
110
+ let profileSessionKey;
111
+ let createdSessionProfile = false;
112
+ let createdSessionProfileSignature;
113
+ let createdDefaultSessionProfileClaim = false;
114
+ let releaseSessionProfileCreate;
64
115
  try {
65
- const { url, userId, listItemId = 'default' } = req.body;
116
+ if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
117
+ return res.status(403).json({ error: 'Forbidden' });
118
+ }
119
+ const { url, userId, listItemId = 'default', proxyProfile, proxy, geoMode } = req.body;
66
120
  if (!userId) {
67
121
  return res.status(400).json({ error: 'userId is required' });
68
122
  }
69
123
  if (!url) {
70
124
  return res.status(400).json({ error: 'url is required' });
71
125
  }
72
- const urlErr = (0, tab_1.validateUrl)(url);
126
+ profileUserId = userId;
127
+ profileSessionKey = listItemId;
128
+ // Establish/check session profile if proxy/geo fields provided
129
+ let resolvedSessionProfile;
130
+ if (proxyProfile || proxy || geoMode) {
131
+ const { resolveSessionProfileInput, getConfiguredServerProxy, loadProxyProfiles } = await Promise.resolve().then(() => __importStar(require('../utils/proxy-profiles')));
132
+ const profileInput = {
133
+ proxy: proxy,
134
+ proxyProfile,
135
+ geoMode: geoMode,
136
+ };
137
+ const deps = {
138
+ serverProxy: getConfiguredServerProxy(CONFIG.proxy),
139
+ proxyProfiles: loadProxyProfiles(CONFIG.proxyProfilesFile),
140
+ };
141
+ try {
142
+ const resolvedProfileBase = resolveSessionProfileInput(profileInput, deps);
143
+ resolvedSessionProfile = { ...resolvedProfileBase, sessionKey: listItemId };
144
+ }
145
+ catch (err) {
146
+ const message = err instanceof Error ? err.message : String(err);
147
+ return res.status(400).json({ error: message });
148
+ }
149
+ }
150
+ const urlErr = await (0, tab_1.validateNavigationUrl)(url, {
151
+ allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
152
+ });
73
153
  if (urlErr)
74
154
  return res.status(400).json({ error: urlErr });
75
- const sessionMapKey = (0, session_1.getSessionMapKey)(userId, null);
76
- const session = await (0, session_1.getSession)(userId);
155
+ const canonical = (0, session_1.getCanonicalProfile)(userId);
156
+ if (!canonical) {
157
+ (0, logging_1.log)('warn', 'openclaw tab open rejected: no canonical profile', { userId: String(userId) });
158
+ return res.status(409).json({
159
+ error: 'No canonical profile',
160
+ message: 'Cannot open tabs via this endpoint without an established canonical profile. Use core POST /tabs first.',
161
+ });
162
+ }
163
+ if (resolvedSessionProfile) {
164
+ while (true) {
165
+ await (0, session_1.waitForSessionProfileCreate)(userId, listItemId);
166
+ const existingProfile = (0, session_1.getEstablishedSessionProfile)(userId, listItemId);
167
+ if (existingProfile) {
168
+ if (existingProfile.signature !== resolvedSessionProfile.signature) {
169
+ return res.status(409).json({ error: 'Session profile conflict' });
170
+ }
171
+ break;
172
+ }
173
+ if ((0, session_1.hasDefaultSessionProfileRuntime)(userId, listItemId)) {
174
+ return res.status(409).json({ error: 'Session profile conflict' });
175
+ }
176
+ const mutex = (0, session_1.acquireSessionProfileCreateMutex)(userId, listItemId, resolvedSessionProfile.signature);
177
+ if (mutex.acquired) {
178
+ releaseSessionProfileCreate = mutex.release;
179
+ try {
180
+ (0, session_1.establishSessionProfile)(userId, listItemId, resolvedSessionProfile);
181
+ createdSessionProfile = true;
182
+ createdSessionProfileSignature = resolvedSessionProfile.signature;
183
+ }
184
+ catch (err) {
185
+ releaseSessionProfileCreate(false);
186
+ releaseSessionProfileCreate = undefined;
187
+ const message = err instanceof Error ? err.message : String(err);
188
+ if (message === 'Session profile conflict') {
189
+ return res.status(409).json({ error: 'Session profile conflict' });
190
+ }
191
+ return res.status(400).json({ error: message });
192
+ }
193
+ break;
194
+ }
195
+ await mutex.wait;
196
+ }
197
+ }
198
+ else {
199
+ await (0, session_1.waitForSessionProfileCreate)(userId, listItemId);
200
+ if (!(0, session_1.getEstablishedSessionProfile)(userId, listItemId)) {
201
+ createdDefaultSessionProfileClaim = (0, session_1.claimDefaultSessionProfileRuntime)(userId, listItemId);
202
+ }
203
+ }
204
+ const establishedProfile = (0, session_1.getEstablishedSessionProfile)(userId, listItemId);
205
+ const sessionMapKey = establishedProfile
206
+ ? (0, session_1.getSessionMapKey)(userId, listItemId, establishedProfile.signature)
207
+ : (0, session_1.getSessionMapKey)(userId, canonical.resolvedOverrides);
208
+ const session = await (0, session_1.getSession)(userId, canonical.resolvedOverrides, listItemId);
77
209
  const totalTabs = (0, session_1.countTotalTabsForSessions)([[sessionMapKey, session]]);
78
210
  if (totalTabs >= session_1.MAX_TABS_PER_SESSION) {
211
+ if (createdSessionProfile && createdSessionProfileSignature) {
212
+ await (0, session_1.rollbackSessionProfileRuntime)(userId, listItemId, createdSessionProfileSignature);
213
+ }
214
+ if (createdDefaultSessionProfileClaim) {
215
+ (0, session_1.clearDefaultSessionProfileClaim)(userId, listItemId);
216
+ createdDefaultSessionProfileClaim = false;
217
+ }
218
+ releaseSessionProfileCreate?.(false);
219
+ releaseSessionProfileCreate = undefined;
79
220
  return res.status(429).json({ error: 'Maximum tabs per session reached' });
80
221
  }
81
222
  const group = (0, session_1.getTabGroup)(session, listItemId);
82
223
  const page = await session.context.newPage();
83
224
  const tabId = node_crypto_1.default.randomUUID();
84
- const tabState = (0, tab_1.createTabState)(page);
225
+ const tabState = await (0, tab_1.createTabState)(page);
85
226
  group.set(tabId, tabState);
86
227
  (0, session_1.indexTab)(tabId, sessionMapKey);
87
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
228
+ (0, download_1.registerDownloadListener)(tabId, String(userId), page);
229
+ await (0, tab_1.navigateWithSafetyGuard)(page, url, {
230
+ allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
231
+ waitUntil: 'domcontentloaded',
232
+ timeout: 30000,
233
+ });
88
234
  tabState.visitedUrls.add(url);
89
235
  (0, logging_1.log)('info', 'openclaw tab opened', { reqId: req.reqId, tabId, url: page.url() });
236
+ lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
237
+ releaseSessionProfileCreate?.(true);
238
+ releaseSessionProfileCreate = undefined;
239
+ createdSessionProfile = false;
90
240
  return res.json({
91
241
  ok: true,
92
242
  targetId: tabId,
@@ -96,9 +246,22 @@ router.post('/tabs/open', async (req, res) => {
96
246
  });
97
247
  }
98
248
  catch (err) {
249
+ if (createdSessionProfile && profileUserId && profileSessionKey) {
250
+ if (createdSessionProfileSignature) {
251
+ await (0, session_1.rollbackSessionProfileRuntime)(profileUserId, profileSessionKey, createdSessionProfileSignature);
252
+ }
253
+ else {
254
+ (0, session_1.clearSessionProfile)(profileUserId, profileSessionKey);
255
+ }
256
+ }
257
+ if (createdDefaultSessionProfileClaim && profileUserId && profileSessionKey) {
258
+ (0, session_1.clearDefaultSessionProfileClaim)(profileUserId, profileSessionKey);
259
+ }
260
+ releaseSessionProfileCreate?.(false);
261
+ releaseSessionProfileCreate = undefined;
99
262
  const message = err instanceof Error ? err.message : String(err);
100
263
  (0, logging_1.log)('error', 'openclaw tab open failed', { reqId: req.reqId, error: message });
101
- return res.status(500).json({ error: (0, errors_1.safeError)(err) });
264
+ return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
102
265
  }
103
266
  });
104
267
  // POST /start - Start browser (OpenClaw expects this)
@@ -128,16 +291,16 @@ router.post('/stop', async (req, res) => {
128
291
  // POST /navigate - Navigate (OpenClaw format with targetId in body)
129
292
  router.post('/navigate', async (req, res) => {
130
293
  try {
131
- const { targetId, url, userId } = req.body;
294
+ if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
295
+ return res.status(403).json({ error: 'Forbidden' });
296
+ }
297
+ const { targetId, url, macro, query, userId } = req.body;
132
298
  if (!userId) {
133
299
  return res.status(400).json({ error: 'userId is required' });
134
300
  }
135
- if (!url) {
136
- return res.status(400).json({ error: 'url is required' });
301
+ if (!url && !macro) {
302
+ return res.status(400).json({ error: 'url or macro required' });
137
303
  }
138
- const urlErr = (0, tab_1.validateUrl)(url);
139
- if (urlErr)
140
- return res.status(400).json({ error: urlErr });
141
304
  const found = (0, session_1.findTabById)(String(targetId), userId);
142
305
  if (!found) {
143
306
  return res.status(404).json({ error: 'Tab not found' });
@@ -145,17 +308,36 @@ router.post('/navigate', async (req, res) => {
145
308
  const { tabState } = found;
146
309
  tabState.toolCalls++;
147
310
  const result = await (0, session_1.withUserLimit)(String(userId), CONFIG.maxConcurrentPerUser, () => (0, tab_1.withTimeout)((0, tab_1.withTabLock)(String(targetId), async () => {
148
- await tabState.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
149
- tabState.visitedUrls.add(url);
311
+ let targetUrl = url;
312
+ if (macro) {
313
+ const { expandMacro } = await Promise.resolve().then(() => __importStar(require('../utils/macros')));
314
+ targetUrl = expandMacro(macro, query) || url;
315
+ }
316
+ if (!targetUrl)
317
+ return { status: 400, body: { error: 'url or macro required' } };
318
+ const urlErr = await (0, tab_1.validateNavigationUrl)(targetUrl, {
319
+ allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
320
+ });
321
+ if (urlErr)
322
+ return { status: 400, body: { error: urlErr } };
323
+ await (0, tab_1.navigateWithSafetyGuard)(tabState.page, targetUrl, {
324
+ allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets,
325
+ waitUntil: 'domcontentloaded',
326
+ timeout: 30000,
327
+ });
328
+ tabState.visitedUrls.add(targetUrl);
150
329
  tabState.refs = await (0, tab_1.buildRefs)(tabState.page);
151
- return { ok: true, targetId, url: tabState.page.url() };
330
+ return { status: 200, body: { ok: true, targetId, url: tabState.page.url() } };
152
331
  }), CONFIG.handlerTimeoutMs, 'openclaw-navigate'));
153
- return res.json(result);
332
+ if (result.status !== 200)
333
+ return res.status(result.status).json(result.body);
334
+ lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
335
+ return res.json(result.body);
154
336
  }
155
337
  catch (err) {
156
338
  const message = err instanceof Error ? err.message : String(err);
157
339
  (0, logging_1.log)('error', 'openclaw navigate failed', { reqId: req.reqId, error: message });
158
- return res.status(500).json({ error: (0, errors_1.safeError)(err) });
340
+ return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
159
341
  }
160
342
  });
161
343
  // GET /snapshot - Snapshot (OpenClaw format with query params)
@@ -171,25 +353,29 @@ router.get('/snapshot', async (req, res) => {
171
353
  }
172
354
  const { tabState } = found;
173
355
  tabState.toolCalls++;
174
- const result = await (0, tab_1.withTimeout)((0, tab_1.snapshotTab)(tabState), CONFIG.handlerTimeoutMs, 'openclaw-snapshot');
356
+ const rawOffset = Number(req.query.offset);
357
+ const offset = Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : 0;
358
+ const raw = await (0, session_1.withUserLimit)(String(userId), CONFIG.maxConcurrentPerUser, () => (0, tab_1.withTimeout)((0, tab_1.snapshotTab)(tabState), CONFIG.handlerTimeoutMs, 'openclaw-snapshot'));
359
+ const payload = (0, tab_1.buildSnapshotPayload)(raw, offset);
175
360
  return res.json({
176
361
  ok: true,
177
362
  format: 'aria',
178
363
  targetId,
179
- url: result.url,
180
- snapshot: result.snapshot,
181
- refsCount: result.refsCount,
364
+ ...payload,
182
365
  });
183
366
  }
184
367
  catch (err) {
185
368
  const message = err instanceof Error ? err.message : String(err);
186
369
  (0, logging_1.log)('error', 'openclaw snapshot failed', { reqId: req.reqId, error: message });
187
- return res.status(500).json({ error: (0, errors_1.safeError)(err) });
370
+ return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
188
371
  }
189
372
  });
190
373
  // POST /act - Combined action endpoint (OpenClaw format)
191
374
  router.post('/act', async (req, res) => {
192
375
  try {
376
+ if (CONFIG.apiKey && !(0, auth_1.isAuthorizedWithApiKey)(req, CONFIG.apiKey)) {
377
+ return res.status(403).json({ error: 'Forbidden' });
378
+ }
193
379
  const body = req.body;
194
380
  const kind = typeof body.kind === 'string' ? body.kind : undefined;
195
381
  const targetId = typeof body.targetId === 'string' ? body.targetId : undefined;
@@ -235,16 +421,17 @@ router.post('/act', async (req, res) => {
235
421
  }
236
422
  }
237
423
  };
238
- if (ref) {
239
- const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
240
- if (!locator)
241
- throw new Error(`Unknown ref: ${ref}`);
242
- await doClick(locator);
243
- }
244
- else {
245
- await doClick(String(selector));
246
- }
247
- await tabState.page.waitForTimeout(500);
424
+ await (0, tab_1.withBlockedNavigationTracking)(tabState.page, async () => {
425
+ if (ref) {
426
+ const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
427
+ if (!locator)
428
+ throw new Error(`Unknown ref: ${ref}`);
429
+ await doClick(locator);
430
+ }
431
+ else {
432
+ await doClick(String(selector));
433
+ }
434
+ });
248
435
  tabState.refs = await (0, tab_1.buildRefs)(tabState.page);
249
436
  return { ok: true, targetId, url: tabState.page.url() };
250
437
  }
@@ -257,20 +444,22 @@ router.post('/act', async (req, res) => {
257
444
  if (typeof text !== 'string' || text.length === 0) {
258
445
  throw new Error('text is required');
259
446
  }
260
- if (ref) {
261
- const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
262
- if (!locator)
263
- throw new Error(`Unknown ref: ${ref}`);
264
- await (0, tab_1.smartFill)(locator, tabState.page, text);
265
- if (submit)
266
- await tabState.page.keyboard.press('Enter');
267
- }
268
- else {
269
- const locator = tabState.page.locator(String(selector));
270
- await (0, tab_1.smartFill)(locator, tabState.page, text);
271
- if (submit)
272
- await tabState.page.keyboard.press('Enter');
273
- }
447
+ await (0, tab_1.withBlockedNavigationTracking)(tabState.page, async () => {
448
+ if (ref) {
449
+ const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
450
+ if (!locator)
451
+ throw new Error(`Unknown ref: ${ref}`);
452
+ await (0, tab_1.smartFill)(locator, tabState.page, text);
453
+ if (submit)
454
+ await tabState.page.keyboard.press('Enter');
455
+ }
456
+ else {
457
+ const locator = tabState.page.locator(String(selector));
458
+ await (0, tab_1.smartFill)(locator, tabState.page, text);
459
+ if (submit)
460
+ await tabState.page.keyboard.press('Enter');
461
+ }
462
+ });
274
463
  return { ok: true, targetId };
275
464
  }
276
465
  case 'select': {
@@ -282,15 +471,17 @@ router.post('/act', async (req, res) => {
282
471
  if (typeof value !== 'string') {
283
472
  throw new Error('value is required');
284
473
  }
285
- if (ref) {
286
- const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
287
- if (!locator)
288
- throw new Error(`Unknown ref: ${ref}`);
289
- await locator.selectOption(value);
290
- }
291
- else {
292
- await tabState.page.locator(String(selector)).selectOption(value);
293
- }
474
+ await (0, tab_1.withBlockedNavigationTracking)(tabState.page, async () => {
475
+ if (ref) {
476
+ const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
477
+ if (!locator)
478
+ throw new Error(`Unknown ref: ${ref}`);
479
+ await locator.selectOption(value);
480
+ }
481
+ else {
482
+ await tabState.page.locator(String(selector)).selectOption(value);
483
+ }
484
+ });
294
485
  return { ok: true, targetId };
295
486
  }
296
487
  case 'press': {
@@ -298,7 +489,9 @@ router.post('/act', async (req, res) => {
298
489
  const { key } = params;
299
490
  if (!key)
300
491
  throw new Error('key is required');
301
- await tabState.page.keyboard.press(key);
492
+ await (0, tab_1.withBlockedNavigationTracking)(tabState.page, async () => {
493
+ await tabState.page.keyboard.press(key);
494
+ });
302
495
  return { ok: true, targetId };
303
496
  }
304
497
  case 'scroll':
@@ -307,17 +500,20 @@ router.post('/act', async (req, res) => {
307
500
  const ref = params.ref;
308
501
  const direction = params.direction ?? 'down';
309
502
  const amount = params.amount ?? 500;
310
- if (ref) {
311
- const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
312
- if (!locator)
313
- throw new Error(`Unknown ref: ${ref}`);
314
- await locator.scrollIntoViewIfNeeded({ timeout: 5000 });
315
- }
316
- else {
317
- const delta = direction === 'up' ? -amount : amount;
318
- await tabState.page.mouse.wheel(0, delta);
319
- }
320
- await tabState.page.waitForTimeout(300);
503
+ await (0, tab_1.withBlockedNavigationTracking)(tabState.page, async () => {
504
+ if (ref) {
505
+ const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
506
+ if (!locator)
507
+ throw new Error(`Unknown ref: ${ref}`);
508
+ await locator.scrollIntoViewIfNeeded({ timeout: 5000 });
509
+ }
510
+ else {
511
+ const isHorizontal = direction === 'left' || direction === 'right';
512
+ const delta = direction === 'up' || direction === 'left' ? -amount : amount;
513
+ await tabState.page.mouse.wheel(isHorizontal ? delta : 0, isHorizontal ? 0 : delta);
514
+ }
515
+ await tabState.page.waitForTimeout(300);
516
+ });
321
517
  return { ok: true, targetId };
322
518
  }
323
519
  case 'hover': {
@@ -325,15 +521,17 @@ router.post('/act', async (req, res) => {
325
521
  const { ref, selector } = params;
326
522
  if (!ref && !selector)
327
523
  throw new Error('ref or selector required');
328
- if (ref) {
329
- const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
330
- if (!locator)
331
- throw new Error(`Unknown ref: ${ref}`);
332
- await locator.hover({ timeout: 5000 });
333
- }
334
- else {
335
- await tabState.page.locator(String(selector)).hover({ timeout: 5000 });
336
- }
524
+ await (0, tab_1.withBlockedNavigationTracking)(tabState.page, async () => {
525
+ if (ref) {
526
+ const locator = await (0, tab_1.refToLocator)(tabState.page, ref, tabState.refs);
527
+ if (!locator)
528
+ throw new Error(`Unknown ref: ${ref}`);
529
+ await locator.hover({ timeout: 5000 });
530
+ }
531
+ else {
532
+ await tabState.page.locator(String(selector)).hover({ timeout: 5000 });
533
+ }
534
+ });
337
535
  return { ok: true, targetId };
338
536
  }
339
537
  case 'wait': {
@@ -341,17 +539,38 @@ router.post('/act', async (req, res) => {
341
539
  const { timeMs, text } = params;
342
540
  const loadStateUnknown = params.loadState;
343
541
  const loadState = typeof loadStateUnknown === 'string' && isLoadState(loadStateUnknown) ? loadStateUnknown : undefined;
344
- if (timeMs) {
345
- await tabState.page.waitForTimeout(timeMs);
346
- }
347
- else if (text) {
348
- await tabState.page.waitForSelector(`text=${text}`, { timeout: 30000 });
349
- }
350
- else if (loadState) {
351
- await tabState.page.waitForLoadState(loadState, { timeout: 30000 });
352
- }
542
+ await (0, tab_1.withBlockedNavigationTracking)(tabState.page, async () => {
543
+ try {
544
+ if (timeMs) {
545
+ await tabState.page.waitForTimeout(timeMs);
546
+ }
547
+ else if (text) {
548
+ await tabState.page.waitForSelector(`text=${text}`, { timeout: 30000 });
549
+ }
550
+ else if (loadState) {
551
+ await tabState.page.waitForLoadState(loadState, { timeout: 30000 });
552
+ }
553
+ }
554
+ catch (err) {
555
+ await (0, tab_1.flushBlockedNavigationError)(tabState.page);
556
+ throw err;
557
+ }
558
+ });
353
559
  return { ok: true, targetId, url: tabState.page.url() };
354
560
  }
561
+ case 'extractStructured': {
562
+ const params = body;
563
+ if (!params.schema || typeof params.schema !== 'object' || Array.isArray(params.schema)) {
564
+ throw new structured_extractor_1.StructuredExtractSchemaError('schema must be an object');
565
+ }
566
+ const payload = await (0, structured_extractor_1.extractStructuredData)(tabState.page, params.schema);
567
+ return {
568
+ ok: true,
569
+ targetId,
570
+ data: payload.data,
571
+ metadata: payload.metadata,
572
+ };
573
+ }
355
574
  case 'close': {
356
575
  await (0, tab_1.safePageClose)(tabState.page);
357
576
  found.group.delete(String(targetId));
@@ -365,16 +584,24 @@ router.post('/act', async (req, res) => {
365
584
  throw new Error(`Unsupported action kind: ${kind}`);
366
585
  }
367
586
  }), CONFIG.handlerTimeoutMs, 'act'));
587
+ lifecycle_controller_1.lifecycleController.recordInteractiveActivity();
368
588
  return res.json(result);
369
589
  }
370
590
  catch (err) {
591
+ if (err instanceof structured_extractor_1.StructuredExtractSchemaError) {
592
+ return res.status(err.statusCode).json({ error: err.message });
593
+ }
594
+ if (err instanceof structured_extractor_1.StructuredExtractRuntimeError) {
595
+ return res.status(err.statusCode).json({
596
+ error: 'Structured extraction failed',
597
+ fieldPath: err.fieldPath,
598
+ reason: err.reason,
599
+ });
600
+ }
371
601
  const message = err instanceof Error ? err.message : String(err);
372
- const statusCode = err?.statusCode;
373
602
  const kindFromBody = typeof req.body.kind === 'string' ? req.body.kind : undefined;
374
603
  (0, logging_1.log)('error', 'act failed', { reqId: req.reqId, kind: kindFromBody, error: message });
375
- if (statusCode === 400)
376
- return res.status(400).json({ error: message });
377
- return res.status(500).json({ error: (0, errors_1.safeError)(err) });
604
+ return res.status(getRouteErrorStatus(err)).json({ error: (0, errors_1.safeError)(err) });
378
605
  }
379
606
  });
380
607
  exports.default = router;