@ceki/n8n-nodes-ceki 0.2.9 → 0.2.10

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) {
@@ -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"],
@@ -416,7 +472,9 @@ var BrowserCeki = class {
416
472
  { name: "Wait for Selector", value: "waitForSelector" },
417
473
  { name: "Upload", value: "upload" },
418
474
  { name: "Close", value: "close" },
419
- { name: "Full: Rent \u2192 Navigate \u2192 Screenshot", value: "full" }
475
+ { name: "Full: Rent \u2192 Navigate \u2192 Screenshot", value: "full" },
476
+ { name: "My Sessions", value: "my_sessions" },
477
+ { name: "Stop Session", value: "stop_session" }
420
478
  ]
421
479
  },
422
480
  // === Rent: rental parameters ===
@@ -434,7 +492,7 @@ var BrowserCeki = class {
434
492
  type: "string",
435
493
  default: "",
436
494
  placeholder: "RU, EE, US\u2026",
437
- displayOptions: { show: { operation: ["rent", "search"] } }
495
+ displayOptions: { show: { operation: ["search"] } }
438
496
  },
439
497
  {
440
498
  displayName: "Max $/min",
@@ -442,14 +500,14 @@ var BrowserCeki = class {
442
500
  type: "number",
443
501
  typeOptions: { numberPrecision: 4 },
444
502
  default: 0.02,
445
- displayOptions: { show: { operation: ["rent", "search"] } }
503
+ displayOptions: { show: { operation: ["search"] } }
446
504
  },
447
505
  {
448
506
  displayName: "Min rating",
449
507
  name: "minRating",
450
508
  type: "number",
451
509
  default: 0,
452
- displayOptions: { show: { operation: ["rent"] } }
510
+ displayOptions: { show: { operation: ["search"] } }
453
511
  },
454
512
  {
455
513
  displayName: "Profile mode",
@@ -488,7 +546,7 @@ var BrowserCeki = class {
488
546
  required: true,
489
547
  displayOptions: {
490
548
  show: {
491
- operation: ["navigate", "click", "type", "scroll", "screenshot", "snapshot", "wait", "waitForSelector", "upload", "close"]
549
+ operation: ["navigate", "click", "type", "scroll", "screenshot", "snapshot", "wait", "waitForSelector", "upload", "close", "stop_session"]
492
550
  }
493
551
  }
494
552
  },
@@ -610,28 +668,89 @@ var BrowserCeki = class {
610
668
  for (let i = 0; i < items.length; i++) {
611
669
  const op = this.getNodeParameter("operation", i);
612
670
  const client = new CekiClient(token);
613
- await client.connect();
614
671
  let browser;
672
+ let needFullClose = false;
615
673
  try {
674
+ await client.connect();
616
675
  if (op === "rent") {
617
- const sid2 = await resolveSid(i, client);
618
- const mode = this.getNodeParameter("mode", i);
619
- browser = await client.rent(sid2, { mode });
620
- out.push({
621
- json: { session_id: browser.sessionId, schedule_id: sid2, mode }
622
- });
623
- await client.disconnect();
676
+ try {
677
+ const sid2 = await resolveSid(i, client);
678
+ const mode = this.getNodeParameter("mode", i);
679
+ browser = await client.rent(sid2, { mode });
680
+ out.push({
681
+ json: { session_id: browser.sessionId, schedule_id: sid2, mode }
682
+ });
683
+ } catch (e) {
684
+ throw new Error(`Rent failed: ${e.message}`);
685
+ } finally {
686
+ await client.disconnect();
687
+ }
624
688
  continue;
625
689
  }
626
690
  if (op === "search") {
627
- const geo = this.getNodeParameter("geo", i);
628
- const maxPrice = this.getNodeParameter("maxPrice", i);
629
- const list = await client.search({
630
- geo: geo || void 0,
631
- max_price_per_min: maxPrice
632
- });
633
- out.push({ json: { browsers: list, count: list.length } });
634
- await client.disconnect();
691
+ try {
692
+ const geo = this.getNodeParameter("geo", i);
693
+ const maxPrice = this.getNodeParameter("maxPrice", i);
694
+ const list = await client.search({
695
+ geo: geo || void 0,
696
+ max_price_per_min: maxPrice
697
+ });
698
+ out.push({ json: { browsers: list, count: list.length } });
699
+ } catch (e) {
700
+ throw new Error(`Search failed: ${e.message}`);
701
+ } finally {
702
+ await client.disconnect();
703
+ }
704
+ continue;
705
+ }
706
+ if (op === "my_sessions") {
707
+ try {
708
+ const resp = await fetch("https://api.ceki.me/api/agent/sessions", {
709
+ headers: { Authorization: `Bearer ${token}` }
710
+ });
711
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
712
+ const body = await resp.json();
713
+ const sessions = body.data ?? [];
714
+ out.push({ json: { sessions, count: sessions.length } });
715
+ } catch (e) {
716
+ throw new Error(`My sessions failed: ${e.message}`);
717
+ }
718
+ continue;
719
+ }
720
+ if (op === "stop_session") {
721
+ const sessionId2 = this.getNodeParameter("sessionId", i);
722
+ try {
723
+ await new Promise((resolve, reject) => {
724
+ const ws = new WebSocket("wss://browser.ceki.me/ws/agent", [`bearer.${token}`]);
725
+ const t = AbortSignal.timeout(15e3);
726
+ t.addEventListener("abort", () => {
727
+ try {
728
+ ws.close();
729
+ } catch {
730
+ }
731
+ reject(new Error("Stop session timed out"));
732
+ }, { once: true });
733
+ ws.onopen = () => {
734
+ ws.send(JSON.stringify({ type: "stop", session_id: sessionId2, reason: "n8n stop_session" }));
735
+ };
736
+ ws.onmessage = (ev) => {
737
+ const msg = JSON.parse(ev.data);
738
+ if (msg.type === "session_ended" || msg.type === "pong") {
739
+ try {
740
+ ws.close();
741
+ } catch {
742
+ }
743
+ resolve();
744
+ }
745
+ };
746
+ ws.onerror = () => {
747
+ reject(new Error("WebSocket connection failed"));
748
+ };
749
+ });
750
+ out.push({ json: { stopped: true, session_id: sessionId2 } });
751
+ } catch (e) {
752
+ throw new Error(`Stop session failed: ${e.message}`);
753
+ }
635
754
  continue;
636
755
  }
637
756
  if (op === "full") {
@@ -640,7 +759,11 @@ var BrowserCeki = class {
640
759
  if (demoMode) {
641
760
  sid2 = 99999;
642
761
  } else {
643
- sid2 = await resolveSid(i, client);
762
+ try {
763
+ sid2 = await resolveSid(i, client);
764
+ } catch (e) {
765
+ throw new Error(`Full: resolve schedule failed: ${e.message}`);
766
+ }
644
767
  }
645
768
  const mode = this.getNodeParameter("mode", i);
646
769
  const url = this.getNodeParameter("url", i);
@@ -653,14 +776,18 @@ var BrowserCeki = class {
653
776
  const demoPng = Buffer.from("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", "base64");
654
777
  binary = await this.helpers.prepareBinaryData(demoPng, "screenshot.png", "image/png");
655
778
  } else {
656
- browser = await client.rent(sid2, { mode });
657
- session_id = browser.sessionId;
658
- await browser.navigate(url);
659
- await sleep(ms);
660
- const shot = await browser.screenshot({ format: "png", fullPage: true });
661
- const data = shot.toString("base64");
662
- binary = await this.helpers.prepareBinaryData(Buffer.from(data, "base64"), "screenshot.png", "image/png");
663
- await browser.close();
779
+ try {
780
+ browser = await client.rent(sid2, { mode });
781
+ session_id = browser.sessionId;
782
+ await browser.navigate(url);
783
+ await sleep(ms);
784
+ const shot = await browser.screenshot({ format: "png", fullPage: true });
785
+ const data = shot.toString("base64");
786
+ binary = await this.helpers.prepareBinaryData(Buffer.from(data, "base64"), "screenshot.png", "image/png");
787
+ await browser.close();
788
+ } catch (e) {
789
+ throw new Error(`Full op failed at step "${browser?.sessionId ? "navigate/screenshot" : "rent"}": ${e.message}`);
790
+ }
664
791
  }
665
792
  out.push({
666
793
  json: {
@@ -673,57 +800,86 @@ var BrowserCeki = class {
673
800
  binary: { data: binary }
674
801
  });
675
802
  } finally {
676
- await client.close();
803
+ await client.close().catch(() => {
804
+ });
677
805
  }
678
806
  continue;
679
807
  }
680
808
  const sessionId = this.getNodeParameter("sessionId", i);
681
- browser = await client.resume(sessionId);
809
+ try {
810
+ browser = await client.resume(sessionId);
811
+ } catch (e) {
812
+ throw new Error(`Resume session "${sessionId}" failed: ${e.message}. Session may have expired or still be in grace.`);
813
+ }
682
814
  const sid = browser.sessionId;
683
815
  switch (op) {
684
816
  case "navigate": {
685
- const url = this.getNodeParameter("url", i);
686
- await browser.navigate(url);
687
- out.push({ json: { session_id: sid, url } });
817
+ try {
818
+ const url = this.getNodeParameter("url", i);
819
+ await browser.navigate(url);
820
+ out.push({ json: { session_id: sid, url } });
821
+ } catch (e) {
822
+ throw new Error(`Navigate failed: ${e.message}`);
823
+ }
688
824
  break;
689
825
  }
690
826
  case "click": {
691
- const x = this.getNodeParameter("x", i);
692
- const y = this.getNodeParameter("y", i);
693
- await browser.click(x, y);
694
- out.push({ json: { session_id: sid, clicked: [x, y] } });
827
+ try {
828
+ const x2 = this.getNodeParameter("x", i);
829
+ const y2 = this.getNodeParameter("y", i);
830
+ await browser.click(x2, y2);
831
+ out.push({ json: { session_id: sid, clicked: [x2, y2] } });
832
+ } catch (e) {
833
+ throw new Error(`Click at (${x},${y}) failed: ${e.message}`);
834
+ }
695
835
  break;
696
836
  }
697
837
  case "type": {
698
- const text = this.getNodeParameter("text", i);
699
- await browser.type(text);
700
- out.push({ json: { session_id: sid, typed: text } });
838
+ try {
839
+ const text = this.getNodeParameter("text", i);
840
+ await browser.type(text);
841
+ out.push({ json: { session_id: sid, typed: text } });
842
+ } catch (e) {
843
+ throw new Error(`Type failed: ${e.message}`);
844
+ }
701
845
  break;
702
846
  }
703
847
  case "scroll": {
704
- const deltaY = this.getNodeParameter("deltaY", i);
705
- await browser.scroll(deltaY);
706
- out.push({ json: { session_id: sid, scrolled: deltaY } });
848
+ try {
849
+ const deltaY = this.getNodeParameter("deltaY", i);
850
+ await browser.scroll(deltaY);
851
+ out.push({ json: { session_id: sid, scrolled: deltaY } });
852
+ } catch (e) {
853
+ throw new Error(`Scroll failed: ${e.message}`);
854
+ }
707
855
  break;
708
856
  }
709
857
  case "screenshot": {
710
- const format = this.getNodeParameter("format", i);
711
- const fullPage = this.getNodeParameter("fullPage", i);
712
- const shot = await browser.screenshot({ format, fullPage });
713
- const data = format === "base64" ? shot.data : shot.toString("base64");
714
- const binary = await this.helpers.prepareBinaryData(
715
- Buffer.from(data, "base64"),
716
- "screenshot.png",
717
- "image/png"
718
- );
719
- out.push({ json: { session_id: sid }, binary: { data: binary } });
858
+ try {
859
+ const format = this.getNodeParameter("format", i);
860
+ const fullPage = this.getNodeParameter("fullPage", i);
861
+ const shot = await browser.screenshot({ format, fullPage });
862
+ const data = format === "base64" ? shot.data : shot.toString("base64");
863
+ const binary = await this.helpers.prepareBinaryData(
864
+ Buffer.from(data, "base64"),
865
+ "screenshot.png",
866
+ "image/png"
867
+ );
868
+ out.push({ json: { session_id: sid }, binary: { data: binary } });
869
+ } catch (e) {
870
+ throw new Error(`Screenshot failed: ${e.message}`);
871
+ }
720
872
  break;
721
873
  }
722
874
  case "snapshot": {
723
- const snap = await browser.snapshot();
724
- out.push({
725
- json: { session_id: sid, screenshot: snap.screenshot }
726
- });
875
+ try {
876
+ const snap = await browser.snapshot();
877
+ out.push({
878
+ json: { session_id: sid, screenshot: snap.screenshot }
879
+ });
880
+ } catch (e) {
881
+ throw new Error(`Snapshot failed: ${e.message}`);
882
+ }
727
883
  break;
728
884
  }
729
885
  case "wait": {
@@ -760,30 +916,79 @@ var BrowserCeki = class {
760
916
  break;
761
917
  }
762
918
  case "upload": {
763
- const selector = this.getNodeParameter("selector", i);
764
- const bpn = this.getNodeParameter("binaryPropertyName", i);
765
- const bin = items[i].binary?.[bpn];
766
- if (!bin) throw new Error(`Binary property "${bpn}" not found on input`);
767
- const stream = await this.helpers.getBinaryStream(bin.id);
768
- const chunks = [];
769
- for await (const c of stream) chunks.push(c);
770
- const buf = Buffer.concat(chunks);
771
- const res = await browser.upload(selector, buf);
772
- out.push({ json: { session_id: sid, uploaded: res } });
919
+ try {
920
+ const selector = this.getNodeParameter("selector", i);
921
+ const bpn = this.getNodeParameter("binaryPropertyName", i);
922
+ const bin = items[i].binary?.[bpn];
923
+ if (!bin) throw new Error(`Binary property "${bpn}" not found on input`);
924
+ const stream = await this.helpers.getBinaryStream(bin.id);
925
+ const chunks = [];
926
+ for await (const c of stream) chunks.push(c);
927
+ const buf = Buffer.concat(chunks);
928
+ const res = await browser.upload(selector, buf);
929
+ out.push({ json: { session_id: sid, uploaded: res } });
930
+ } catch (e) {
931
+ throw new Error(`Upload failed: ${e.message}`);
932
+ }
773
933
  break;
774
934
  }
775
935
  case "close": {
776
- await browser.close();
936
+ try {
937
+ await browser.close().catch(() => {
938
+ });
939
+ } catch {
940
+ }
941
+ await new Promise((resolve) => {
942
+ const stopWs = new WebSocket("wss://browser.ceki.me/ws/agent", [`bearer.${token}`]);
943
+ const abortTimer = AbortSignal.timeout(1e4);
944
+ const onTimer = () => {
945
+ try {
946
+ stopWs.close();
947
+ } catch {
948
+ }
949
+ resolve();
950
+ };
951
+ abortTimer.addEventListener("abort", onTimer, { once: true });
952
+ stopWs.onopen = () => {
953
+ stopWs.send(JSON.stringify({ type: "stop", session_id: sessionId, reason: "n8n close" }));
954
+ };
955
+ stopWs.onmessage = (ev) => {
956
+ try {
957
+ const msg = JSON.parse(ev.data);
958
+ if (msg.type === "session_ended") {
959
+ abortTimer.removeEventListener("abort", onTimer);
960
+ try {
961
+ stopWs.close();
962
+ } catch {
963
+ }
964
+ resolve();
965
+ }
966
+ } catch {
967
+ }
968
+ };
969
+ stopWs.onerror = () => {
970
+ abortTimer.removeEventListener("abort", onTimer);
971
+ resolve();
972
+ };
973
+ stopWs.onclose = () => {
974
+ abortTimer.removeEventListener("abort", onTimer);
975
+ resolve();
976
+ };
977
+ });
777
978
  out.push({ json: { closed: true, session_id: sessionId } });
778
979
  break;
779
980
  }
780
981
  }
781
- if (op === "close") {
782
- await client.close();
783
- } else {
784
- await client.disconnect();
785
- }
982
+ needFullClose = op === "close";
786
983
  } finally {
984
+ try {
985
+ if (needFullClose) {
986
+ await client.close();
987
+ } else {
988
+ await client.disconnect();
989
+ }
990
+ } catch {
991
+ }
787
992
  }
788
993
  }
789
994
  return [out];