@ceki/n8n-nodes-ceki 0.2.5 → 0.2.7

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/dist/index.js ADDED
@@ -0,0 +1,1384 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ credentials: () => credentials,
24
+ nodes: () => nodes
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // lib/ceki-client.ts
29
+ function jsonParse(raw) {
30
+ try {
31
+ return JSON.parse(raw);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ var CekiClient = class {
37
+ constructor(_token, _relayUrl = "wss://browser.ceki.me/ws/agent", _apiUrl = "https://api.ceki.me") {
38
+ this._token = _token;
39
+ this._relayUrl = _relayUrl;
40
+ this._apiUrl = _apiUrl;
41
+ this._ws = null;
42
+ this._connected = false;
43
+ this._pendingRents = /* @__PURE__ */ new Map();
44
+ this._pendingResumes = /* @__PURE__ */ new Map();
45
+ this._pendingCdp = /* @__PURE__ */ new Map();
46
+ this._cdpCounter = 1;
47
+ this._activeSessions = /* @__PURE__ */ new Map();
48
+ this._connectReject = null;
49
+ this._closed = false;
50
+ }
51
+ /** Connect to the relay WebSocket. */
52
+ async connect() {
53
+ if (this._connected) return;
54
+ const protocols = [`bearer.${this._token}`];
55
+ this._ws = new WebSocket(this._relayUrl, protocols);
56
+ this._ws.onopen = () => {
57
+ this._connected = true;
58
+ };
59
+ this._ws.onmessage = (ev) => {
60
+ this._handleMessage(ev.data);
61
+ };
62
+ this._ws.onclose = (ev) => {
63
+ this._connected = false;
64
+ if ((ev.code === 4401 || ev.code === 4403) && this._connectReject) {
65
+ this._connectReject(new Error(`Auth failed: ${ev.reason || String(ev.code)}`));
66
+ this._connectReject = null;
67
+ }
68
+ };
69
+ this._ws.onerror = () => {
70
+ if (!this._connected && this._connectReject) {
71
+ this._connectReject(new Error("WebSocket connection failed"));
72
+ this._connectReject = null;
73
+ }
74
+ };
75
+ if (this._ws.readyState === WebSocket.CONNECTING) {
76
+ await new Promise((resolve, reject) => {
77
+ if (!this._ws) {
78
+ reject(new Error("No WebSocket"));
79
+ return;
80
+ }
81
+ this._connectReject = reject;
82
+ this._ws.onopen = () => {
83
+ this._connected = true;
84
+ resolve();
85
+ };
86
+ });
87
+ }
88
+ }
89
+ _sendRaw(msg) {
90
+ if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
91
+ throw new Error("WebSocket not connected");
92
+ }
93
+ this._ws.send(JSON.stringify(msg));
94
+ }
95
+ /** Search for available browser providers (HTTP GET). */
96
+ async search(filters, limit) {
97
+ const params = new URLSearchParams();
98
+ if (limit != null) params.set("limit", String(limit));
99
+ if (filters) {
100
+ for (const [k, v] of Object.entries(filters)) {
101
+ if (v != null) params.set(k, String(v));
102
+ }
103
+ }
104
+ const resp = await fetch(`${this._apiUrl}/api/browsers/search?${params}`, {
105
+ headers: { Authorization: `Bearer ${this._token}` }
106
+ });
107
+ if (!resp.ok) throw new Error(`Search failed: ${resp.status}`);
108
+ const body = await resp.json();
109
+ const data = body.data ?? body;
110
+ return Array.isArray(data) ? data : [];
111
+ }
112
+ /** Rent a browser by schedule_id. */
113
+ async rent(scheduleId, opts) {
114
+ const msg = { type: "rent", browser_id: scheduleId };
115
+ if (opts?.mode) msg.mode = opts.mode;
116
+ this._sendRaw(msg);
117
+ return this._awaitRent(`rent:${scheduleId}`, scheduleId, 9e4);
118
+ }
119
+ /** Resume an existing session. */
120
+ async resume(sessionId) {
121
+ this._sendRaw({ type: "resume", session_id: sessionId });
122
+ return this._awaitResume(sessionId);
123
+ }
124
+ /** Close the WS connection — session stays alive in grace. */
125
+ disconnect() {
126
+ this._closed = true;
127
+ this._activeSessions.clear();
128
+ this._pendingRents.clear();
129
+ this._pendingResumes.clear();
130
+ this._closeWs();
131
+ }
132
+ /** Close everything. */
133
+ close() {
134
+ this.disconnect();
135
+ }
136
+ // ── private ────────────────────────────────────────────────
137
+ _awaitRent(key, scheduleId, timeoutMs) {
138
+ return new Promise((resolve, reject) => {
139
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
140
+ timeoutSignal.addEventListener("abort", () => {
141
+ this._pendingRents.delete(key);
142
+ reject(new Error("Rent timed out"));
143
+ }, { once: true });
144
+ this._pendingRents.set(key, {
145
+ resolve: (match) => {
146
+ const browser = new CekiBrowser(this, match);
147
+ this._activeSessions.set(browser.sessionId, browser);
148
+ resolve(browser);
149
+ },
150
+ reject: (err) => {
151
+ reject(err);
152
+ }
153
+ });
154
+ });
155
+ }
156
+ _awaitResume(sessionId) {
157
+ return new Promise((resolve, reject) => {
158
+ const timeoutSignal = AbortSignal.timeout(1e4);
159
+ timeoutSignal.addEventListener("abort", () => {
160
+ this._pendingResumes.delete(sessionId);
161
+ reject(new Error("Resume timed out"));
162
+ }, { once: true });
163
+ this._pendingResumes.set(sessionId, {
164
+ resolve: (match) => {
165
+ const browser = new CekiBrowser(this, match);
166
+ this._activeSessions.set(browser.sessionId, browser);
167
+ resolve(browser);
168
+ },
169
+ reject: (err) => {
170
+ reject(err);
171
+ }
172
+ });
173
+ });
174
+ }
175
+ _closeWs() {
176
+ if (this._ws) {
177
+ try {
178
+ this._ws.onopen = null;
179
+ this._ws.onmessage = null;
180
+ this._ws.onclose = null;
181
+ this._ws.onerror = null;
182
+ this._ws.close();
183
+ } catch {
184
+ }
185
+ this._ws = null;
186
+ }
187
+ }
188
+ _handleMessage(data) {
189
+ const msg = jsonParse(String(data));
190
+ if (!msg || typeof msg !== "object") return;
191
+ const type = String(msg.type ?? "");
192
+ const sid = msg.session_id ? String(msg.session_id) : null;
193
+ switch (type) {
194
+ case "pong":
195
+ case "rent_pending":
196
+ break;
197
+ case "match":
198
+ this._onMatch(msg);
199
+ break;
200
+ case "rent.error":
201
+ this._onRentError(msg);
202
+ break;
203
+ case "resume_ok":
204
+ this._onResumeOk(msg);
205
+ break;
206
+ case "resume_failed":
207
+ this._onResumeFailed(msg);
208
+ break;
209
+ case "cdp_response":
210
+ if (sid) this._onCdpResponse(sid, msg);
211
+ break;
212
+ case "session.ended":
213
+ if (sid) {
214
+ this._activeSessions.delete(sid);
215
+ const b = this._activeSessions.get(sid);
216
+ if (b) b._ended = String(msg.reason ?? "ended");
217
+ }
218
+ break;
219
+ }
220
+ }
221
+ _onMatch(msg) {
222
+ const scheduleId = Number(msg.schedule_id ?? 0);
223
+ const eventId = msg.event_id ? String(msg.event_id) : null;
224
+ let pending = this._pendingRents.get(`event:${eventId}`);
225
+ if (!pending) pending = this._pendingRents.get(`rent:${scheduleId}`);
226
+ if (pending) {
227
+ this._pendingRents.delete(`event:${eventId}`);
228
+ this._pendingRents.delete(`rent:${scheduleId}`);
229
+ pending.resolve({
230
+ session_id: String(msg.session_id ?? ""),
231
+ schedule_id: scheduleId,
232
+ event_id: eventId,
233
+ chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
234
+ provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
235
+ browser_info: msg.browser_info ?? {}
236
+ });
237
+ }
238
+ }
239
+ _onRentError(msg) {
240
+ const code = String(msg.code ?? "");
241
+ const message = String(msg.message ?? "");
242
+ for (const [key, pending] of this._pendingRents) {
243
+ this._pendingRents.delete(key);
244
+ pending.reject(new Error(message || `Rent error: ${code}`));
245
+ return;
246
+ }
247
+ }
248
+ _onResumeOk(msg) {
249
+ const sessionId = String(msg.session_id ?? "");
250
+ const pending = this._pendingResumes.get(sessionId);
251
+ if (!pending) return;
252
+ this._pendingResumes.delete(sessionId);
253
+ pending.resolve({
254
+ session_id: sessionId,
255
+ schedule_id: Number(msg.schedule_id ?? 0),
256
+ event_id: msg.event_id ? String(msg.event_id) : null,
257
+ chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
258
+ provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
259
+ browser_info: msg.browser_info ?? {}
260
+ });
261
+ }
262
+ _onResumeFailed(msg) {
263
+ const sessionId = String(msg.session_id ?? "");
264
+ const pending = this._pendingResumes.get(sessionId);
265
+ if (!pending) return;
266
+ this._pendingResumes.delete(sessionId);
267
+ pending.reject(new Error(String(msg.reason ?? "Resume failed")));
268
+ }
269
+ _onCdpResponse(sessionId, msg) {
270
+ const browser = this._activeSessions.get(sessionId);
271
+ if (!browser) return;
272
+ const id = Number(msg.id ?? 0);
273
+ const pending = this._pendingCdp.get(id);
274
+ if (!pending) return;
275
+ this._pendingCdp.delete(id);
276
+ if (msg.error) {
277
+ pending.reject(new Error(String(msg.error.message ?? "CDP error")));
278
+ } else {
279
+ pending.resolve(msg.result);
280
+ }
281
+ }
282
+ };
283
+ var CekiBrowser = class {
284
+ constructor(client, match) {
285
+ this._cdpId = 1;
286
+ this._pendingCdp = /* @__PURE__ */ new Map();
287
+ this._client = client;
288
+ this.sessionId = match.session_id;
289
+ this.scheduleId = match.schedule_id;
290
+ this.chatTopicId = match.chat_topic_id ?? null;
291
+ this.browserInfo = match.browser_info ?? {};
292
+ this.providerUserId = match.provider_user_id ?? null;
293
+ }
294
+ /** Send a CDP command. Uses AbortSignal.timeout() (no restricted setTimeout global). */
295
+ async send(method, params, timeoutMs = 3e4) {
296
+ const id = this._cdpId++;
297
+ const msg = { type: "cdp", session_id: this.sessionId, id, method, params: params ?? {} };
298
+ this._client._sendRaw(msg);
299
+ return new Promise((resolve, reject) => {
300
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
301
+ timeoutSignal.addEventListener("abort", () => {
302
+ this._pendingCdp.delete(id);
303
+ reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`));
304
+ }, { once: true });
305
+ this._pendingCdp.set(id, {
306
+ resolve: (v) => {
307
+ resolve(v);
308
+ },
309
+ reject: (e) => {
310
+ reject(e);
311
+ }
312
+ });
313
+ });
314
+ }
315
+ async navigate(url, timeoutMs) {
316
+ await this.send("Page.navigate", { url }, timeoutMs);
317
+ }
318
+ async click(x, y) {
319
+ await this.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
320
+ await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
321
+ }
322
+ async type(text) {
323
+ await this.send("Ceki.typeText", { text });
324
+ }
325
+ async scroll(deltaY) {
326
+ await this.send("Input.dispatchMouseEvent", { type: "mouseWheel", x: 0, y: 0, deltaX: 0, deltaY });
327
+ }
328
+ async screenshot(opts) {
329
+ const format = opts?.format ?? "base64";
330
+ const fullPage = opts?.fullPage ?? false;
331
+ let clip;
332
+ if (fullPage) {
333
+ const metrics = await this.send("Page.getLayoutMetrics");
334
+ const contentSize = metrics?.contentSize;
335
+ if (contentSize) {
336
+ clip = { x: 0, y: 0, width: Number(contentSize.width ?? 1920), height: Math.min(Number(contentSize.height ?? 1080), 16384), scale: 1 };
337
+ }
338
+ }
339
+ const result = await this.send("Page.captureScreenshot", { format: "png", ...clip ? { clip } : {} });
340
+ const data = String(result?.data ?? "");
341
+ if (format === "png") {
342
+ return Buffer.from(data, "base64");
343
+ }
344
+ return { data };
345
+ }
346
+ async snapshot() {
347
+ const ssResult = await this.screenshot({ format: "base64" });
348
+ return { screenshot: ssResult.data, ts: /* @__PURE__ */ new Date() };
349
+ }
350
+ async upload(selector, buf, filename = "file") {
351
+ const b64 = buf.toString("base64");
352
+ const size = buf.length;
353
+ const mime = "application/octet-stream";
354
+ const expression = `(function(){
355
+ var input = document.querySelector(${JSON.stringify(selector)});
356
+ if (!input) return JSON.stringify({ok:false,error:'Element not found'});
357
+ var b = atob(${JSON.stringify(b64)});
358
+ var u = new Uint8Array(b.length);
359
+ for (var i = 0; i < b.length; i++) u[i] = b.charCodeAt(i);
360
+ var f = new File([u], ${JSON.stringify(filename)}, {type: ${JSON.stringify(mime)}});
361
+ var dt = new DataTransfer(); dt.items.add(f);
362
+ input.files = dt.files;
363
+ input.dispatchEvent(new Event('change',{bubbles:true}));
364
+ return JSON.stringify({ok:true,filename:${JSON.stringify(filename)},size:${size}});
365
+ })()`;
366
+ const result = await this.send("Runtime.evaluate", { expression, returnByValue: true });
367
+ try {
368
+ await this.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 });
369
+ await this.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 });
370
+ } catch {
371
+ }
372
+ const resultObj = result?.result;
373
+ if (resultObj?.value) return JSON.parse(String(resultObj.value));
374
+ return { ok: true, filename, size };
375
+ }
376
+ async close() {
377
+ await this.send("Ceki.close", {}).catch(() => {
378
+ });
379
+ this._client._activeSessions.delete(this.sessionId);
380
+ }
381
+ };
382
+
383
+ // nodes/BrowserCeki/BrowserCeki.node.ts
384
+ var sleep = (ms) => new Promise((resolve) => {
385
+ AbortSignal.timeout(ms).addEventListener("abort", () => resolve(), { once: true });
386
+ });
387
+ var BrowserCeki = class {
388
+ constructor() {
389
+ this.description = {
390
+ displayName: "Browser Ceki",
391
+ name: "browserCeki",
392
+ icon: "file:ceki.png",
393
+ group: ["transform"],
394
+ version: 1,
395
+ subtitle: '={{ "Ceki: " + $operation }}',
396
+ description: "Rent a real human browser and control it: rent, navigate, click, type, screenshot, solve captchas, and more",
397
+ defaults: { name: "Browser Ceki" },
398
+ inputs: ["main"],
399
+ outputs: ["main"],
400
+ credentials: [{ name: "cekiApi", required: true }],
401
+ properties: [
402
+ {
403
+ displayName: "Operation",
404
+ name: "operation",
405
+ type: "options",
406
+ default: "rent",
407
+ options: [
408
+ { name: "Rent", value: "rent" },
409
+ { name: "Navigate", value: "navigate" },
410
+ { name: "Click", value: "click" },
411
+ { name: "Type", value: "type" },
412
+ { name: "Scroll", value: "scroll" },
413
+ { name: "Screenshot", value: "screenshot" },
414
+ { name: "Snapshot", value: "snapshot" },
415
+ { name: "Wait", value: "wait" },
416
+ { name: "Wait for Selector", value: "waitForSelector" },
417
+ { name: "Upload", value: "upload" },
418
+ { name: "Close", value: "close" }
419
+ ]
420
+ },
421
+ // === Rent: rental parameters ===
422
+ {
423
+ displayName: "Schedule ID",
424
+ name: "scheduleId",
425
+ type: "number",
426
+ default: 0,
427
+ description: "0 \u2014 search by the filters below",
428
+ displayOptions: { show: { operation: ["rent"] } }
429
+ },
430
+ {
431
+ displayName: "Geo",
432
+ name: "geo",
433
+ type: "string",
434
+ default: "",
435
+ placeholder: "RU, EE, US\u2026",
436
+ displayOptions: { show: { operation: ["rent"] } }
437
+ },
438
+ {
439
+ displayName: "Max $/min",
440
+ name: "maxPrice",
441
+ type: "number",
442
+ typeOptions: { numberPrecision: 4 },
443
+ default: 0.02,
444
+ displayOptions: { show: { operation: ["rent"] } }
445
+ },
446
+ {
447
+ displayName: "Min rating",
448
+ name: "minRating",
449
+ type: "number",
450
+ default: 0,
451
+ displayOptions: { show: { operation: ["rent"] } }
452
+ },
453
+ {
454
+ displayName: "Profile mode",
455
+ name: "mode",
456
+ type: "options",
457
+ default: "main",
458
+ options: [
459
+ { name: "main", value: "main" },
460
+ { name: "incognito", value: "incognito" }
461
+ ],
462
+ displayOptions: { show: { operation: ["rent"] } }
463
+ },
464
+ // === Operations: session_id ===
465
+ {
466
+ displayName: "Session ID",
467
+ name: "sessionId",
468
+ type: "string",
469
+ default: "={{ $json.session_id }}",
470
+ description: "From the Rent operation",
471
+ required: true,
472
+ displayOptions: {
473
+ show: {
474
+ operation: ["navigate", "click", "type", "scroll", "screenshot", "snapshot", "wait", "waitForSelector", "upload", "close"]
475
+ }
476
+ }
477
+ },
478
+ {
479
+ displayName: "URL",
480
+ name: "url",
481
+ type: "string",
482
+ default: "",
483
+ required: true,
484
+ displayOptions: { show: { operation: ["navigate"] } }
485
+ },
486
+ {
487
+ displayName: "X",
488
+ name: "x",
489
+ type: "number",
490
+ default: 0,
491
+ displayOptions: { show: { operation: ["click"] } }
492
+ },
493
+ {
494
+ displayName: "Y",
495
+ name: "y",
496
+ type: "number",
497
+ default: 0,
498
+ displayOptions: { show: { operation: ["click"] } }
499
+ },
500
+ {
501
+ displayName: "Text",
502
+ name: "text",
503
+ type: "string",
504
+ default: "",
505
+ displayOptions: { show: { operation: ["type"] } }
506
+ },
507
+ {
508
+ displayName: "Delta Y",
509
+ name: "deltaY",
510
+ type: "number",
511
+ default: -300,
512
+ displayOptions: { show: { operation: ["scroll"] } }
513
+ },
514
+ {
515
+ displayName: "Format",
516
+ name: "format",
517
+ type: "options",
518
+ default: "png",
519
+ options: [
520
+ { name: "PNG (binary)", value: "png" },
521
+ { name: "Base64", value: "base64" }
522
+ ],
523
+ displayOptions: { show: { operation: ["screenshot"] } }
524
+ },
525
+ {
526
+ displayName: "Full page",
527
+ name: "fullPage",
528
+ type: "boolean",
529
+ default: false,
530
+ displayOptions: { show: { operation: ["screenshot"] } }
531
+ },
532
+ {
533
+ displayName: "Milliseconds",
534
+ name: "ms",
535
+ type: "number",
536
+ default: 1e3,
537
+ typeOptions: { minValue: 0 },
538
+ description: "Fixed delay on the active session",
539
+ displayOptions: { show: { operation: ["wait"] } }
540
+ },
541
+ {
542
+ displayName: "CSS Selector",
543
+ name: "waitSelector",
544
+ type: "string",
545
+ default: "",
546
+ required: true,
547
+ placeholder: "e.g. .results, #content, table tr",
548
+ displayOptions: { show: { operation: ["waitForSelector"] } }
549
+ },
550
+ {
551
+ displayName: "Timeout (ms)",
552
+ name: "waitTimeout",
553
+ type: "number",
554
+ default: 3e4,
555
+ description: "Waits until the selector appears in the DOM",
556
+ displayOptions: { show: { operation: ["waitForSelector"] } }
557
+ },
558
+ {
559
+ displayName: "CSS Selector",
560
+ name: "selector",
561
+ type: "string",
562
+ default: "",
563
+ required: true,
564
+ displayOptions: { show: { operation: ["upload"] } }
565
+ },
566
+ {
567
+ displayName: "Binary Property",
568
+ name: "binaryPropertyName",
569
+ type: "string",
570
+ default: "data",
571
+ displayOptions: { show: { operation: ["upload"] } }
572
+ }
573
+ ]
574
+ };
575
+ }
576
+ async execute() {
577
+ const items = this.getInputData();
578
+ const out = [];
579
+ const creds = await this.getCredentials("cekiApi");
580
+ const token = creds.token;
581
+ const resolveSid = async (i, client) => {
582
+ const scheduleId = this.getNodeParameter("scheduleId", i);
583
+ if (scheduleId) return scheduleId;
584
+ const geo = this.getNodeParameter("geo", i);
585
+ const maxPrice = this.getNodeParameter("maxPrice", i);
586
+ const list = await client.search({
587
+ geo: geo || void 0,
588
+ max_price_per_min: maxPrice
589
+ });
590
+ if (!list.length) throw new Error("No browsers found by filters");
591
+ return list[0].schedule_id;
592
+ };
593
+ for (let i = 0; i < items.length; i++) {
594
+ const op = this.getNodeParameter("operation", i);
595
+ const client = new CekiClient(token);
596
+ await client.connect();
597
+ let browser;
598
+ try {
599
+ if (op === "rent") {
600
+ const sid2 = await resolveSid(i, client);
601
+ const mode = this.getNodeParameter("mode", i);
602
+ browser = await client.rent(sid2, { mode });
603
+ out.push({
604
+ json: { session_id: browser.sessionId, schedule_id: sid2, mode }
605
+ });
606
+ await client.disconnect();
607
+ continue;
608
+ }
609
+ const sessionId = this.getNodeParameter("sessionId", i);
610
+ browser = await client.resume(sessionId);
611
+ const sid = browser.sessionId;
612
+ switch (op) {
613
+ case "navigate": {
614
+ const url = this.getNodeParameter("url", i);
615
+ await browser.navigate(url);
616
+ out.push({ json: { session_id: sid, url } });
617
+ break;
618
+ }
619
+ case "click": {
620
+ const x = this.getNodeParameter("x", i);
621
+ const y = this.getNodeParameter("y", i);
622
+ await browser.click(x, y);
623
+ out.push({ json: { session_id: sid, clicked: [x, y] } });
624
+ break;
625
+ }
626
+ case "type": {
627
+ const text = this.getNodeParameter("text", i);
628
+ await browser.type(text);
629
+ out.push({ json: { session_id: sid, typed: text } });
630
+ break;
631
+ }
632
+ case "scroll": {
633
+ const deltaY = this.getNodeParameter("deltaY", i);
634
+ await browser.scroll(deltaY);
635
+ out.push({ json: { session_id: sid, scrolled: deltaY } });
636
+ break;
637
+ }
638
+ case "screenshot": {
639
+ const format = this.getNodeParameter("format", i);
640
+ const fullPage = this.getNodeParameter("fullPage", i);
641
+ const shot = await browser.screenshot({ format, fullPage });
642
+ const data = format === "base64" ? shot.data : shot.toString("base64");
643
+ const binary = await this.helpers.prepareBinaryData(
644
+ Buffer.from(data, "base64"),
645
+ "screenshot.png",
646
+ "image/png"
647
+ );
648
+ out.push({ json: { session_id: sid }, binary: { data: binary } });
649
+ break;
650
+ }
651
+ case "snapshot": {
652
+ const snap = await browser.snapshot();
653
+ out.push({
654
+ json: { session_id: sid, screenshot: snap.screenshot }
655
+ });
656
+ break;
657
+ }
658
+ case "wait": {
659
+ const ms = this.getNodeParameter("ms", i);
660
+ await sleep(ms);
661
+ out.push({ json: { session_id: sid, waited: ms } });
662
+ break;
663
+ }
664
+ case "waitForSelector": {
665
+ const selector = this.getNodeParameter("waitSelector", i);
666
+ const timeout = this.getNodeParameter("waitTimeout", i);
667
+ const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
668
+ const deadline = Date.now() + timeout;
669
+ let ok = false;
670
+ let lastErr = null;
671
+ while (Date.now() < deadline) {
672
+ try {
673
+ const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
674
+ if (res?.result?.value === true) {
675
+ ok = true;
676
+ break;
677
+ }
678
+ } catch (e) {
679
+ lastErr = e;
680
+ }
681
+ await sleep(500);
682
+ }
683
+ if (!ok) {
684
+ throw new Error(
685
+ `waitForSelector("${selector}") timed out after ${timeout}ms${lastErr ? `: ${lastErr.message}` : ""}`
686
+ );
687
+ }
688
+ out.push({ json: { session_id: sid, selector, found: true } });
689
+ break;
690
+ }
691
+ case "upload": {
692
+ const selector = this.getNodeParameter("selector", i);
693
+ const bpn = this.getNodeParameter("binaryPropertyName", i);
694
+ const bin = items[i].binary?.[bpn];
695
+ if (!bin) throw new Error(`Binary property "${bpn}" not found on input`);
696
+ const stream = await this.helpers.getBinaryStream(bin.id);
697
+ const chunks = [];
698
+ for await (const c of stream) chunks.push(c);
699
+ const buf = Buffer.concat(chunks);
700
+ const res = await browser.upload(selector, buf);
701
+ out.push({ json: { session_id: sid, uploaded: res } });
702
+ break;
703
+ }
704
+ case "close": {
705
+ await browser.close();
706
+ out.push({ json: { closed: true, session_id: sessionId } });
707
+ break;
708
+ }
709
+ }
710
+ if (op === "close") {
711
+ await client.close();
712
+ } else {
713
+ await client.disconnect();
714
+ }
715
+ } finally {
716
+ }
717
+ }
718
+ return [out];
719
+ }
720
+ };
721
+
722
+ // lib/contract-client.ts
723
+ function cleanArgs(o) {
724
+ const out = { ...o };
725
+ for (const k of Object.keys(out)) {
726
+ if (out[k] === void 0 || out[k] === null) delete out[k];
727
+ }
728
+ return out;
729
+ }
730
+ function parseBenefitable(value) {
731
+ if (!value) return null;
732
+ const m = /^(agent|user):(\d+)$/.exec(value);
733
+ if (!m) return null;
734
+ return { type: m[1], value: Number(m[2]) };
735
+ }
736
+ function parseParticipant(value, roleId) {
737
+ const b = parseBenefitable(value);
738
+ if (!b) return null;
739
+ return { participable_id: b.value, type: b.type, role_id: roleId };
740
+ }
741
+ function deriveLabel(desc) {
742
+ if (!desc) return "";
743
+ const line = desc.split("\n")[0].trim();
744
+ return line.length > 60 ? line.slice(0, 57) + "..." : line;
745
+ }
746
+ var ROLE_REVIEWER = 5;
747
+ var ROLE_QA = 6;
748
+ var ContractClient = class {
749
+ constructor(token, endpoint, apiBase) {
750
+ this._endpoint = (endpoint ?? "https://api.ceki.me/mcp").replace(/\/+$/, "");
751
+ this._apiBase = (apiBase ?? "https://api.ceki.me").replace(/\/+$/, "");
752
+ this._token = token;
753
+ }
754
+ _headers() {
755
+ return {
756
+ "Content-Type": "application/json",
757
+ Accept: "application/json",
758
+ Authorization: `Bearer ${this._token}`
759
+ };
760
+ }
761
+ async _rpc(method, params) {
762
+ const body = JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params });
763
+ const resp = await fetch(this._endpoint, {
764
+ method: "POST",
765
+ headers: this._headers(),
766
+ body
767
+ });
768
+ if (!resp.ok) {
769
+ const text = await resp.text().catch(() => "");
770
+ throw new Error(`HTTP ${resp.status}: ${text.slice(0, 400)}`);
771
+ }
772
+ return resp.json();
773
+ }
774
+ async _call(tool, args) {
775
+ const body = await this._rpc("tools/call", { name: tool, arguments: args ?? {} });
776
+ if (body.error) throw new Error(`${tool} \u2192 ${JSON.stringify(body.error).slice(0, 400)}`);
777
+ const result = body.result ?? {};
778
+ const content = result.content;
779
+ if (Array.isArray(content)) {
780
+ const texts = content.filter((c) => c.type === "text").map((c) => String(c.text ?? ""));
781
+ const joined = texts.join("\n");
782
+ try {
783
+ return JSON.parse(joined);
784
+ } catch {
785
+ return joined;
786
+ }
787
+ }
788
+ if (result.structuredContent !== void 0) return result.structuredContent;
789
+ return result;
790
+ }
791
+ // ── domain methods ─────────────────────────────────────────
792
+ async listContracts() {
793
+ return this._call("list-contracts");
794
+ }
795
+ async members(contractId) {
796
+ return this._call("contract-members", { contract_id: contractId });
797
+ }
798
+ async tasks(contractId) {
799
+ return this._call("contract-tasks", { contract_id: contractId });
800
+ }
801
+ async myEvents() {
802
+ return this._call("get-my-events");
803
+ }
804
+ async task(eventId) {
805
+ return this._call("get-event", { event_id: eventId });
806
+ }
807
+ async create(contractId, opts) {
808
+ const args = cleanArgs({
809
+ contract_id: contractId,
810
+ label: opts.label,
811
+ type_id: opts.type,
812
+ status_id: opts.status,
813
+ description: opts.description,
814
+ benefitable: opts.benefitable ? parseBenefitable(opts.benefitable) : void 0
815
+ });
816
+ const users = [];
817
+ const rev = parseParticipant(opts.reviewer, ROLE_REVIEWER);
818
+ if (rev) users.push(rev);
819
+ const qa = parseParticipant(opts.qa, ROLE_QA);
820
+ if (qa) users.push(qa);
821
+ if (users.length) args.users = users;
822
+ return this._call("create-contract-event", args);
823
+ }
824
+ async propose(eventId, opts) {
825
+ return this._call("propose-correction", cleanArgs({
826
+ event_id: eventId,
827
+ status_id: opts.status,
828
+ label: opts.label,
829
+ description: opts.description,
830
+ benefitable: opts.benefitable ? parseBenefitable(opts.benefitable) : void 0
831
+ }));
832
+ }
833
+ async comment(eventId, opts) {
834
+ return this._call("comment", cleanArgs({
835
+ event_id: eventId,
836
+ label: opts?.label,
837
+ description: opts?.description
838
+ }));
839
+ }
840
+ async progress(eventId, opts) {
841
+ let statusResult = null;
842
+ if (opts.status != null) {
843
+ statusResult = await this.propose(eventId, { status: opts.status });
844
+ }
845
+ const commentResult = await this.comment(eventId, { label: deriveLabel(opts.desc), description: opts.desc });
846
+ return { status_correction: statusResult, comment: commentResult };
847
+ }
848
+ async callHuman(eventId, kind, desc) {
849
+ if (!["input", "review", "stuck"].includes(kind)) throw new Error(`kind must be input|review|stuck, got ${kind}`);
850
+ return this._call("call-human", { event_id: eventId, kind, desc });
851
+ }
852
+ /** GET /agent/polling. Returns [] on 429. */
853
+ async poll() {
854
+ const resp = await fetch(`${this._apiBase}/agent/polling`, {
855
+ headers: { Accept: "application/json", Authorization: `Bearer ${this._token}` }
856
+ });
857
+ if (resp.status === 429) return [];
858
+ if (!resp.ok) {
859
+ const text = await resp.text().catch(() => "");
860
+ throw new Error(`poll HTTP ${resp.status}: ${text.slice(0, 300)}`);
861
+ }
862
+ const body = await resp.json();
863
+ if (Array.isArray(body)) return body;
864
+ if (body && typeof body === "object") {
865
+ for (const k of ["notifications", "data", "items"]) {
866
+ if (Array.isArray(body[k])) return body[k];
867
+ }
868
+ }
869
+ return [];
870
+ }
871
+ };
872
+
873
+ // nodes/CekiContract/CekiContract.node.ts
874
+ var STATUS_OPTIONS = [
875
+ { name: "100 \xB7 Backlog", value: 100 },
876
+ { name: "200 \xB7 Hand (assigned)", value: 200 },
877
+ { name: "222 \xB7 Hand done", value: 222 },
878
+ { name: "300 \xB7 QA", value: 300 },
879
+ { name: "350 \xB7 QA done", value: 350 },
880
+ { name: "499 \xB7 Reviewer", value: 499 }
881
+ ];
882
+ var CekiContract = class {
883
+ constructor() {
884
+ this.description = {
885
+ displayName: "Ceki Contract",
886
+ name: "cekiContract",
887
+ icon: "file:ceki.png",
888
+ group: ["transform"],
889
+ version: 1,
890
+ subtitle: '={{ "Contract: " + $operation }}',
891
+ description: "Work with Ceki contract tasks: list, create, assign, update status, comment, report progress, escalate to a human, and poll",
892
+ defaults: { name: "Ceki Contract" },
893
+ inputs: ["main"],
894
+ outputs: ["main"],
895
+ credentials: [{ name: "cekiApi", required: true }],
896
+ properties: [
897
+ {
898
+ displayName: "Operation",
899
+ name: "operation",
900
+ type: "options",
901
+ default: "myEvents",
902
+ options: [
903
+ { name: "List My Contracts", value: "listContracts" },
904
+ { name: "List Tasks in Contract", value: "listTasks" },
905
+ { name: "Get Task", value: "getTask" },
906
+ { name: "My Assigned Events", value: "myEvents" },
907
+ { name: "Create Task", value: "createTask" },
908
+ { name: "Assign Executor", value: "assign" },
909
+ { name: "Update Status", value: "setStatus" },
910
+ { name: "Comment", value: "comment" },
911
+ { name: "Progress Report", value: "progress" },
912
+ { name: "Call Human", value: "callHuman" },
913
+ { name: "Poll Notifications", value: "poll" }
914
+ ]
915
+ },
916
+ // --- contractId / eventId ---
917
+ {
918
+ displayName: "Contract ID",
919
+ name: "contractId",
920
+ type: "number",
921
+ default: 0,
922
+ description: "ceki contract id",
923
+ displayOptions: { show: { operation: ["listTasks", "createTask"] } }
924
+ },
925
+ {
926
+ displayName: "Event ID",
927
+ name: "eventId",
928
+ type: "number",
929
+ default: 0,
930
+ description: "Task / event id (KalEvent)",
931
+ displayOptions: {
932
+ show: { operation: ["getTask", "assign", "setStatus", "comment", "progress", "callHuman"] }
933
+ }
934
+ },
935
+ // --- createTask fields ---
936
+ {
937
+ displayName: "Label",
938
+ name: "label",
939
+ type: "string",
940
+ default: "",
941
+ required: true,
942
+ displayOptions: { show: { operation: ["createTask"] } }
943
+ },
944
+ {
945
+ displayName: "Description",
946
+ name: "description",
947
+ type: "string",
948
+ typeOptions: { rows: 4 },
949
+ default: "",
950
+ displayOptions: { show: { operation: ["createTask", "comment"] } }
951
+ },
952
+ {
953
+ displayName: "Executor (benefitable)",
954
+ name: "benefitableType",
955
+ type: "options",
956
+ default: "agent",
957
+ options: [
958
+ { name: "Agent", value: "agent" },
959
+ { name: "User (human)", value: "user" }
960
+ ],
961
+ displayOptions: { show: { operation: ["createTask", "assign"] } }
962
+ },
963
+ {
964
+ displayName: "Executor ID",
965
+ name: "benefitableValue",
966
+ type: "number",
967
+ default: 0,
968
+ description: "Agent ID or user ID of the executor",
969
+ displayOptions: { show: { operation: ["createTask", "assign"] } }
970
+ },
971
+ // --- status ---
972
+ {
973
+ displayName: "Status",
974
+ name: "status",
975
+ type: "options",
976
+ options: STATUS_OPTIONS,
977
+ default: 200,
978
+ displayOptions: { show: { operation: ["createTask", "setStatus", "progress"] } }
979
+ },
980
+ // --- progress desc ---
981
+ {
982
+ displayName: "Progress Description",
983
+ name: "progressDesc",
984
+ type: "string",
985
+ typeOptions: { rows: 4 },
986
+ default: "",
987
+ required: true,
988
+ description: "Body of the progress comment (does not overwrite the task spec)",
989
+ displayOptions: { show: { operation: ["progress"] } }
990
+ },
991
+ // --- call human (escalate) ---
992
+ {
993
+ displayName: "Call Kind",
994
+ name: "callKind",
995
+ type: "options",
996
+ default: "review",
997
+ options: [
998
+ { name: "Input (need clarification)", value: "input" },
999
+ { name: "Review (done, take a look)", value: "review" },
1000
+ { name: "Stuck (blocked)", value: "stuck" }
1001
+ ],
1002
+ description: "Type of escalation to a human (the call-human action)",
1003
+ displayOptions: { show: { operation: ["callHuman"] } }
1004
+ },
1005
+ {
1006
+ displayName: "Message",
1007
+ name: "callDesc",
1008
+ type: "string",
1009
+ typeOptions: { rows: 4 },
1010
+ default: "",
1011
+ required: true,
1012
+ description: "What to tell the human \u2014 context, question, or what was done",
1013
+ displayOptions: { show: { operation: ["callHuman"] } }
1014
+ }
1015
+ ]
1016
+ };
1017
+ }
1018
+ async execute() {
1019
+ const items = this.getInputData();
1020
+ const out = [];
1021
+ const creds = await this.getCredentials("cekiApi");
1022
+ const token = creds.token;
1023
+ const client = new ContractClient(token);
1024
+ for (let i = 0; i < items.length; i++) {
1025
+ const op = this.getNodeParameter("operation", i);
1026
+ let result;
1027
+ switch (op) {
1028
+ case "listContracts":
1029
+ result = await client.listContracts();
1030
+ break;
1031
+ case "listTasks": {
1032
+ const contractId = this.getNodeParameter("contractId", i);
1033
+ result = await client.tasks(contractId);
1034
+ break;
1035
+ }
1036
+ case "getTask": {
1037
+ const eventId = this.getNodeParameter("eventId", i);
1038
+ result = await client.task(eventId);
1039
+ break;
1040
+ }
1041
+ case "myEvents":
1042
+ result = await client.myEvents();
1043
+ break;
1044
+ case "createTask": {
1045
+ const contractId = this.getNodeParameter("contractId", i);
1046
+ const label = this.getNodeParameter("label", i);
1047
+ const description = this.getNodeParameter("description", i) || "";
1048
+ const status = this.getNodeParameter("status", i);
1049
+ const bType = this.getNodeParameter("benefitableType", i);
1050
+ const bValue = this.getNodeParameter("benefitableValue", i);
1051
+ result = await client.create(contractId, {
1052
+ label,
1053
+ description: description || void 0,
1054
+ status,
1055
+ benefitable: bValue ? `${bType}:${bValue}` : void 0
1056
+ });
1057
+ break;
1058
+ }
1059
+ case "assign": {
1060
+ const eventId = this.getNodeParameter("eventId", i);
1061
+ const bType = this.getNodeParameter("benefitableType", i);
1062
+ const bValue = this.getNodeParameter("benefitableValue", i);
1063
+ if (!bValue) throw new Error("Executor ID is required for Assign");
1064
+ result = await client.propose(eventId, { benefitable: `${bType}:${bValue}` });
1065
+ break;
1066
+ }
1067
+ case "setStatus": {
1068
+ const eventId = this.getNodeParameter("eventId", i);
1069
+ const status = this.getNodeParameter("status", i);
1070
+ result = await client.propose(eventId, { status });
1071
+ break;
1072
+ }
1073
+ case "comment": {
1074
+ const eventId = this.getNodeParameter("eventId", i);
1075
+ const description = this.getNodeParameter("description", i) || "";
1076
+ if (!description) throw new Error("Comment text is required");
1077
+ result = await client.comment(eventId, { description });
1078
+ break;
1079
+ }
1080
+ case "progress": {
1081
+ const eventId = this.getNodeParameter("eventId", i);
1082
+ const status = this.getNodeParameter("status", i);
1083
+ const desc = this.getNodeParameter("progressDesc", i);
1084
+ result = await client.progress(eventId, { status, desc });
1085
+ break;
1086
+ }
1087
+ case "callHuman": {
1088
+ const eventId = this.getNodeParameter("eventId", i);
1089
+ const kind = this.getNodeParameter("callKind", i);
1090
+ const desc = this.getNodeParameter("callDesc", i);
1091
+ if (!desc) throw new Error("Message is required for Call Human");
1092
+ result = await client.callHuman(eventId, kind, desc);
1093
+ break;
1094
+ }
1095
+ case "poll":
1096
+ result = await client.poll();
1097
+ break;
1098
+ default:
1099
+ throw new Error(`Unknown operation: ${op}`);
1100
+ }
1101
+ out.push({ json: { op, result } });
1102
+ }
1103
+ return [out];
1104
+ }
1105
+ };
1106
+
1107
+ // nodes/recipes/CekiCaptchaScrape/CekiCaptchaScrape.node.ts
1108
+ var sleep2 = (ms) => new Promise((resolve) => {
1109
+ AbortSignal.timeout(ms).addEventListener("abort", () => resolve(), { once: true });
1110
+ });
1111
+ async function waitForSelector(browser, selector, timeoutMs, intervalMs = 500) {
1112
+ const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
1113
+ const deadline = Date.now() + timeoutMs;
1114
+ let lastErr = null;
1115
+ while (Date.now() < deadline) {
1116
+ try {
1117
+ const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
1118
+ if (res?.result?.value === true) return true;
1119
+ } catch (e) {
1120
+ lastErr = e;
1121
+ }
1122
+ await sleep2(intervalMs);
1123
+ }
1124
+ throw new Error(
1125
+ `waitForSelector("${selector}") timed out after ${timeoutMs}ms${lastErr ? `: ${lastErr.message}` : ""}`
1126
+ );
1127
+ }
1128
+ async function extractHtml(browser, selector) {
1129
+ const expr = selector.trim() === "" || selector === "body" ? `document.body ? document.body.outerHTML : ''` : `(function(){ var el = document.querySelector(${JSON.stringify(selector)}); return el ? el.outerHTML : ''; })()`;
1130
+ const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
1131
+ return res?.result?.value ?? "";
1132
+ }
1133
+ var CekiCaptchaScrape = class {
1134
+ constructor() {
1135
+ this.description = {
1136
+ displayName: "Browser Ceki: Captcha-protected Scrape",
1137
+ name: "cekiCaptchaScrape",
1138
+ description: "Rent a human browser, wait, screenshot/HTML, release",
1139
+ icon: "file:ceki.png",
1140
+ group: ["transform"],
1141
+ version: 1,
1142
+ subtitle: "=rent in {{ $geo }} \u2192 snapshot",
1143
+ defaults: { name: "Browser Ceki: Captcha-protected Scrape" },
1144
+ inputs: ["main"],
1145
+ outputs: ["main"],
1146
+ credentials: [{ name: "cekiApi", required: true }],
1147
+ properties: [
1148
+ {
1149
+ displayName: "URL",
1150
+ name: "url",
1151
+ type: "string",
1152
+ default: "https://example.com",
1153
+ required: true,
1154
+ description: "Page protected by anti-bot / captcha"
1155
+ },
1156
+ {
1157
+ displayName: "Geo",
1158
+ name: "geo",
1159
+ type: "string",
1160
+ default: "RU",
1161
+ placeholder: "RU, EE, US\u2026"
1162
+ },
1163
+ { displayName: "Max $/min", name: "maxPrice", type: "number", typeOptions: { numberPrecision: 4 }, default: 0.02 },
1164
+ {
1165
+ displayName: "Wait for Selector",
1166
+ name: "waitSelector",
1167
+ type: "string",
1168
+ default: "",
1169
+ placeholder: "CSS selector (optional)",
1170
+ description: "Wait until this selector appears in the DOM"
1171
+ },
1172
+ {
1173
+ displayName: "Wait Timeout (ms)",
1174
+ name: "waitTimeout",
1175
+ type: "number",
1176
+ default: 3e4
1177
+ },
1178
+ {
1179
+ displayName: "Extract HTML",
1180
+ name: "extractHtml",
1181
+ type: "boolean",
1182
+ default: true
1183
+ },
1184
+ {
1185
+ displayName: "HTML Selector",
1186
+ name: "htmlSelector",
1187
+ type: "string",
1188
+ default: "body",
1189
+ placeholder: 'CSS selector or "body"',
1190
+ description: "outerHTML of this selector is returned as `html`",
1191
+ displayOptions: { show: { extractHtml: [true] } }
1192
+ },
1193
+ {
1194
+ displayName: "Full Page Screenshot",
1195
+ name: "fullPage",
1196
+ type: "boolean",
1197
+ default: false
1198
+ }
1199
+ ]
1200
+ };
1201
+ }
1202
+ async execute() {
1203
+ const items = this.getInputData();
1204
+ const out = [];
1205
+ const creds = await this.getCredentials("cekiApi");
1206
+ for (let i = 0; i < items.length; i++) {
1207
+ const url = this.getNodeParameter("url", i);
1208
+ const geo = this.getNodeParameter("geo", i);
1209
+ const maxPrice = this.getNodeParameter("maxPrice", i);
1210
+ const waitSelector = this.getNodeParameter("waitSelector", i) || "";
1211
+ const waitTimeout = this.getNodeParameter("waitTimeout", i);
1212
+ const extractHtmlFlag = this.getNodeParameter("extractHtml", i);
1213
+ const htmlSelector = this.getNodeParameter("htmlSelector", i) || "body";
1214
+ const fullPage = this.getNodeParameter("fullPage", i);
1215
+ const client = new CekiClient(creds.token);
1216
+ await client.connect();
1217
+ let browser;
1218
+ let scheduleId = 0;
1219
+ try {
1220
+ const list = await client.search({ geo: geo || void 0, max_price_per_min: maxPrice });
1221
+ if (!list.length) throw new Error(`No browsers in geo ${geo || "*"}`);
1222
+ scheduleId = list[0].schedule_id;
1223
+ browser = await client.rent(scheduleId);
1224
+ await browser.navigate(url);
1225
+ if (waitSelector) {
1226
+ await waitForSelector(browser, waitSelector, waitTimeout);
1227
+ }
1228
+ const shot = await browser.screenshot({ format: "base64", fullPage });
1229
+ const data = shot.data ?? (shot instanceof Buffer ? shot.toString("base64") : "");
1230
+ const binary = await this.helpers.prepareBinaryData(
1231
+ Buffer.from(data, "base64"),
1232
+ "captcha-scrape.png",
1233
+ "image/png"
1234
+ );
1235
+ const json = {
1236
+ url,
1237
+ geo,
1238
+ schedule_id: scheduleId
1239
+ };
1240
+ if (extractHtmlFlag) {
1241
+ json.html = await extractHtml(browser, htmlSelector);
1242
+ }
1243
+ out.push({ json, binary: { data: binary } });
1244
+ } finally {
1245
+ if (browser) {
1246
+ try {
1247
+ await browser.close();
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ try {
1252
+ await client.close();
1253
+ } catch {
1254
+ }
1255
+ }
1256
+ }
1257
+ return [out];
1258
+ }
1259
+ };
1260
+
1261
+ // nodes/recipes/CekiScreenshotGeo/CekiScreenshotGeo.node.ts
1262
+ var CekiScreenshotGeo = class {
1263
+ constructor() {
1264
+ this.description = {
1265
+ displayName: "Browser Ceki: Screenshot in Geo",
1266
+ name: "cekiScreenshotGeo",
1267
+ description: "Rent a browser in a given geo, screenshot a page, release",
1268
+ icon: "file:ceki.png",
1269
+ group: ["transform"],
1270
+ version: 1,
1271
+ subtitle: "=rent in {{ $geo }} \u2192 screenshot \u2192 release",
1272
+ defaults: { name: "Browser Ceki: Screenshot in Geo" },
1273
+ inputs: ["main"],
1274
+ outputs: ["main"],
1275
+ credentials: [{ name: "cekiApi", required: true }],
1276
+ properties: [
1277
+ {
1278
+ displayName: "URL",
1279
+ name: "url",
1280
+ type: "string",
1281
+ default: "https://ifconfig.me",
1282
+ required: true
1283
+ },
1284
+ { displayName: "Geo", name: "geo", type: "string", default: "RU", placeholder: "RU, EE, US\u2026" },
1285
+ { displayName: "Full page", name: "fullPage", type: "boolean", default: false },
1286
+ { displayName: "Max $/min", name: "maxPrice", type: "number", default: 0.02 }
1287
+ ]
1288
+ };
1289
+ }
1290
+ async execute() {
1291
+ const items = this.getInputData();
1292
+ const out = [];
1293
+ const creds = await this.getCredentials("cekiApi");
1294
+ for (let i = 0; i < items.length; i++) {
1295
+ const url = this.getNodeParameter("url", i);
1296
+ const geo = this.getNodeParameter("geo", i);
1297
+ const fullPage = this.getNodeParameter("fullPage", i);
1298
+ const maxPrice = this.getNodeParameter("maxPrice", i);
1299
+ const client = new CekiClient(creds.token);
1300
+ await client.connect();
1301
+ let browser;
1302
+ try {
1303
+ const list = await client.search({ geo, max_price_per_min: maxPrice });
1304
+ if (!list.length) throw new Error(`No browsers in geo ${geo}`);
1305
+ browser = await client.rent(list[0].schedule_id);
1306
+ await browser.navigate(url);
1307
+ const shot = await browser.screenshot({ format: "base64", fullPage });
1308
+ const binary = await this.helpers.prepareBinaryData(
1309
+ Buffer.from(shot.data, "base64"),
1310
+ "screenshot.png",
1311
+ "image/png"
1312
+ );
1313
+ out.push({
1314
+ json: { url, geo, schedule_id: list[0].schedule_id },
1315
+ binary: { data: binary }
1316
+ });
1317
+ } finally {
1318
+ if (browser) {
1319
+ try {
1320
+ await browser.close();
1321
+ } catch {
1322
+ }
1323
+ }
1324
+ try {
1325
+ await client.close();
1326
+ } catch {
1327
+ }
1328
+ }
1329
+ }
1330
+ return [out];
1331
+ }
1332
+ };
1333
+
1334
+ // credentials/CekiApi.credentials.ts
1335
+ var CekiApi = class {
1336
+ constructor() {
1337
+ this.name = "cekiApi";
1338
+ this.displayName = "Ceki API";
1339
+ this.documentationUrl = "https://browser.ceki.me/docs#api-key";
1340
+ this.properties = [
1341
+ {
1342
+ displayName: "API Key",
1343
+ name: "token",
1344
+ type: "string",
1345
+ typeOptions: { password: true },
1346
+ default: "",
1347
+ description: "Agent token (ag_...). [Get your API key \u2192](https://browser.ceki.me/docs#api-key)",
1348
+ required: true
1349
+ }
1350
+ ];
1351
+ this.authenticate = {
1352
+ type: "generic",
1353
+ properties: {
1354
+ headers: {
1355
+ Authorization: "=Bearer {{$credentials?.token}}"
1356
+ }
1357
+ }
1358
+ };
1359
+ this.test = {
1360
+ request: {
1361
+ baseURL: "https://api.ceki.me",
1362
+ url: "/api/browsers/search",
1363
+ method: "GET"
1364
+ }
1365
+ };
1366
+ }
1367
+ };
1368
+
1369
+ // index.ts
1370
+ var nodes = [
1371
+ BrowserCeki,
1372
+ CekiContract,
1373
+ CekiCaptchaScrape,
1374
+ CekiScreenshotGeo
1375
+ ];
1376
+ var credentials = [
1377
+ CekiApi
1378
+ ];
1379
+ // Annotate the CommonJS export names for ESM import in node:
1380
+ 0 && (module.exports = {
1381
+ credentials,
1382
+ nodes
1383
+ });
1384
+ //# sourceMappingURL=index.js.map