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