channel-worker 1.0.0 → 1.0.2

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/lib/api-client.js CHANGED
@@ -59,6 +59,15 @@ class ApiClient {
59
59
  const data = await this.request('GET', `/settings/${key}`);
60
60
  return data?.value;
61
61
  }
62
+
63
+ // Commands
64
+ async getNextCommand(workerId) {
65
+ return this.request('GET', `/workers/commands?worker_id=${workerId}`);
66
+ }
67
+
68
+ async updateCommand(commandId, data) {
69
+ return this.request('PUT', `/workers/commands/${commandId}`, data);
70
+ }
62
71
  }
63
72
 
64
73
  module.exports = { ApiClient };
@@ -0,0 +1,109 @@
1
+ const { NstManager } = require('./nst-manager');
2
+
3
+ class CommandPoller {
4
+ constructor(api, config) {
5
+ this.api = api;
6
+ this.config = config;
7
+ this.timer = null;
8
+ this.pollIntervalMs = 3000;
9
+ this.nst = null;
10
+
11
+ // Init Nstbrowser if API key available
12
+ if (config.nst_api_key) {
13
+ try {
14
+ this.nst = new NstManager(config.nst_api_key);
15
+ console.log('[commands] Nstbrowser SDK initialized');
16
+ } catch (err) {
17
+ console.warn(`[commands] Nstbrowser SDK not available: ${err.message}`);
18
+ }
19
+ }
20
+ }
21
+
22
+ start() {
23
+ console.log('[commands] Polling for commands (every 3s)');
24
+ this.poll();
25
+ this.timer = setInterval(() => this.poll(), this.pollIntervalMs);
26
+ }
27
+
28
+ stop() {
29
+ if (this.timer) {
30
+ clearInterval(this.timer);
31
+ this.timer = null;
32
+ }
33
+ }
34
+
35
+ async poll() {
36
+ try {
37
+ const command = await this.api.getNextCommand(this.config.worker_id);
38
+ if (!command) return;
39
+
40
+ console.log(`[commands] Received: ${command.type} (${command._id})`);
41
+
42
+ switch (command.type) {
43
+ case 'launch_profile':
44
+ await this.handleLaunchProfile(command);
45
+ break;
46
+ case 'close_profile':
47
+ await this.handleCloseProfile(command);
48
+ break;
49
+ case 'verify_logins':
50
+ await this.handleVerifyLogins(command);
51
+ break;
52
+ default:
53
+ console.warn(`[commands] Unknown command type: ${command.type}`);
54
+ await this.api.updateCommand(command._id, { status: 'failed', error: 'Unknown command type' });
55
+ }
56
+ } catch (err) {
57
+ if (this.config.verbose) {
58
+ console.error(`[commands] Poll error: ${err.message}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ async handleLaunchProfile(command) {
64
+ const { profile_id } = command.payload || {};
65
+ console.log(`[commands] Launching Nstbrowser profile: ${profile_id}`);
66
+
67
+ try {
68
+ if (!this.nst) {
69
+ // No Nstbrowser SDK — try to get API key from dashboard settings
70
+ const apiKey = await this.api.getSetting('nst_api_key');
71
+ if (apiKey) {
72
+ this.nst = new NstManager(apiKey);
73
+ } else {
74
+ throw new Error('Nstbrowser API key not configured. Set it in Dashboard → Settings.');
75
+ }
76
+ }
77
+
78
+ const result = await this.nst.launchProfile(profile_id);
79
+ console.log(`[commands] Profile ${profile_id} launched — user can now login via RDP`);
80
+
81
+ await this.api.updateCommand(command._id, {
82
+ status: 'done',
83
+ result: { profile_id: result.profileId, launched_at: new Date().toISOString() },
84
+ });
85
+ } catch (err) {
86
+ console.error(`[commands] Failed to launch profile: ${err.message}`);
87
+ await this.api.updateCommand(command._id, {
88
+ status: 'failed',
89
+ error: err.message,
90
+ });
91
+ }
92
+ }
93
+
94
+ async handleCloseProfile(command) {
95
+ const { profile_id } = command.payload || {};
96
+ console.log(`[commands] Closing profile: ${profile_id}`);
97
+ // TODO: close Nstbrowser profile
98
+ await this.api.updateCommand(command._id, { status: 'done' });
99
+ }
100
+
101
+ async handleVerifyLogins(command) {
102
+ const { profile_id, channel_id } = command.payload || {};
103
+ console.log(`[commands] Verifying logins for profile: ${profile_id}`);
104
+ // TODO: launch headless, check cookies for Google/Facebook/TikTok
105
+ await this.api.updateCommand(command._id, { status: 'done' });
106
+ }
107
+ }
108
+
109
+ module.exports = { CommandPoller };
package/lib/daemon.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { ApiClient } = require('./api-client');
2
2
  const { Heartbeat } = require('./heartbeat');
3
3
  const { JobPoller } = require('./job-poller');
4
+ const { CommandPoller } = require('./command-poller');
4
5
  const os = require('os');
5
6
 
6
7
  class Daemon {
@@ -9,6 +10,7 @@ class Daemon {
9
10
  this.api = new ApiClient(config.api_url);
10
11
  this.heartbeat = new Heartbeat(this.api, config.worker_id);
11
12
  this.poller = new JobPoller(this.api, config);
13
+ this.commandPoller = new CommandPoller(this.api, config);
12
14
  }
13
15
 
14
16
  async start() {
@@ -45,13 +47,18 @@ class Daemon {
45
47
  // Start job polling
46
48
  this.poller.start();
47
49
  console.log('[daemon] Job poller started (every 5s)');
48
- console.log('[daemon] Waiting for jobs...\n');
50
+
51
+ // Start command polling
52
+ this.commandPoller.start();
53
+ console.log('[daemon] Command poller started (every 3s)');
54
+ console.log('[daemon] Waiting for jobs & commands...\n');
49
55
 
50
56
  // Graceful shutdown
51
57
  const shutdown = async () => {
52
58
  console.log('\n[daemon] Shutting down...');
53
59
  this.heartbeat.stop();
54
60
  this.poller.stop();
61
+ this.commandPoller.stop();
55
62
 
56
63
  // Mark offline
57
64
  try {
@@ -0,0 +1,119 @@
1
+ let NstBrowserV2;
2
+ try {
3
+ NstBrowserV2 = require('nstbrowser-sdk-node').NstBrowserV2;
4
+ } catch {
5
+ // SDK not installed — will fail gracefully
6
+ }
7
+
8
+ class NstManager {
9
+ constructor(apiKey, options = {}) {
10
+ if (!NstBrowserV2) {
11
+ throw new Error('nstbrowser-sdk-node not installed. Run: npm install -g nstbrowser-sdk-node');
12
+ }
13
+ this.client = new NstBrowserV2(apiKey, {
14
+ timeout: options.timeout || 60000,
15
+ apiAddress: options.apiAddress || 'http://localhost:8848/api/v2',
16
+ });
17
+ }
18
+
19
+ // Check if profile exists by name, return profileId
20
+ async findProfile(name) {
21
+ try {
22
+ const res = await this.client.profiles().getProfiles({ keyword: name });
23
+ const profiles = res?.data?.list || res?.data || [];
24
+ const match = profiles.find(p => p.name === name);
25
+ return match?.profileId || match?.id || null;
26
+ } catch (err) {
27
+ console.error(`[nst] Error finding profile "${name}":`, err.message);
28
+ return null;
29
+ }
30
+ }
31
+
32
+ // Create profile if not exists, return profileId
33
+ async ensureProfile(name) {
34
+ // Check existing
35
+ let profileId = await this.findProfile(name);
36
+ if (profileId) {
37
+ console.log(`[nst] Profile "${name}" exists: ${profileId}`);
38
+ return profileId;
39
+ }
40
+
41
+ // Create new
42
+ console.log(`[nst] Creating profile "${name}"...`);
43
+ const res = await this.client.profiles().createProfile({
44
+ name,
45
+ platform: 'Windows',
46
+ kernelMilestone: '132',
47
+ fingerprint: {
48
+ flags: {
49
+ audio: 'Noise',
50
+ canvas: 'Noise',
51
+ fonts: 'Masked',
52
+ gpu: 'Allow',
53
+ webgl: 'Noise',
54
+ },
55
+ hardwareConcurrency: 8,
56
+ deviceMemory: 8,
57
+ },
58
+ });
59
+
60
+ profileId = res?.data?.profileId || res?.data?.id;
61
+ if (!profileId) throw new Error('Failed to create profile — no ID returned');
62
+
63
+ console.log(`[nst] Profile "${name}" created: ${profileId}`);
64
+ return profileId;
65
+ }
66
+
67
+ // Launch browser for profile (visible, not headless)
68
+ async launchProfile(profileIdOrName) {
69
+ // If it looks like a name (not UUID), find/create by name
70
+ let profileId = profileIdOrName;
71
+ if (!this.isUUID(profileIdOrName)) {
72
+ profileId = await this.ensureProfile(profileIdOrName);
73
+ }
74
+
75
+ console.log(`[nst] Starting browser for profile: ${profileId}`);
76
+ const res = await this.client.browsers().startBrowser(profileId);
77
+ console.log(`[nst] Browser started`);
78
+ return { profileId, response: res };
79
+ }
80
+
81
+ // Launch and get CDP WebSocket URL (for automation)
82
+ async connectProfile(profileIdOrName) {
83
+ let profileId = profileIdOrName;
84
+ if (!this.isUUID(profileIdOrName)) {
85
+ profileId = await this.ensureProfile(profileIdOrName);
86
+ }
87
+
88
+ console.log(`[nst] Connecting to profile: ${profileId}`);
89
+ const res = await this.client.cdpEndpoints().connectBrowser(profileId, {
90
+ headless: false,
91
+ autoClose: false,
92
+ });
93
+
94
+ const wsEndpoint = res?.data?.webSocketDebuggerUrl;
95
+ if (!wsEndpoint) throw new Error('No WebSocket endpoint returned');
96
+
97
+ console.log(`[nst] Connected — WS: ${wsEndpoint}`);
98
+ return { profileId, wsEndpoint };
99
+ }
100
+
101
+ // Stop browser for profile
102
+ async stopProfile(profileIdOrName) {
103
+ let profileId = profileIdOrName;
104
+ if (!this.isUUID(profileIdOrName)) {
105
+ profileId = await this.findProfile(profileIdOrName);
106
+ if (!profileId) return;
107
+ }
108
+
109
+ console.log(`[nst] Stopping browser for profile: ${profileId}`);
110
+ await this.client.browsers().stopBrowser(profileId);
111
+ console.log(`[nst] Browser stopped`);
112
+ }
113
+
114
+ isUUID(str) {
115
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
116
+ }
117
+ }
118
+
119
+ module.exports = { NstManager };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "dev": "node bin/cli.js start --verbose"
16
16
  },
17
17
  "dependencies": {
18
- "node-fetch": "^3.3.2"
18
+ "node-fetch": "^3.3.2",
19
+ "nstbrowser-sdk-node": "^0.1.1"
19
20
  },
20
21
  "engines": {
21
22
  "node": ">=18"