@stubber/virtual-worker 1.5.5 → 1.6.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/.env_dev CHANGED
@@ -1,4 +1,4 @@
1
- FILESERVER_URL=https://uploads.secure-link.services.dev.stubber.com/api/v1
1
+ FILESERVER_URL=https://uploads.secure-link.services.staging.stubber.com/api/v1
2
2
  API_KEY=123-456-789
3
3
  WORKER_NAME=worker-01
4
4
  VNC_ORIGIN=ws://localhost:3000
@@ -0,0 +1,292 @@
1
+ /* eslint-disable id-match */
2
+ import { create_error_conceptual } from "#app/functions/create_error_conceptual.js";
3
+ import { create_error_technical } from "#app/functions/create_error_technical.js";
4
+ import { create_success } from "#app/functions/create_success.js";
5
+ import { get_chromium_page } from "../../helpers/get_chromium_page.js";
6
+ import * as playwright from "playwright";
7
+ import { upload_files } from "../file-server/upload_files.js";
8
+
9
+ const MAX_SERIALIZATION_DEPTH = 6;
10
+ const MAX_SERIALIZATION_KEYS = 100;
11
+ const async_function_constructor = async function () {}.constructor;
12
+
13
+ /**
14
+ * Runs user-provided Playwright code against the current Chromium page.
15
+ * The code is executed as the body of an async function, so `await` and `return`
16
+ * can be used directly.
17
+ *
18
+ * Exposed variables inside the execution context:
19
+ * - `page`: current Playwright page
20
+ * - `context`: the page's BrowserContext
21
+ * - `browser`: the underlying browser instance, if available
22
+ * - `playwright`: Playwright module namespace
23
+ * - `args`: arbitrary user-provided arguments
24
+ * - `params`: original params object
25
+ * - `stubber_context`: task stubber context
26
+ *
27
+ * This runs in-process by design. Isolation is expected to come from the
28
+ * worker container, not from a JavaScript sandbox.
29
+ * @param {Object} params
30
+ * @param {string} params.code - The Playwright code to execute
31
+ * @param {Object} [params.args] - Optional arguments to pass to the code
32
+ *
33
+ *
34
+ */
35
+ export const browser_execute = async (params, stubber_context) => {
36
+ if (!params || typeof params.code !== "string" || !params.code.trim()) {
37
+ return create_error_conceptual({ message: "Missing required parameter: code" });
38
+ }
39
+
40
+ const page_result = await get_chromium_page(params, stubber_context);
41
+ if (!page_result.success) {
42
+ return page_result;
43
+ }
44
+
45
+ const page = page_result.payload;
46
+ const context = page.context();
47
+ const browser = context.browser();
48
+
49
+ const logs = [];
50
+ const attachments = [];
51
+
52
+ try {
53
+ const uploadFile = create_upload_file(stubber_context, attachments);
54
+ const execution_console = create_execution_console(logs);
55
+ const execution = run_user_code(params.code, {
56
+ page,
57
+ context,
58
+ browser,
59
+ playwright,
60
+ params,
61
+ args: params.args || {},
62
+ console: execution_console,
63
+ uploadFile,
64
+ setTimeout,
65
+ clearTimeout,
66
+ setInterval,
67
+ clearInterval,
68
+ URL,
69
+ URLSearchParams,
70
+ TextEncoder,
71
+ TextDecoder,
72
+ AbortController,
73
+ AbortSignal,
74
+ });
75
+
76
+ const raw_result = await Promise.resolve(execution);
77
+ const result = serialize_for_payload(raw_result);
78
+
79
+ return create_success({
80
+ message: "Playwright code executed successfully",
81
+ payload: {
82
+ result,
83
+ logs,
84
+ attachments,
85
+ },
86
+ });
87
+ } catch (error) {
88
+ const technical_error = create_error_technical(error);
89
+ technical_error.error.details.logs = logs;
90
+ technical_error.error.details.attachments = attachments;
91
+ return technical_error;
92
+ }
93
+ };
94
+
95
+ const create_upload_file = (stubber_context, attachments) => {
96
+ return async (screenshot_path) => {
97
+ if (!screenshot_path) {
98
+ return create_error_conceptual({ message: "No screenshot path provided for upload" });
99
+ }
100
+
101
+ const upload_result = await upload_files({ files: [screenshot_path] }, stubber_context);
102
+ if (!upload_result.success) {
103
+ return upload_result;
104
+ }
105
+
106
+ const uploaded_files = upload_result.payload.uploaded_files;
107
+ const file_info = uploaded_files.length > 0 ? uploaded_files[0] : null;
108
+
109
+ if (file_info && attachments) {
110
+ attachments.push(file_info);
111
+ }
112
+
113
+ return create_success({
114
+ message: "Screenshot uploaded successfully",
115
+ payload: {
116
+ file_info,
117
+ },
118
+ });
119
+ };
120
+ };
121
+
122
+ /**
123
+ * @param {string} code
124
+ * @param {object} execution_scope
125
+ */
126
+ const run_user_code = (code, execution_scope) => {
127
+ const parameter_names = Object.keys(execution_scope);
128
+ const parameter_values = Object.values(execution_scope);
129
+ const execute = new async_function_constructor(...parameter_names, code);
130
+
131
+ return execute(...parameter_values);
132
+ };
133
+
134
+ /**
135
+ * @param {Array<{ level: string, args: unknown[] }>} logs
136
+ */
137
+ const create_execution_console = (logs) => {
138
+ return {
139
+ debug: (...args) => log_execution("debug", args, logs),
140
+ info: (...args) => log_execution("info", args, logs),
141
+ log: (...args) => log_execution("log", args, logs),
142
+ warn: (...args) => log_execution("warn", args, logs),
143
+ error: (...args) => log_execution("error", args, logs),
144
+ };
145
+ };
146
+
147
+ /**
148
+ * @param {string} level
149
+ * @param {unknown[]} args
150
+ * @param {Array<{ level: string, args: unknown[] }>} logs
151
+ */
152
+ const log_execution = (level, args, logs) => {
153
+ const serialized_args = args.map((arg) => serialize_for_payload(arg));
154
+ logs.push({ level, args: serialized_args });
155
+ };
156
+
157
+ /**
158
+ * @param {unknown} value
159
+ * @param {WeakSet<object>} [seen]
160
+ * @param {number} [depth]
161
+ * @returns {unknown}
162
+ */
163
+ const serialize_for_payload = (value, seen = new WeakSet(), depth = 0) => {
164
+ if (value === null || value === undefined) {
165
+ return value ?? null;
166
+ }
167
+
168
+ if (depth >= MAX_SERIALIZATION_DEPTH) {
169
+ return `[MaxDepth:${get_constructor_name(value)}]`;
170
+ }
171
+
172
+ const value_type = typeof value;
173
+
174
+ if (value_type === "string" || value_type === "number" || value_type === "boolean") {
175
+ return value;
176
+ }
177
+
178
+ if (value_type === "bigint") {
179
+ return value.toString();
180
+ }
181
+
182
+ if (value_type === "function") {
183
+ return `[Function:${value.name || "anonymous"}]`;
184
+ }
185
+
186
+ if (Array.isArray(value)) {
187
+ return value.map((item) => serialize_for_payload(item, seen, depth + 1));
188
+ }
189
+
190
+ if (value instanceof Error) {
191
+ return {
192
+ name: value.name,
193
+ message: value.message,
194
+ stack: value.stack?.split("\n").slice(0, 2).join("\n"),
195
+ };
196
+ }
197
+
198
+ if (value instanceof Date) {
199
+ return value.toISOString();
200
+ }
201
+
202
+ if (value_type === "object") {
203
+ if (seen.has(value)) {
204
+ return `[Circular:${get_constructor_name(value)}]`;
205
+ }
206
+
207
+ seen.add(value);
208
+
209
+ const playwright_summary = summarize_playwright_object(value);
210
+ if (playwright_summary) {
211
+ return playwright_summary;
212
+ }
213
+
214
+ const result = {};
215
+ const entries = Object.entries(value).slice(0, MAX_SERIALIZATION_KEYS);
216
+ for (const [key, entry_value] of entries) {
217
+ result[key] = serialize_for_payload(entry_value, seen, depth + 1);
218
+ }
219
+
220
+ return result;
221
+ }
222
+
223
+ return String(value);
224
+ };
225
+
226
+ /**
227
+ * @param {unknown} value
228
+ */
229
+ const get_constructor_name = (value) => {
230
+ return value && typeof value === "object" && value.constructor?.name ? value.constructor.name : typeof value;
231
+ };
232
+
233
+ /**
234
+ * @param {unknown} value
235
+ * @returns {object|null}
236
+ */
237
+ const summarize_playwright_object = (value) => {
238
+ const constructor_name = get_constructor_name(value);
239
+
240
+ if (constructor_name === "Page") {
241
+ return {
242
+ type: "Page",
243
+ url: safely_call(value, "url"),
244
+ };
245
+ }
246
+
247
+ if (constructor_name === "BrowserContext") {
248
+ const pages = safely_call(value, "pages");
249
+
250
+ return {
251
+ type: "BrowserContext",
252
+ page_count: Array.isArray(pages) ? pages.length : null,
253
+ };
254
+ }
255
+
256
+ if (constructor_name === "Browser") {
257
+ return {
258
+ type: "Browser",
259
+ is_connected: safely_call(value, "isConnected"),
260
+ };
261
+ }
262
+
263
+ if (constructor_name === "Locator") {
264
+ return {
265
+ type: "Locator",
266
+ };
267
+ }
268
+
269
+ if (constructor_name === "ElementHandle") {
270
+ return {
271
+ type: "ElementHandle",
272
+ };
273
+ }
274
+
275
+ return null;
276
+ };
277
+
278
+ /**
279
+ * @param {unknown} value
280
+ * @param {string} method_name
281
+ */
282
+ const safely_call = (value, method_name) => {
283
+ try {
284
+ if (value && typeof value[method_name] === "function") {
285
+ return value[method_name]();
286
+ }
287
+ } catch {
288
+ return null;
289
+ }
290
+
291
+ return null;
292
+ };
@@ -72,11 +72,14 @@ export const upload_files = async (params, _stubber) => {
72
72
  });
73
73
 
74
74
  if (!response.ok) {
75
- let error_payload = null;
75
+ let error_payload;
76
+
77
+ const raw = await response.text();
78
+
76
79
  try {
77
- error_payload = await response.json();
78
- } catch (_) {
79
- error_payload = { raw_error: await response.text() };
80
+ error_payload = JSON.parse(raw);
81
+ } catch {
82
+ error_payload = { raw_error: raw };
80
83
  }
81
84
 
82
85
  return create_error_conceptual({
@@ -17,6 +17,7 @@ import { api_proxy } from "./api_proxy/api_proxy.js";
17
17
  import { browser_extract_markdown } from "./browser/browser_extract_markdown.js";
18
18
  import { browser_wait } from "./browser/browser_wait.js";
19
19
  import { browser_find } from "./browser/browser_find.js";
20
+ import { browser_execute } from "./browser/browser_execute.js";
20
21
 
21
22
  const all_commands = {
22
23
  browser_click,
@@ -32,6 +33,7 @@ const all_commands = {
32
33
  browser_manage_sessions,
33
34
  browser_wait,
34
35
  browser_find,
36
+ browser_execute,
35
37
 
36
38
  cli_run,
37
39
 
@@ -41,6 +41,7 @@ export const get_chromium_page = async (params, stubber_context) => {
41
41
  headless: headless === true || headless === "true",
42
42
  // eslint-disable-next-line id-match
43
43
  slowMo: slow_mo,
44
+ // executablePath: "/usr/bin/google-chrome",
44
45
  });
45
46
  browser.on("disconnected", () => {
46
47
  console.log("Chromium browser disconnected. Cleaning up browser instance.");
@@ -0,0 +1,70 @@
1
+ @baseUrl = http://localhost:3000
2
+ @apiKey = 123-456-789
3
+ @orguuid = c8cfd7f1-8015-43ff-8878-d22c136a2325
4
+ @stubref = my-stub-ref
5
+
6
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
7
+ Content-Type: application/json
8
+ stubber-virtual-worker-apikey: {{apiKey}}
9
+
10
+ {
11
+ "task": {
12
+ "tasktype": "virtual_worker_send_commands",
13
+ "task_name": "virtual_worker_send_commands",
14
+ "params": {
15
+ "commands": {
16
+ "command_1": {
17
+ "commandtype": "browser_navigate",
18
+ "params": {
19
+ "url": "https://en.wikipedia.org/wiki/Main_Page"
20
+ }
21
+ },
22
+ "command_2": {
23
+ "commandtype": "browser_execute",
24
+ "params": {
25
+ "args": {
26
+ "selector": "h1"
27
+ },
28
+ "code": "const title = await page.title();const headings = await page.locator(args.selector).allInnerTexts();const screenshotPath = `./screenshot-${Date.now()}.png`;await page.screenshot({ path: screenshotPath, fullPage: true });const upload = await uploadScreenshot(screenshotPath);console.log({ title, headings, upload });return {title,headings,url: page.url(),screenshotPath};"
29
+ }
30
+ }
31
+ }
32
+ },
33
+ "_stubber": {
34
+ "orguuid": "{{orguuid}}",
35
+ "stubref": "{{stubref}}"
36
+ }
37
+ }
38
+ }
39
+
40
+ ###
41
+
42
+ # Run eval on the current page context.
43
+ # Assumes the virtual worker already has an active browser page for this stub.
44
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
45
+ Content-Type: application/json
46
+ stubber-virtual-worker-apikey: {{apiKey}}
47
+
48
+ {
49
+ "task": {
50
+ "tasktype": "virtual_worker_send_commands",
51
+ "task_name": "virtual_worker_send_commands",
52
+ "params": {
53
+ "commands": {
54
+ "command_1": {
55
+ "commandtype": "browser_execute",
56
+ "params": {
57
+ "args": {
58
+ "expression": "document.title"
59
+ },
60
+ "code": "const result = await page.evaluate((expression) => eval(expression), args.expression);\nreturn { expression: args.expression, result, url: page.url() };"
61
+ }
62
+ }
63
+ }
64
+ },
65
+ "_stubber": {
66
+ "orguuid": "{{orguuid}}",
67
+ "stubref": "{{stubref}}"
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,39 @@
1
+ @baseUrl = http://localhost:3000
2
+ @apiKey = 123-456-789
3
+ @orguuid = c8cfd7f1-8015-43ff-8878-d22c136a2325
4
+ @stubref = my-stub-ref
5
+
6
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
7
+ Content-Type: application/json
8
+ stubber-virtual-worker-apikey: {{apiKey}}
9
+
10
+ {
11
+ "task": {
12
+ "tasktype": "virtual_worker_send_commands",
13
+ "task_name": "virtual_worker_send_commands",
14
+ "params": {
15
+ "commands": {
16
+ "command_1": {
17
+ "commandtype": "browser_navigate",
18
+ "params": {
19
+ "url": "https://fast.com"
20
+ }
21
+ },
22
+ "command_2": {
23
+ "commandtype": "browser_wait",
24
+ "params": {
25
+ "ms": 2000
26
+ }
27
+ },
28
+ "command_3": {
29
+ "commandtype": "browser_extract_markdown",
30
+ "params": {}
31
+ }
32
+ }
33
+ },
34
+ "_stubber": {
35
+ "orguuid": "{{orguuid}}",
36
+ "stubref": "{{stubref}}"
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,39 @@
1
+ @baseUrl = http://localhost:3000
2
+ @apiKey = 123-456-789
3
+ @orguuid = c8cfd7f1-8015-43ff-8878-d22c136a2325
4
+ @stubref = my-stub-ref
5
+
6
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
7
+ Content-Type: application/json
8
+ stubber-virtual-worker-apikey: {{apiKey}}
9
+
10
+ {
11
+ "task": {
12
+ "tasktype": "virtual_worker_send_commands",
13
+ "task_name": "virtual_worker_send_commands",
14
+ "params": {
15
+ "commands": {
16
+ "command_1": {
17
+ "commandtype": "browser_navigate",
18
+ "params": {
19
+ "url": "https://berkshirehathaway.com"
20
+ }
21
+ },
22
+ "command_2": {
23
+ "commandtype": "browser_find",
24
+ "params": {
25
+ "text": {
26
+ "pattern": "Warren.*Buffett",
27
+ "flags": "i"
28
+ },
29
+ "parent_levels": 1
30
+ }
31
+ }
32
+ }
33
+ },
34
+ "_stubber": {
35
+ "orguuid": "{{orguuid}}",
36
+ "stubref": "{{stubref}}"
37
+ }
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubber/virtual-worker",
3
- "version": "1.5.5",
3
+ "version": "1.6.1",
4
4
  "description": "Template to easily create a node app and keep development standards",
5
5
  "main": "app.js",
6
6
  "directories": {
package/devel/tests.sh DELETED
@@ -1,68 +0,0 @@
1
- curl -X POST "http://localhost:3000/api/v1/task-gateway/virtual_worker_send_commands" \
2
- -H "Content-Type: application/json" \
3
- -H "stubber-virtual-worker-apikey: 123-456-789" \
4
- -d '{
5
- "task": {
6
- "tasktype": "virtual_worker_send_commands",
7
- "task_name": "virtual_worker_send_commands",
8
- "params": {
9
- "commands": {
10
- "command_1": {
11
- "commandtype": "browser_navigate",
12
- "params": {
13
- "url": "https://fast.com"
14
- }
15
- },
16
- "command_2": {
17
- "commandtype": "browser_wait",
18
- "params": {
19
- "ms": 2000
20
- }
21
- },
22
- "command_3": {
23
- "commandtype": "browser_extract_markdown",
24
- "params": {
25
- }
26
- }
27
- }
28
- },
29
- "_stubber": {
30
- "orguuid": "c8cfd7f1-8015-43ff-8878-d22c136a2325",
31
- "stubref": "my-stub-ref"
32
- }
33
- }
34
- }'
35
-
36
- curl -X POST "http://localhost:3000/api/v1/task-gateway/virtual_worker_send_commands" \
37
- -H "Content-Type: application/json" \
38
- -H "stubber-virtual-worker-apikey: 123-456-789" \
39
- -d '{
40
- "task": {
41
- "tasktype": "virtual_worker_send_commands",
42
- "task_name": "virtual_worker_send_commands",
43
- "params": {
44
- "commands": {
45
- "command_1": {
46
- "commandtype": "browser_navigate",
47
- "params": {
48
- "url": "https://berkshirehathaway.com"
49
- }
50
- },
51
- "command_2": {
52
- "commandtype": "browser_find",
53
- "params": {
54
- "text": {
55
- "pattern": "Warren.*Buffett",
56
- "flags": "i"
57
- },
58
- "parent_levels": 1
59
- }
60
- }
61
- }
62
- },
63
- "_stubber": {
64
- "orguuid": "c8cfd7f1-8015-43ff-8878-d22c136a2325",
65
- "stubref": "my-stub-ref"
66
- }
67
- }
68
- }'