@ceki/n8n-nodes-ceki 0.2.9 → 0.2.11

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