@ceki/n8n-nodes-ceki 0.2.7 → 0.2.8

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 CHANGED
@@ -121,17 +121,53 @@ var CekiClient = class {
121
121
  this._sendRaw({ type: "resume", session_id: sessionId });
122
122
  return this._awaitResume(sessionId);
123
123
  }
124
- /** Close the WS connection — session stays alive in grace. */
125
- disconnect() {
124
+ /** Close the WS connection — session stays alive in grace.
125
+ * Waits for the close handshake so the relay processes the close
126
+ * before a new connection with the same renterId is established
127
+ * (avoids the close-handler race condition).
128
+ */
129
+ async disconnect() {
126
130
  this._closed = true;
127
131
  this._activeSessions.clear();
128
132
  this._pendingRents.clear();
129
133
  this._pendingResumes.clear();
130
- this._closeWs();
134
+ if (!this._ws) return;
135
+ const ws = this._ws;
136
+ if (ws.readyState === WebSocket.CLOSED) {
137
+ this._ws = null;
138
+ return;
139
+ }
140
+ if (ws.readyState === WebSocket.CLOSING) {
141
+ await new Promise((r) => {
142
+ ws.onclose = () => {
143
+ this._connected = false;
144
+ r();
145
+ };
146
+ });
147
+ this._ws = null;
148
+ return;
149
+ }
150
+ await new Promise((r) => {
151
+ const t = setTimeout(() => {
152
+ r();
153
+ }, 5e3);
154
+ ws.onclose = () => {
155
+ this._connected = false;
156
+ clearTimeout(t);
157
+ r();
158
+ };
159
+ try {
160
+ ws.close();
161
+ } catch {
162
+ clearTimeout(t);
163
+ r();
164
+ }
165
+ });
166
+ this._ws = null;
131
167
  }
132
168
  /** Close everything. */
133
- close() {
134
- this.disconnect();
169
+ async close() {
170
+ await this.disconnect();
135
171
  }
136
172
  // ── private ────────────────────────────────────────────────
137
173
  _awaitRent(key, scheduleId, timeoutMs) {
@@ -155,7 +191,7 @@ var CekiClient = class {
155
191
  }
156
192
  _awaitResume(sessionId) {
157
193
  return new Promise((resolve, reject) => {
158
- const timeoutSignal = AbortSignal.timeout(1e4);
194
+ const timeoutSignal = AbortSignal.timeout(6e4);
159
195
  timeoutSignal.addEventListener("abort", () => {
160
196
  this._pendingResumes.delete(sessionId);
161
197
  reject(new Error("Resume timed out"));
@@ -221,13 +257,33 @@ var CekiClient = class {
221
257
  _onMatch(msg) {
222
258
  const scheduleId = Number(msg.schedule_id ?? 0);
223
259
  const eventId = msg.event_id ? String(msg.event_id) : null;
260
+ const sessionId = String(msg.session_id ?? "");
261
+ if (sessionId && this._ws?.readyState === WebSocket.OPEN) {
262
+ try {
263
+ this._ws.send(JSON.stringify({ type: "match_ack", session_id: sessionId }));
264
+ } catch {
265
+ }
266
+ }
224
267
  let pending = this._pendingRents.get(`event:${eventId}`);
225
268
  if (!pending) pending = this._pendingRents.get(`rent:${scheduleId}`);
226
269
  if (pending) {
227
270
  this._pendingRents.delete(`event:${eventId}`);
228
271
  this._pendingRents.delete(`rent:${scheduleId}`);
229
272
  pending.resolve({
230
- session_id: String(msg.session_id ?? ""),
273
+ session_id: sessionId,
274
+ schedule_id: scheduleId,
275
+ event_id: eventId,
276
+ chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
277
+ provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
278
+ browser_info: msg.browser_info ?? {}
279
+ });
280
+ return;
281
+ }
282
+ const resumePending = sessionId ? this._pendingResumes.get(sessionId) : null;
283
+ if (resumePending) {
284
+ this._pendingResumes.delete(sessionId);
285
+ resumePending.resolve({
286
+ session_id: sessionId,
231
287
  schedule_id: scheduleId,
232
288
  event_id: eventId,
233
289
  chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
@@ -270,9 +326,9 @@ var CekiClient = class {
270
326
  const browser = this._activeSessions.get(sessionId);
271
327
  if (!browser) return;
272
328
  const id = Number(msg.id ?? 0);
273
- const pending = this._pendingCdp.get(id);
329
+ const pending = browser._pendingCdp.get(id);
274
330
  if (!pending) return;
275
- this._pendingCdp.delete(id);
331
+ browser._pendingCdp.delete(id);
276
332
  if (msg.error) {
277
333
  pending.reject(new Error(String(msg.error.message ?? "CDP error")));
278
334
  } else {
@@ -315,9 +371,9 @@ var CekiBrowser = class {
315
371
  async navigate(url, timeoutMs) {
316
372
  await this.send("Page.navigate", { url }, timeoutMs);
317
373
  }
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 });
374
+ async click(x2, y2) {
375
+ await this.send("Input.dispatchMouseEvent", { type: "mousePressed", x: x2, y: y2, button: "left", clickCount: 1 });
376
+ await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: x2, y: y2, button: "left", clickCount: 1 });
321
377
  }
322
378
  async type(text) {
323
379
  await this.send("Ceki.typeText", { text });
@@ -392,7 +448,7 @@ var BrowserCeki = class {
392
448
  icon: "file:ceki.png",
393
449
  group: ["transform"],
394
450
  version: 1,
395
- subtitle: '={{ "Ceki: " + $operation }}',
451
+ subtitle: '={{ "Ceki: " + $parameter.operation }}',
396
452
  description: "Rent a real human browser and control it: rent, navigate, click, type, screenshot, solve captchas, and more",
397
453
  defaults: { name: "Browser Ceki" },
398
454
  inputs: ["main"],
@@ -403,8 +459,9 @@ var BrowserCeki = class {
403
459
  displayName: "Operation",
404
460
  name: "operation",
405
461
  type: "options",
406
- default: "rent",
462
+ default: "search",
407
463
  options: [
464
+ { name: "Search", value: "search" },
408
465
  { name: "Rent", value: "rent" },
409
466
  { name: "Navigate", value: "navigate" },
410
467
  { name: "Click", value: "click" },
@@ -415,7 +472,8 @@ var BrowserCeki = class {
415
472
  { name: "Wait", value: "wait" },
416
473
  { name: "Wait for Selector", value: "waitForSelector" },
417
474
  { name: "Upload", value: "upload" },
418
- { name: "Close", value: "close" }
475
+ { name: "Close", value: "close" },
476
+ { name: "Full: Rent \u2192 Navigate \u2192 Screenshot", value: "full" }
419
477
  ]
420
478
  },
421
479
  // === Rent: rental parameters ===
@@ -433,7 +491,7 @@ var BrowserCeki = class {
433
491
  type: "string",
434
492
  default: "",
435
493
  placeholder: "RU, EE, US\u2026",
436
- displayOptions: { show: { operation: ["rent"] } }
494
+ displayOptions: { show: { operation: ["search"] } }
437
495
  },
438
496
  {
439
497
  displayName: "Max $/min",
@@ -441,32 +499,48 @@ var BrowserCeki = class {
441
499
  type: "number",
442
500
  typeOptions: { numberPrecision: 4 },
443
501
  default: 0.02,
444
- displayOptions: { show: { operation: ["rent"] } }
502
+ displayOptions: { show: { operation: ["search"] } }
445
503
  },
446
504
  {
447
505
  displayName: "Min rating",
448
506
  name: "minRating",
449
507
  type: "number",
450
508
  default: 0,
451
- displayOptions: { show: { operation: ["rent"] } }
509
+ displayOptions: { show: { operation: ["search"] } }
452
510
  },
453
511
  {
454
512
  displayName: "Profile mode",
455
513
  name: "mode",
456
514
  type: "options",
457
- default: "main",
515
+ default: "incognito",
458
516
  options: [
459
517
  { name: "main", value: "main" },
460
518
  { name: "incognito", value: "incognito" }
461
519
  ],
462
- displayOptions: { show: { operation: ["rent"] } }
520
+ displayOptions: { show: { operation: ["rent", "full"] } }
521
+ },
522
+ {
523
+ displayName: "URL",
524
+ name: "url",
525
+ type: "string",
526
+ default: "https://example.com",
527
+ required: true,
528
+ displayOptions: { show: { operation: ["navigate", "full"] } }
529
+ },
530
+ {
531
+ displayName: "Demo mode (no browser needed)",
532
+ name: "demoMode",
533
+ type: "boolean",
534
+ default: true,
535
+ description: "Skip actual browser rent and generate demo output",
536
+ displayOptions: { show: { operation: ["full"] } }
463
537
  },
464
538
  // === Operations: session_id ===
465
539
  {
466
540
  displayName: "Session ID",
467
541
  name: "sessionId",
468
542
  type: "string",
469
- default: "={{ $json.session_id }}",
543
+ default: "",
470
544
  description: "From the Rent operation",
471
545
  required: true,
472
546
  displayOptions: {
@@ -536,7 +610,7 @@ var BrowserCeki = class {
536
610
  default: 1e3,
537
611
  typeOptions: { minValue: 0 },
538
612
  description: "Fixed delay on the active session",
539
- displayOptions: { show: { operation: ["wait"] } }
613
+ displayOptions: { show: { operation: ["wait", "full"] } }
540
614
  },
541
615
  {
542
616
  displayName: "CSS Selector",
@@ -593,66 +667,168 @@ var BrowserCeki = class {
593
667
  for (let i = 0; i < items.length; i++) {
594
668
  const op = this.getNodeParameter("operation", i);
595
669
  const client = new CekiClient(token);
596
- await client.connect();
597
670
  let browser;
671
+ let needFullClose = false;
598
672
  try {
673
+ await client.connect();
599
674
  if (op === "rent") {
600
- const sid2 = await resolveSid(i, client);
675
+ try {
676
+ const sid2 = await resolveSid(i, client);
677
+ const mode = this.getNodeParameter("mode", i);
678
+ browser = await client.rent(sid2, { mode });
679
+ out.push({
680
+ json: { session_id: browser.sessionId, schedule_id: sid2, mode }
681
+ });
682
+ } catch (e) {
683
+ throw new Error(`Rent failed: ${e.message}`);
684
+ } finally {
685
+ await client.disconnect();
686
+ }
687
+ continue;
688
+ }
689
+ if (op === "search") {
690
+ try {
691
+ const geo = this.getNodeParameter("geo", i);
692
+ const maxPrice = this.getNodeParameter("maxPrice", i);
693
+ const list = await client.search({
694
+ geo: geo || void 0,
695
+ max_price_per_min: maxPrice
696
+ });
697
+ out.push({ json: { browsers: list, count: list.length } });
698
+ } catch (e) {
699
+ throw new Error(`Search failed: ${e.message}`);
700
+ } finally {
701
+ await client.disconnect();
702
+ }
703
+ continue;
704
+ }
705
+ if (op === "full") {
706
+ const demoMode = this.getNodeParameter("demoMode", i);
707
+ let sid2;
708
+ if (demoMode) {
709
+ sid2 = 99999;
710
+ } else {
711
+ try {
712
+ sid2 = await resolveSid(i, client);
713
+ } catch (e) {
714
+ throw new Error(`Full: resolve schedule failed: ${e.message}`);
715
+ }
716
+ }
601
717
  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();
718
+ const url = this.getNodeParameter("url", i);
719
+ const ms = this.getNodeParameter("ms", i);
720
+ let session_id;
721
+ let binary;
722
+ try {
723
+ if (demoMode) {
724
+ session_id = "demo-" + Date.now();
725
+ const demoPng = Buffer.from("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", "base64");
726
+ binary = await this.helpers.prepareBinaryData(demoPng, "screenshot.png", "image/png");
727
+ } else {
728
+ try {
729
+ browser = await client.rent(sid2, { mode });
730
+ session_id = browser.sessionId;
731
+ await browser.navigate(url);
732
+ await sleep(ms);
733
+ const shot = await browser.screenshot({ format: "png", fullPage: true });
734
+ const data = shot.toString("base64");
735
+ binary = await this.helpers.prepareBinaryData(Buffer.from(data, "base64"), "screenshot.png", "image/png");
736
+ await browser.close();
737
+ } catch (e) {
738
+ throw new Error(`Full op failed at step "${browser?.sessionId ? "navigate/screenshot" : "rent"}": ${e.message}`);
739
+ }
740
+ }
741
+ out.push({
742
+ json: {
743
+ session_id,
744
+ schedule_id: sid2,
745
+ mode,
746
+ url,
747
+ waited: ms
748
+ },
749
+ binary: { data: binary }
750
+ });
751
+ } finally {
752
+ await client.close().catch(() => {
753
+ });
754
+ }
607
755
  continue;
608
756
  }
609
757
  const sessionId = this.getNodeParameter("sessionId", i);
610
- browser = await client.resume(sessionId);
758
+ try {
759
+ browser = await client.resume(sessionId);
760
+ } catch (e) {
761
+ throw new Error(`Resume session "${sessionId}" failed: ${e.message}. Session may have expired or still be in grace.`);
762
+ }
611
763
  const sid = browser.sessionId;
612
764
  switch (op) {
613
765
  case "navigate": {
614
- const url = this.getNodeParameter("url", i);
615
- await browser.navigate(url);
616
- out.push({ json: { session_id: sid, url } });
766
+ try {
767
+ const url = this.getNodeParameter("url", i);
768
+ await browser.navigate(url);
769
+ out.push({ json: { session_id: sid, url } });
770
+ } catch (e) {
771
+ throw new Error(`Navigate failed: ${e.message}`);
772
+ }
617
773
  break;
618
774
  }
619
775
  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] } });
776
+ try {
777
+ const x2 = this.getNodeParameter("x", i);
778
+ const y2 = this.getNodeParameter("y", i);
779
+ await browser.click(x2, y2);
780
+ out.push({ json: { session_id: sid, clicked: [x2, y2] } });
781
+ } catch (e) {
782
+ throw new Error(`Click at (${x},${y}) failed: ${e.message}`);
783
+ }
624
784
  break;
625
785
  }
626
786
  case "type": {
627
- const text = this.getNodeParameter("text", i);
628
- await browser.type(text);
629
- out.push({ json: { session_id: sid, typed: text } });
787
+ try {
788
+ const text = this.getNodeParameter("text", i);
789
+ await browser.type(text);
790
+ out.push({ json: { session_id: sid, typed: text } });
791
+ } catch (e) {
792
+ throw new Error(`Type failed: ${e.message}`);
793
+ }
630
794
  break;
631
795
  }
632
796
  case "scroll": {
633
- const deltaY = this.getNodeParameter("deltaY", i);
634
- await browser.scroll(deltaY);
635
- out.push({ json: { session_id: sid, scrolled: deltaY } });
797
+ try {
798
+ const deltaY = this.getNodeParameter("deltaY", i);
799
+ await browser.scroll(deltaY);
800
+ out.push({ json: { session_id: sid, scrolled: deltaY } });
801
+ } catch (e) {
802
+ throw new Error(`Scroll failed: ${e.message}`);
803
+ }
636
804
  break;
637
805
  }
638
806
  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 } });
807
+ try {
808
+ const format = this.getNodeParameter("format", i);
809
+ const fullPage = this.getNodeParameter("fullPage", i);
810
+ const shot = await browser.screenshot({ format, fullPage });
811
+ const data = format === "base64" ? shot.data : shot.toString("base64");
812
+ const binary = await this.helpers.prepareBinaryData(
813
+ Buffer.from(data, "base64"),
814
+ "screenshot.png",
815
+ "image/png"
816
+ );
817
+ out.push({ json: { session_id: sid }, binary: { data: binary } });
818
+ } catch (e) {
819
+ throw new Error(`Screenshot failed: ${e.message}`);
820
+ }
649
821
  break;
650
822
  }
651
823
  case "snapshot": {
652
- const snap = await browser.snapshot();
653
- out.push({
654
- json: { session_id: sid, screenshot: snap.screenshot }
655
- });
824
+ try {
825
+ const snap = await browser.snapshot();
826
+ out.push({
827
+ json: { session_id: sid, screenshot: snap.screenshot }
828
+ });
829
+ } catch (e) {
830
+ throw new Error(`Snapshot failed: ${e.message}`);
831
+ }
656
832
  break;
657
833
  }
658
834
  case "wait": {
@@ -689,30 +865,79 @@ var BrowserCeki = class {
689
865
  break;
690
866
  }
691
867
  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 } });
868
+ try {
869
+ const selector = this.getNodeParameter("selector", i);
870
+ const bpn = this.getNodeParameter("binaryPropertyName", i);
871
+ const bin = items[i].binary?.[bpn];
872
+ if (!bin) throw new Error(`Binary property "${bpn}" not found on input`);
873
+ const stream = await this.helpers.getBinaryStream(bin.id);
874
+ const chunks = [];
875
+ for await (const c of stream) chunks.push(c);
876
+ const buf = Buffer.concat(chunks);
877
+ const res = await browser.upload(selector, buf);
878
+ out.push({ json: { session_id: sid, uploaded: res } });
879
+ } catch (e) {
880
+ throw new Error(`Upload failed: ${e.message}`);
881
+ }
702
882
  break;
703
883
  }
704
884
  case "close": {
705
- await browser.close();
885
+ try {
886
+ await browser.close().catch(() => {
887
+ });
888
+ } catch {
889
+ }
890
+ await new Promise((resolve) => {
891
+ const stopWs = new WebSocket("wss://browser.ceki.me/ws/agent", [`bearer.${token}`]);
892
+ const abortTimer = AbortSignal.timeout(1e4);
893
+ const onTimer = () => {
894
+ try {
895
+ stopWs.close();
896
+ } catch {
897
+ }
898
+ resolve();
899
+ };
900
+ abortTimer.addEventListener("abort", onTimer, { once: true });
901
+ stopWs.onopen = () => {
902
+ stopWs.send(JSON.stringify({ type: "stop", session_id: sessionId, reason: "n8n close" }));
903
+ };
904
+ stopWs.onmessage = (ev) => {
905
+ try {
906
+ const msg = JSON.parse(ev.data);
907
+ if (msg.type === "session_ended") {
908
+ abortTimer.removeEventListener("abort", onTimer);
909
+ try {
910
+ stopWs.close();
911
+ } catch {
912
+ }
913
+ resolve();
914
+ }
915
+ } catch {
916
+ }
917
+ };
918
+ stopWs.onerror = () => {
919
+ abortTimer.removeEventListener("abort", onTimer);
920
+ resolve();
921
+ };
922
+ stopWs.onclose = () => {
923
+ abortTimer.removeEventListener("abort", onTimer);
924
+ resolve();
925
+ };
926
+ });
706
927
  out.push({ json: { closed: true, session_id: sessionId } });
707
928
  break;
708
929
  }
709
930
  }
710
- if (op === "close") {
711
- await client.close();
712
- } else {
713
- await client.disconnect();
714
- }
931
+ needFullClose = op === "close";
715
932
  } finally {
933
+ try {
934
+ if (needFullClose) {
935
+ await client.close();
936
+ } else {
937
+ await client.disconnect();
938
+ }
939
+ } catch {
940
+ }
716
941
  }
717
942
  }
718
943
  return [out];