@ceki/n8n-nodes-ceki 0.2.30 → 0.2.31
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 +71 -69
- package/dist/index.js.map +2 -2
- package/dist/nodes/BrowserCeki/BrowserCeki.node.js +71 -69
- package/dist/nodes/BrowserCeki/BrowserCeki.node.js.map +2 -2
- package/package.json +1 -1
- package/dist/nodes/CekiContract/CekiContract.node.js +0 -415
- package/dist/nodes/CekiContract/CekiContract.node.js.map +0 -7
- package/dist/nodes/CekiContract/ceki-dark.svg +0 -9
- package/dist/nodes/CekiContract/ceki-light.svg +0 -9
- package/dist/nodes/CekiContract/ceki.png +0 -0
- package/dist/nodes/recipes/CekiCaptchaScrape/CekiCaptchaScrape.node.js +0 -595
- package/dist/nodes/recipes/CekiCaptchaScrape/CekiCaptchaScrape.node.js.map +0 -7
- package/dist/nodes/recipes/CekiCaptchaScrape/ceki-dark.svg +0 -9
- package/dist/nodes/recipes/CekiCaptchaScrape/ceki-light.svg +0 -9
- package/dist/nodes/recipes/CekiCaptchaScrape/ceki.png +0 -0
- package/dist/nodes/recipes/CekiScreenshotGeo/CekiScreenshotGeo.node.js +0 -514
- package/dist/nodes/recipes/CekiScreenshotGeo/CekiScreenshotGeo.node.js.map +0 -7
- package/dist/nodes/recipes/CekiScreenshotGeo/ceki-dark.svg +0 -9
- package/dist/nodes/recipes/CekiScreenshotGeo/ceki-light.svg +0 -9
- package/dist/nodes/recipes/CekiScreenshotGeo/ceki.png +0 -0
- package/dist/nodes/recipes/ceki-dark.svg +0 -9
- package/dist/nodes/recipes/ceki-light.svg +0 -9
- package/dist/nodes/recipes/ceki.png +0 -0
|
@@ -1,595 +0,0 @@
|
|
|
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
|
-
// nodes/recipes/CekiCaptchaScrape/CekiCaptchaScrape.node.ts
|
|
21
|
-
var CekiCaptchaScrape_node_exports = {};
|
|
22
|
-
__export(CekiCaptchaScrape_node_exports, {
|
|
23
|
-
CekiCaptchaScrape: () => CekiCaptchaScrape
|
|
24
|
-
});
|
|
25
|
-
module.exports = __toCommonJS(CekiCaptchaScrape_node_exports);
|
|
26
|
-
|
|
27
|
-
// lib/ceki-client.ts
|
|
28
|
-
function jsonParse(raw) {
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(raw);
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
var CekiClient = class {
|
|
36
|
-
constructor(_token, _relayUrl = "wss://browser.ceki.me/ws/agent", _apiUrl = "https://api.ceki.me") {
|
|
37
|
-
this._token = _token;
|
|
38
|
-
this._relayUrl = _relayUrl;
|
|
39
|
-
this._apiUrl = _apiUrl;
|
|
40
|
-
this._ws = null;
|
|
41
|
-
this._connected = false;
|
|
42
|
-
this._pendingRents = /* @__PURE__ */ new Map();
|
|
43
|
-
this._pendingResumes = /* @__PURE__ */ new Map();
|
|
44
|
-
this._pendingCdp = /* @__PURE__ */ new Map();
|
|
45
|
-
this._cdpCounter = 1;
|
|
46
|
-
this._activeSessions = /* @__PURE__ */ new Map();
|
|
47
|
-
this._connectReject = null;
|
|
48
|
-
this._closed = false;
|
|
49
|
-
}
|
|
50
|
-
/** Connect to the relay WebSocket. */
|
|
51
|
-
async connect() {
|
|
52
|
-
if (this._connected) return;
|
|
53
|
-
const protocols = [`bearer.${this._token}`];
|
|
54
|
-
this._ws = new WebSocket(this._relayUrl, protocols);
|
|
55
|
-
this._ws.onopen = () => {
|
|
56
|
-
this._connected = true;
|
|
57
|
-
};
|
|
58
|
-
this._ws.onmessage = (ev) => {
|
|
59
|
-
this._handleMessage(ev.data);
|
|
60
|
-
};
|
|
61
|
-
this._ws.onclose = (ev) => {
|
|
62
|
-
this._connected = false;
|
|
63
|
-
if ((ev.code === 4401 || ev.code === 4403) && this._connectReject) {
|
|
64
|
-
this._connectReject(new Error(`Auth failed: ${ev.reason || String(ev.code)}`));
|
|
65
|
-
this._connectReject = null;
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
this._ws.onerror = () => {
|
|
69
|
-
if (!this._connected && this._connectReject) {
|
|
70
|
-
this._connectReject(new Error("WebSocket connection failed"));
|
|
71
|
-
this._connectReject = null;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
if (this._ws.readyState === WebSocket.CONNECTING) {
|
|
75
|
-
await new Promise((resolve, reject) => {
|
|
76
|
-
if (!this._ws) {
|
|
77
|
-
reject(new Error("No WebSocket"));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
this._connectReject = reject;
|
|
81
|
-
this._ws.onopen = () => {
|
|
82
|
-
this._connected = true;
|
|
83
|
-
resolve();
|
|
84
|
-
};
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
_sendRaw(msg) {
|
|
89
|
-
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
90
|
-
throw new Error("WebSocket not connected");
|
|
91
|
-
}
|
|
92
|
-
this._ws.send(JSON.stringify(msg));
|
|
93
|
-
}
|
|
94
|
-
/** Search for available browser providers (HTTP GET). */
|
|
95
|
-
async search(filters, limit) {
|
|
96
|
-
const params = new URLSearchParams();
|
|
97
|
-
if (limit != null) params.set("limit", String(limit));
|
|
98
|
-
if (filters) {
|
|
99
|
-
for (const [k, v] of Object.entries(filters)) {
|
|
100
|
-
if (v != null) params.set(k, String(v));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const resp = await fetch(`${this._apiUrl}/api/browsers/search?${params}`, {
|
|
104
|
-
headers: { Authorization: `Bearer ${this._token}` }
|
|
105
|
-
});
|
|
106
|
-
if (!resp.ok) throw new Error(`Search failed: ${resp.status}`);
|
|
107
|
-
const body = await resp.json();
|
|
108
|
-
const data = body.data ?? body;
|
|
109
|
-
return Array.isArray(data) ? data : [];
|
|
110
|
-
}
|
|
111
|
-
/** Rent a browser by schedule_id. */
|
|
112
|
-
async rent(scheduleId, opts) {
|
|
113
|
-
const msg = { type: "rent", browser_id: scheduleId };
|
|
114
|
-
if (opts?.mode) msg.mode = opts.mode;
|
|
115
|
-
this._sendRaw(msg);
|
|
116
|
-
return this._awaitRent(`rent:${scheduleId}`, scheduleId, 9e4);
|
|
117
|
-
}
|
|
118
|
-
/** Resume an existing session. */
|
|
119
|
-
async resume(sessionId) {
|
|
120
|
-
this._sendRaw({ type: "resume", session_id: sessionId });
|
|
121
|
-
return this._awaitResume(sessionId);
|
|
122
|
-
}
|
|
123
|
-
/** Close the WS connection — session stays alive in grace.
|
|
124
|
-
* Waits for the close handshake so the relay processes the close
|
|
125
|
-
* before a new connection with the same renterId is established
|
|
126
|
-
* (avoids the close-handler race condition).
|
|
127
|
-
*/
|
|
128
|
-
async disconnect() {
|
|
129
|
-
this._closed = true;
|
|
130
|
-
this._activeSessions.clear();
|
|
131
|
-
this._pendingRents.clear();
|
|
132
|
-
this._pendingResumes.clear();
|
|
133
|
-
if (!this._ws) return;
|
|
134
|
-
const ws = this._ws;
|
|
135
|
-
if (ws.readyState === WebSocket.CLOSED) {
|
|
136
|
-
this._ws = null;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (ws.readyState === WebSocket.CLOSING) {
|
|
140
|
-
await new Promise((r) => {
|
|
141
|
-
ws.onclose = () => {
|
|
142
|
-
this._connected = false;
|
|
143
|
-
r();
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
this._ws = null;
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
await new Promise((r) => {
|
|
150
|
-
const t = setTimeout(() => {
|
|
151
|
-
r();
|
|
152
|
-
}, 5e3);
|
|
153
|
-
ws.onclose = () => {
|
|
154
|
-
this._connected = false;
|
|
155
|
-
clearTimeout(t);
|
|
156
|
-
r();
|
|
157
|
-
};
|
|
158
|
-
try {
|
|
159
|
-
ws.close();
|
|
160
|
-
} catch {
|
|
161
|
-
clearTimeout(t);
|
|
162
|
-
r();
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
this._ws = null;
|
|
166
|
-
}
|
|
167
|
-
/** Close everything. */
|
|
168
|
-
async close() {
|
|
169
|
-
await this.disconnect();
|
|
170
|
-
}
|
|
171
|
-
// ── private ────────────────────────────────────────────────
|
|
172
|
-
_awaitRent(key, scheduleId, timeoutMs) {
|
|
173
|
-
return new Promise((resolve, reject) => {
|
|
174
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
175
|
-
timeoutSignal.addEventListener("abort", () => {
|
|
176
|
-
this._pendingRents.delete(key);
|
|
177
|
-
reject(new Error("Rent timed out"));
|
|
178
|
-
}, { once: true });
|
|
179
|
-
this._pendingRents.set(key, {
|
|
180
|
-
resolve: (match) => {
|
|
181
|
-
const browser = new CekiBrowser(this, match);
|
|
182
|
-
this._activeSessions.set(browser.sessionId, browser);
|
|
183
|
-
resolve(browser);
|
|
184
|
-
},
|
|
185
|
-
reject: (err) => {
|
|
186
|
-
reject(err);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
_awaitResume(sessionId) {
|
|
192
|
-
return new Promise((resolve, reject) => {
|
|
193
|
-
const timeoutSignal = AbortSignal.timeout(6e4);
|
|
194
|
-
timeoutSignal.addEventListener("abort", () => {
|
|
195
|
-
this._pendingResumes.delete(sessionId);
|
|
196
|
-
reject(new Error("Resume timed out"));
|
|
197
|
-
}, { once: true });
|
|
198
|
-
this._pendingResumes.set(sessionId, {
|
|
199
|
-
resolve: (match) => {
|
|
200
|
-
const browser = new CekiBrowser(this, match);
|
|
201
|
-
this._activeSessions.set(browser.sessionId, browser);
|
|
202
|
-
resolve(browser);
|
|
203
|
-
},
|
|
204
|
-
reject: (err) => {
|
|
205
|
-
reject(err);
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
_closeWs() {
|
|
211
|
-
if (this._ws) {
|
|
212
|
-
try {
|
|
213
|
-
this._ws.onopen = null;
|
|
214
|
-
this._ws.onmessage = null;
|
|
215
|
-
this._ws.onclose = null;
|
|
216
|
-
this._ws.onerror = null;
|
|
217
|
-
this._ws.close();
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
this._ws = null;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
_handleMessage(data) {
|
|
224
|
-
const msg = jsonParse(String(data));
|
|
225
|
-
if (!msg || typeof msg !== "object") return;
|
|
226
|
-
const type = String(msg.type ?? "");
|
|
227
|
-
const sid = msg.session_id ? String(msg.session_id) : null;
|
|
228
|
-
switch (type) {
|
|
229
|
-
case "pong":
|
|
230
|
-
case "rent_pending":
|
|
231
|
-
break;
|
|
232
|
-
case "match":
|
|
233
|
-
this._onMatch(msg);
|
|
234
|
-
break;
|
|
235
|
-
case "rent.error":
|
|
236
|
-
this._onRentError(msg);
|
|
237
|
-
break;
|
|
238
|
-
case "resume_ok":
|
|
239
|
-
this._onResumeOk(msg);
|
|
240
|
-
break;
|
|
241
|
-
case "resume_failed":
|
|
242
|
-
this._onResumeFailed(msg);
|
|
243
|
-
break;
|
|
244
|
-
case "cdp_response":
|
|
245
|
-
if (sid) this._onCdpResponse(sid, msg);
|
|
246
|
-
break;
|
|
247
|
-
case "session.ended":
|
|
248
|
-
if (sid) {
|
|
249
|
-
this._activeSessions.delete(sid);
|
|
250
|
-
const b = this._activeSessions.get(sid);
|
|
251
|
-
if (b) b._ended = String(msg.reason ?? "ended");
|
|
252
|
-
}
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
_onMatch(msg) {
|
|
257
|
-
const scheduleId = Number(msg.schedule_id ?? 0);
|
|
258
|
-
const eventId = msg.event_id ? String(msg.event_id) : null;
|
|
259
|
-
const sessionId = String(msg.session_id ?? "");
|
|
260
|
-
if (sessionId && this._ws?.readyState === WebSocket.OPEN) {
|
|
261
|
-
try {
|
|
262
|
-
this._ws.send(JSON.stringify({ type: "match_ack", session_id: sessionId }));
|
|
263
|
-
} catch {
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
let pending = this._pendingRents.get(`event:${eventId}`);
|
|
267
|
-
if (!pending) pending = this._pendingRents.get(`rent:${scheduleId}`);
|
|
268
|
-
if (pending) {
|
|
269
|
-
this._pendingRents.delete(`event:${eventId}`);
|
|
270
|
-
this._pendingRents.delete(`rent:${scheduleId}`);
|
|
271
|
-
pending.resolve({
|
|
272
|
-
session_id: sessionId,
|
|
273
|
-
schedule_id: scheduleId,
|
|
274
|
-
event_id: eventId,
|
|
275
|
-
chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
|
|
276
|
-
provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
|
|
277
|
-
browser_info: msg.browser_info ?? {}
|
|
278
|
-
});
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
const resumePending = sessionId ? this._pendingResumes.get(sessionId) : null;
|
|
282
|
-
if (resumePending) {
|
|
283
|
-
this._pendingResumes.delete(sessionId);
|
|
284
|
-
resumePending.resolve({
|
|
285
|
-
session_id: sessionId,
|
|
286
|
-
schedule_id: scheduleId,
|
|
287
|
-
event_id: eventId,
|
|
288
|
-
chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
|
|
289
|
-
provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
|
|
290
|
-
browser_info: msg.browser_info ?? {}
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
_onRentError(msg) {
|
|
295
|
-
const code = String(msg.code ?? "");
|
|
296
|
-
const message = String(msg.message ?? "");
|
|
297
|
-
for (const [key, pending] of this._pendingRents) {
|
|
298
|
-
this._pendingRents.delete(key);
|
|
299
|
-
pending.reject(new Error(message || `Rent error: ${code}`));
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
_onResumeOk(msg) {
|
|
304
|
-
const sessionId = String(msg.session_id ?? "");
|
|
305
|
-
const pending = this._pendingResumes.get(sessionId);
|
|
306
|
-
if (!pending) return;
|
|
307
|
-
this._pendingResumes.delete(sessionId);
|
|
308
|
-
pending.resolve({
|
|
309
|
-
session_id: sessionId,
|
|
310
|
-
schedule_id: Number(msg.schedule_id ?? 0),
|
|
311
|
-
event_id: msg.event_id ? String(msg.event_id) : null,
|
|
312
|
-
chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
|
|
313
|
-
provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
|
|
314
|
-
browser_info: msg.browser_info ?? {}
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
_onResumeFailed(msg) {
|
|
318
|
-
const sessionId = String(msg.session_id ?? "");
|
|
319
|
-
const pending = this._pendingResumes.get(sessionId);
|
|
320
|
-
if (!pending) return;
|
|
321
|
-
this._pendingResumes.delete(sessionId);
|
|
322
|
-
pending.reject(new Error(String(msg.reason ?? "Resume failed")));
|
|
323
|
-
}
|
|
324
|
-
_onCdpResponse(sessionId, msg) {
|
|
325
|
-
const browser = this._activeSessions.get(sessionId);
|
|
326
|
-
if (!browser) return;
|
|
327
|
-
const id = Number(msg.id ?? 0);
|
|
328
|
-
const pending = browser._pendingCdp.get(id);
|
|
329
|
-
if (!pending) return;
|
|
330
|
-
browser._pendingCdp.delete(id);
|
|
331
|
-
if (msg.error) {
|
|
332
|
-
pending.reject(new Error(String(msg.error.message ?? "CDP error")));
|
|
333
|
-
} else {
|
|
334
|
-
pending.resolve(msg.result);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
var CekiBrowser = class {
|
|
339
|
-
constructor(client, match) {
|
|
340
|
-
this._cdpId = 1;
|
|
341
|
-
this._pendingCdp = /* @__PURE__ */ new Map();
|
|
342
|
-
this._client = client;
|
|
343
|
-
this.sessionId = match.session_id;
|
|
344
|
-
this.scheduleId = match.schedule_id;
|
|
345
|
-
this.chatTopicId = match.chat_topic_id ?? null;
|
|
346
|
-
this.browserInfo = match.browser_info ?? {};
|
|
347
|
-
this.providerUserId = match.provider_user_id ?? null;
|
|
348
|
-
}
|
|
349
|
-
/** Send a CDP command. Uses AbortSignal.timeout() (no restricted setTimeout global). */
|
|
350
|
-
async send(method, params, timeoutMs = 3e4) {
|
|
351
|
-
const id = this._cdpId++;
|
|
352
|
-
const msg = { type: "cdp", session_id: this.sessionId, id, method, params: params ?? {} };
|
|
353
|
-
this._client._sendRaw(msg);
|
|
354
|
-
return new Promise((resolve, reject) => {
|
|
355
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
356
|
-
timeoutSignal.addEventListener("abort", () => {
|
|
357
|
-
this._pendingCdp.delete(id);
|
|
358
|
-
reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`));
|
|
359
|
-
}, { once: true });
|
|
360
|
-
this._pendingCdp.set(id, {
|
|
361
|
-
resolve: (v) => {
|
|
362
|
-
resolve(v);
|
|
363
|
-
},
|
|
364
|
-
reject: (e) => {
|
|
365
|
-
reject(e);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
async navigate(url, timeoutMs) {
|
|
371
|
-
await this.send("Page.navigate", { url }, timeoutMs);
|
|
372
|
-
}
|
|
373
|
-
async click(x, y) {
|
|
374
|
-
await this.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
375
|
-
await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
376
|
-
}
|
|
377
|
-
async type(text) {
|
|
378
|
-
await this.send("Ceki.typeText", { text });
|
|
379
|
-
}
|
|
380
|
-
async scroll(deltaY) {
|
|
381
|
-
await this.send("Input.dispatchMouseEvent", { type: "mouseWheel", x: 0, y: 0, deltaX: 0, deltaY });
|
|
382
|
-
}
|
|
383
|
-
async screenshot(opts) {
|
|
384
|
-
const format = opts?.format ?? "base64";
|
|
385
|
-
const fullPage = opts?.fullPage ?? false;
|
|
386
|
-
let clip;
|
|
387
|
-
if (fullPage) {
|
|
388
|
-
const metrics = await this.send("Page.getLayoutMetrics");
|
|
389
|
-
const contentSize = metrics?.contentSize;
|
|
390
|
-
if (contentSize) {
|
|
391
|
-
clip = { x: 0, y: 0, width: Number(contentSize.width ?? 1920), height: Math.min(Number(contentSize.height ?? 1080), 16384), scale: 1 };
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
const result = await this.send("Page.captureScreenshot", { format: "png", ...clip ? { clip } : {} });
|
|
395
|
-
const data = String(result?.data ?? "");
|
|
396
|
-
if (format === "png") {
|
|
397
|
-
return Buffer.from(data, "base64");
|
|
398
|
-
}
|
|
399
|
-
return { data };
|
|
400
|
-
}
|
|
401
|
-
async snapshot() {
|
|
402
|
-
const ssResult = await this.screenshot({ format: "base64" });
|
|
403
|
-
return { screenshot: ssResult.data, ts: /* @__PURE__ */ new Date() };
|
|
404
|
-
}
|
|
405
|
-
async upload(selector, buf, filename = "file") {
|
|
406
|
-
const b64 = buf.toString("base64");
|
|
407
|
-
const size = buf.length;
|
|
408
|
-
const mime = "application/octet-stream";
|
|
409
|
-
const expression = `(function(){
|
|
410
|
-
var input = document.querySelector(${JSON.stringify(selector)});
|
|
411
|
-
if (!input) return JSON.stringify({ok:false,error:'Element not found'});
|
|
412
|
-
var b = atob(${JSON.stringify(b64)});
|
|
413
|
-
var u = new Uint8Array(b.length);
|
|
414
|
-
for (var i = 0; i < b.length; i++) u[i] = b.charCodeAt(i);
|
|
415
|
-
var f = new File([u], ${JSON.stringify(filename)}, {type: ${JSON.stringify(mime)}});
|
|
416
|
-
var dt = new DataTransfer(); dt.items.add(f);
|
|
417
|
-
input.files = dt.files;
|
|
418
|
-
input.dispatchEvent(new Event('change',{bubbles:true}));
|
|
419
|
-
return JSON.stringify({ok:true,filename:${JSON.stringify(filename)},size:${size}});
|
|
420
|
-
})()`;
|
|
421
|
-
const result = await this.send("Runtime.evaluate", { expression, returnByValue: true });
|
|
422
|
-
try {
|
|
423
|
-
await this.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 });
|
|
424
|
-
await this.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 });
|
|
425
|
-
} catch {
|
|
426
|
-
}
|
|
427
|
-
const resultObj = result?.result;
|
|
428
|
-
if (resultObj?.value) return JSON.parse(String(resultObj.value));
|
|
429
|
-
return { ok: true, filename, size };
|
|
430
|
-
}
|
|
431
|
-
async close() {
|
|
432
|
-
await this.send("Ceki.close", {}).catch(() => {
|
|
433
|
-
});
|
|
434
|
-
this._client._activeSessions.delete(this.sessionId);
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// nodes/recipes/CekiCaptchaScrape/CekiCaptchaScrape.node.ts
|
|
439
|
-
var sleep = (ms) => new Promise((resolve) => {
|
|
440
|
-
AbortSignal.timeout(ms).addEventListener("abort", () => resolve(), { once: true });
|
|
441
|
-
});
|
|
442
|
-
async function waitForSelector(browser, selector, timeoutMs, intervalMs = 500) {
|
|
443
|
-
const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
|
|
444
|
-
const deadline = Date.now() + timeoutMs;
|
|
445
|
-
let lastErr = null;
|
|
446
|
-
while (Date.now() < deadline) {
|
|
447
|
-
try {
|
|
448
|
-
const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
|
|
449
|
-
if (res?.result?.value === true) return true;
|
|
450
|
-
} catch (e) {
|
|
451
|
-
lastErr = e;
|
|
452
|
-
}
|
|
453
|
-
await sleep(intervalMs);
|
|
454
|
-
}
|
|
455
|
-
throw new Error(
|
|
456
|
-
`waitForSelector("${selector}") timed out after ${timeoutMs}ms${lastErr ? `: ${lastErr.message}` : ""}`
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
async function extractHtml(browser, selector) {
|
|
460
|
-
const expr = selector.trim() === "" || selector === "body" ? `document.body ? document.body.outerHTML : ''` : `(function(){ var el = document.querySelector(${JSON.stringify(selector)}); return el ? el.outerHTML : ''; })()`;
|
|
461
|
-
const res = await browser.send("Runtime.evaluate", { expression: expr, returnByValue: true });
|
|
462
|
-
return res?.result?.value ?? "";
|
|
463
|
-
}
|
|
464
|
-
var CekiCaptchaScrape = class {
|
|
465
|
-
constructor() {
|
|
466
|
-
this.description = {
|
|
467
|
-
displayName: "Browser Ceki: Captcha-protected Scrape",
|
|
468
|
-
name: "cekiCaptchaScrape",
|
|
469
|
-
description: "Rent a human browser, wait, screenshot/HTML, release",
|
|
470
|
-
icon: "file:ceki.png",
|
|
471
|
-
group: ["transform"],
|
|
472
|
-
version: 1,
|
|
473
|
-
subtitle: "=rent in {{ $geo }} \u2192 snapshot",
|
|
474
|
-
defaults: { name: "Browser Ceki: Captcha-protected Scrape" },
|
|
475
|
-
inputs: ["main"],
|
|
476
|
-
outputs: ["main"],
|
|
477
|
-
credentials: [{ name: "cekiApi", required: true }],
|
|
478
|
-
properties: [
|
|
479
|
-
{
|
|
480
|
-
displayName: "URL",
|
|
481
|
-
name: "url",
|
|
482
|
-
type: "string",
|
|
483
|
-
default: "https://example.com",
|
|
484
|
-
required: true,
|
|
485
|
-
description: "Page protected by anti-bot / captcha"
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
displayName: "Geo",
|
|
489
|
-
name: "geo",
|
|
490
|
-
type: "string",
|
|
491
|
-
default: "RU",
|
|
492
|
-
placeholder: "RU, EE, US\u2026"
|
|
493
|
-
},
|
|
494
|
-
{ displayName: "Max $/min", name: "maxPrice", type: "number", typeOptions: { numberPrecision: 4 }, default: 0.02 },
|
|
495
|
-
{
|
|
496
|
-
displayName: "Wait for Selector",
|
|
497
|
-
name: "waitSelector",
|
|
498
|
-
type: "string",
|
|
499
|
-
default: "",
|
|
500
|
-
placeholder: "CSS selector (optional)",
|
|
501
|
-
description: "Wait until this selector appears in the DOM"
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
displayName: "Wait Timeout (ms)",
|
|
505
|
-
name: "waitTimeout",
|
|
506
|
-
type: "number",
|
|
507
|
-
default: 3e4
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
displayName: "Extract HTML",
|
|
511
|
-
name: "extractHtml",
|
|
512
|
-
type: "boolean",
|
|
513
|
-
default: true
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
displayName: "HTML Selector",
|
|
517
|
-
name: "htmlSelector",
|
|
518
|
-
type: "string",
|
|
519
|
-
default: "body",
|
|
520
|
-
placeholder: 'CSS selector or "body"',
|
|
521
|
-
description: "outerHTML of this selector is returned as `html`",
|
|
522
|
-
displayOptions: { show: { extractHtml: [true] } }
|
|
523
|
-
},
|
|
524
|
-
{
|
|
525
|
-
displayName: "Full Page Screenshot",
|
|
526
|
-
name: "fullPage",
|
|
527
|
-
type: "boolean",
|
|
528
|
-
default: false
|
|
529
|
-
}
|
|
530
|
-
]
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
async execute() {
|
|
534
|
-
const items = this.getInputData();
|
|
535
|
-
const out = [];
|
|
536
|
-
const creds = await this.getCredentials("cekiApi");
|
|
537
|
-
for (let i = 0; i < items.length; i++) {
|
|
538
|
-
const url = this.getNodeParameter("url", i);
|
|
539
|
-
const geo = this.getNodeParameter("geo", i);
|
|
540
|
-
const maxPrice = this.getNodeParameter("maxPrice", i);
|
|
541
|
-
const waitSelector = this.getNodeParameter("waitSelector", i) || "";
|
|
542
|
-
const waitTimeout = this.getNodeParameter("waitTimeout", i);
|
|
543
|
-
const extractHtmlFlag = this.getNodeParameter("extractHtml", i);
|
|
544
|
-
const htmlSelector = this.getNodeParameter("htmlSelector", i) || "body";
|
|
545
|
-
const fullPage = this.getNodeParameter("fullPage", i);
|
|
546
|
-
const client = new CekiClient(creds.token);
|
|
547
|
-
await client.connect();
|
|
548
|
-
let browser;
|
|
549
|
-
let scheduleId = 0;
|
|
550
|
-
try {
|
|
551
|
-
const list = await client.search({ geo: geo || void 0, max_price_per_min: maxPrice });
|
|
552
|
-
if (!list.length) throw new Error(`No browsers in geo ${geo || "*"}`);
|
|
553
|
-
scheduleId = list[0].schedule_id;
|
|
554
|
-
browser = await client.rent(scheduleId);
|
|
555
|
-
await browser.navigate(url);
|
|
556
|
-
if (waitSelector) {
|
|
557
|
-
await waitForSelector(browser, waitSelector, waitTimeout);
|
|
558
|
-
}
|
|
559
|
-
const shot = await browser.screenshot({ format: "base64", fullPage });
|
|
560
|
-
const data = shot.data ?? (shot instanceof Buffer ? shot.toString("base64") : "");
|
|
561
|
-
const binary = await this.helpers.prepareBinaryData(
|
|
562
|
-
Buffer.from(data, "base64"),
|
|
563
|
-
"captcha-scrape.png",
|
|
564
|
-
"image/png"
|
|
565
|
-
);
|
|
566
|
-
const json = {
|
|
567
|
-
url,
|
|
568
|
-
geo,
|
|
569
|
-
schedule_id: scheduleId
|
|
570
|
-
};
|
|
571
|
-
if (extractHtmlFlag) {
|
|
572
|
-
json.html = await extractHtml(browser, htmlSelector);
|
|
573
|
-
}
|
|
574
|
-
out.push({ json, binary: { data: binary } });
|
|
575
|
-
} finally {
|
|
576
|
-
if (browser) {
|
|
577
|
-
try {
|
|
578
|
-
await browser.close();
|
|
579
|
-
} catch {
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
try {
|
|
583
|
-
await client.close();
|
|
584
|
-
} catch {
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return [out];
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
592
|
-
0 && (module.exports = {
|
|
593
|
-
CekiCaptchaScrape
|
|
594
|
-
});
|
|
595
|
-
//# sourceMappingURL=CekiCaptchaScrape.node.js.map
|