lobstakit-cloud 1.3.2 → 1.3.3

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.
Files changed (2) hide show
  1. package/lib/gateway.js +165 -108
  2. package/package.json +1 -1
package/lib/gateway.js CHANGED
@@ -1,44 +1,148 @@
1
1
  /**
2
2
  * LobstaKit Cloud — Gateway Manager
3
3
  *
4
- * Controls the openclaw-gateway systemd service.
4
+ * Controls the openclaw-gateway systemd user service.
5
5
  */
6
6
 
7
7
  const { execSync } = require('child_process');
8
8
  const fs = require('fs');
9
+ const http = require('http');
10
+ const path = require('path');
11
+ const os = require('os');
9
12
 
10
13
  const SERVICE_NAME = 'openclaw-gateway';
11
- const DROP_IN_DIR = `/etc/systemd/system/${SERVICE_NAME}.service.d`;
12
- const DROP_IN_FILE = `${DROP_IN_DIR}/dbus-env.conf`;
14
+
15
+ // Paths
16
+ const OPENCLAW_CONFIG = path.join(os.homedir(), '.openclaw', 'openclaw.json');
17
+ const LOBSTAKIT_CONFIG = path.join(os.homedir(), '.lobstakit', 'config.json');
18
+
19
+ // Env vars needed for systemctl --user to work from a headless root session
20
+ function userEnv() {
21
+ const uid = (() => { try { return execSync('id -u', { encoding: 'utf8', timeout: 3000 }).trim(); } catch { return '0'; } })();
22
+ return {
23
+ ...process.env,
24
+ XDG_RUNTIME_DIR: `/run/user/${uid}`,
25
+ DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
26
+ };
27
+ }
28
+
29
+ // ─── Token helpers ────────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Read the gateway auth token from ~/.openclaw/openclaw.json.
33
+ * Returns null if not found.
34
+ */
35
+ function readGatewayToken() {
36
+ try {
37
+ const raw = fs.readFileSync(OPENCLAW_CONFIG, 'utf8');
38
+ const cfg = JSON.parse(raw);
39
+ return cfg?.gateway?.auth?.token || null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Persist the gateway auth token into ~/.lobstakit/config.json
47
+ * so health checks can authenticate without re-reading openclaw.json every time.
48
+ */
49
+ function persistGatewayToken(token) {
50
+ try {
51
+ let cfg = {};
52
+ try { cfg = JSON.parse(fs.readFileSync(LOBSTAKIT_CONFIG, 'utf8')); } catch { /* new file */ }
53
+ cfg.gatewayToken = token;
54
+ fs.writeFileSync(LOBSTAKIT_CONFIG, JSON.stringify(cfg, null, 2));
55
+ } catch (err) {
56
+ console.error('[gateway] Failed to persist token:', err.message);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Read token from openclaw.json and persist into lobstakit config.
62
+ * Called after every gateway install/restart so both sides stay in sync.
63
+ */
64
+ function syncGatewayToken() {
65
+ const token = readGatewayToken();
66
+ if (token) {
67
+ persistGatewayToken(token);
68
+ console.log('[gateway] Auth token synced to lobstakit config');
69
+ } else {
70
+ console.warn('[gateway] Could not read gateway auth token from openclaw.json');
71
+ }
72
+ return token;
73
+ }
74
+
75
+ /**
76
+ * Get the stored gateway token from lobstakit config (fast path).
77
+ * Falls back to reading directly from openclaw.json.
78
+ */
79
+ function getStoredToken() {
80
+ try {
81
+ const cfg = JSON.parse(fs.readFileSync(LOBSTAKIT_CONFIG, 'utf8'));
82
+ if (cfg?.gatewayToken) return cfg.gatewayToken;
83
+ } catch { /* fall through */ }
84
+ return readGatewayToken();
85
+ }
86
+
87
+ // ─── Health check via HTTP ────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Ping the gateway HTTP API. Returns a promise resolving to true if healthy.
91
+ */
92
+ function pingGateway(token) {
93
+ return new Promise((resolve) => {
94
+ const opts = {
95
+ hostname: '127.0.0.1',
96
+ port: 3001,
97
+ path: '/api/status',
98
+ method: 'GET',
99
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
100
+ timeout: 3000,
101
+ };
102
+ const req = http.request(opts, (res) => {
103
+ resolve(res.statusCode === 200 || res.statusCode === 401); // 401 = running but token stale
104
+ });
105
+ req.on('error', () => resolve(false));
106
+ req.on('timeout', () => { req.destroy(); resolve(false); });
107
+ req.end();
108
+ });
109
+ }
110
+
111
+ // ─── Service control ──────────────────────────────────────────────────────────
13
112
 
14
113
  /**
15
114
  * Check if the gateway service is currently running.
115
+ * Tries systemctl --user first, then falls back to HTTP ping.
16
116
  */
17
117
  function isGatewayRunning() {
118
+ // Primary: systemctl --user (accurate for user services)
18
119
  try {
19
- const result = execSync(`systemctl is-active ${SERVICE_NAME}`, {
120
+ const result = execSync(`systemctl --user is-active ${SERVICE_NAME}`, {
20
121
  encoding: 'utf8',
21
- timeout: 5000
122
+ timeout: 5000,
123
+ env: userEnv(),
22
124
  }).trim();
23
- return result === 'active';
125
+ if (result === 'active') return true;
126
+ } catch { /* fall through to HTTP check */ }
127
+
128
+ // Fallback: HTTP ping (works even if systemctl --user is unavailable)
129
+ // Run synchronously via a child process trick
130
+ try {
131
+ const token = getStoredToken();
132
+ const authHeader = token ? `-H "Authorization: Bearer ${token}"` : '';
133
+ const result = execSync(
134
+ `curl -s -o /dev/null -w '%{http_code}' ${authHeader} http://127.0.0.1:3001/api/status`,
135
+ { encoding: 'utf8', timeout: 4000 }
136
+ ).trim();
137
+ return result === '200' || result === '401';
24
138
  } catch {
25
139
  return false;
26
140
  }
27
141
  }
28
142
 
29
143
  /**
30
- * Ensure D-Bus user session is available and inject env vars into the
31
- * openclaw-gateway systemd service unit via a drop-in override.
32
- *
33
- * Root cause of "systemctl --user unavailable: Failed to connect to bus:
34
- * No medium found": OpenClaw calls `systemctl --user` internally on startup.
35
- * On headless VPS, no D-Bus user session exists. Even if LobstaKit sets up
36
- * D-Bus in its own process, the gateway runs as a *separate* system service
37
- * and never inherits those env vars.
38
- *
39
- * Fix: write a systemd drop-in that bakes XDG_RUNTIME_DIR and
40
- * DBUS_SESSION_BUS_ADDRESS into the service unit so every restart picks
41
- * them up automatically — no manual intervention required.
144
+ * Ensure D-Bus user session and systemd drop-in are set up.
145
+ * Needed so the gateway can call `systemctl --user` internally.
42
146
  */
43
147
  function ensureUserSession() {
44
148
  try {
@@ -48,86 +152,48 @@ function ensureUserSession() {
48
152
  const busSocket = `${runtimeDir}/bus`;
49
153
  const busAddress = `unix:path=${busSocket}`;
50
154
 
51
- // 1. Enable lingering so user units survive without an active login
52
- execSync(`loginctl enable-linger ${username} 2>/dev/null || true`, {
53
- encoding: 'utf8',
54
- timeout: 5000
55
- });
56
-
57
- // 2. Ensure runtime dir exists with correct permissions
58
- execSync(`mkdir -p ${runtimeDir} && chmod 700 ${runtimeDir} && chown ${uid}:${uid} ${runtimeDir} 2>/dev/null || true`, {
59
- encoding: 'utf8',
60
- timeout: 3000
61
- });
155
+ execSync(`loginctl enable-linger ${username} 2>/dev/null || true`, { encoding: 'utf8', timeout: 5000 });
156
+ execSync(`mkdir -p ${runtimeDir} && chmod 700 ${runtimeDir} 2>/dev/null || true`, { encoding: 'utf8', timeout: 3000 });
62
157
 
63
- // 3. Start dbus-daemon if socket not already present
64
158
  let dbusRunning = false;
65
- try {
66
- execSync(`test -S ${busSocket}`, { encoding: 'utf8', timeout: 2000 });
67
- dbusRunning = true;
68
- } catch { /* socket not there yet */ }
159
+ try { execSync(`test -S ${busSocket}`, { encoding: 'utf8', timeout: 2000 }); dbusRunning = true; } catch { /* not running */ }
69
160
 
70
161
  if (!dbusRunning) {
71
162
  try {
72
- execSync(
73
- `dbus-daemon --session --address=${busAddress} --fork --nopidfile`,
74
- {
75
- encoding: 'utf8',
76
- timeout: 5000,
77
- env: { ...process.env, XDG_RUNTIME_DIR: runtimeDir }
78
- }
79
- );
80
- // Wait up to 2s for socket to appear
163
+ execSync(`dbus-daemon --session --address=${busAddress} --fork --nopidfile`, {
164
+ encoding: 'utf8', timeout: 5000,
165
+ env: { ...process.env, XDG_RUNTIME_DIR: runtimeDir },
166
+ });
81
167
  for (let i = 0; i < 10; i++) {
82
- try {
83
- execSync(`test -S ${busSocket}`, { encoding: 'utf8', timeout: 500 });
84
- dbusRunning = true;
85
- break;
86
- } catch {
87
- execSync('sleep 0.2', { timeout: 500 });
88
- }
168
+ try { execSync(`test -S ${busSocket}`, { encoding: 'utf8', timeout: 500 }); dbusRunning = true; break; } catch { /* wait */ }
169
+ execSync('sleep 0.2', { encoding: 'utf8', timeout: 1000 });
89
170
  }
90
- } catch (e) {
91
- console.warn('[gateway] dbus-daemon unavailable:', e.message);
92
- // dbus-daemon may not be installed — XDG_RUNTIME_DIR alone may be enough
93
- }
94
- }
95
-
96
- // 4. Write systemd drop-in so the service unit inherits these env vars
97
- // on every start/restart, not just when LobstaKit happens to call this.
98
- const lines = [
99
- '# Auto-generated by LobstaKit — do not edit manually',
100
- '[Service]',
101
- `Environment="XDG_RUNTIME_DIR=${runtimeDir}"`,
102
- ];
103
- if (dbusRunning) {
104
- lines.push(`Environment="DBUS_SESSION_BUS_ADDRESS=${busAddress}"`);
171
+ } catch (e) { console.warn('[gateway] dbus-daemon start failed:', e.message); }
105
172
  }
106
- const dropInContent = lines.join('\n') + '\n';
107
-
108
- execSync(`mkdir -p ${DROP_IN_DIR}`, { encoding: 'utf8', timeout: 3000 });
109
- fs.writeFileSync(DROP_IN_FILE, dropInContent, 'utf8');
110
173
 
111
- // 5. Reload systemd so it picks up the new drop-in
112
- execSync('systemctl daemon-reload', { encoding: 'utf8', timeout: 10000 });
174
+ // Write systemd drop-in so the gateway service inherits D-Bus env
175
+ const dropInDir = `/root/.config/systemd/user`;
176
+ const dropInFile = `${dropInDir}/${SERVICE_NAME}.service.d/dbus-env.conf`;
177
+ const dropInContent = `[Service]\nEnvironment=XDG_RUNTIME_DIR=${runtimeDir}\nEnvironment=DBUS_SESSION_BUS_ADDRESS=${busAddress}\n`;
178
+ try {
179
+ execSync(`mkdir -p ${path.dirname(dropInFile)}`, { encoding: 'utf8', timeout: 3000 });
180
+ fs.writeFileSync(dropInFile, dropInContent);
181
+ execSync(`systemctl --user daemon-reload`, { encoding: 'utf8', timeout: 10000, env: userEnv() });
182
+ } catch (e) { console.warn('[gateway] drop-in write failed:', e.message); }
113
183
 
114
- console.log(`[gateway] Session ready — runtimeDir=${runtimeDir} dbus=${dbusRunning} drop-in written`);
184
+ console.log(`[gateway] Session ready — runtimeDir=${runtimeDir} dbus=${dbusRunning}`);
115
185
  } catch (err) {
116
- console.warn('[gateway] ensureUserSession warning:', err.message);
186
+ console.error('[gateway] ensureUserSession failed:', err.message);
117
187
  }
118
188
  }
119
189
 
120
- /**
121
- * Start the gateway service.
122
- */
123
190
  function startGateway() {
124
191
  ensureUserSession();
125
192
  try {
126
- execSync(`systemctl start ${SERVICE_NAME}`, {
127
- encoding: 'utf8',
128
- timeout: 15000
129
- });
193
+ execSync(`systemctl --user enable ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 10000, env: userEnv() });
194
+ execSync(`systemctl --user start ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 15000, env: userEnv() });
130
195
  console.log('[gateway] Service started');
196
+ setTimeout(() => syncGatewayToken(), 3000); // token available after ~2s startup
131
197
  return { success: true };
132
198
  } catch (err) {
133
199
  console.error('[gateway] Failed to start:', err.message);
@@ -135,15 +201,9 @@ function startGateway() {
135
201
  }
136
202
  }
137
203
 
138
- /**
139
- * Stop the gateway service.
140
- */
141
204
  function stopGateway() {
142
205
  try {
143
- execSync(`systemctl stop ${SERVICE_NAME}`, {
144
- encoding: 'utf8',
145
- timeout: 15000
146
- });
206
+ execSync(`systemctl --user stop ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 15000, env: userEnv() });
147
207
  console.log('[gateway] Service stopped');
148
208
  return { success: true };
149
209
  } catch (err) {
@@ -152,21 +212,14 @@ function stopGateway() {
152
212
  }
153
213
  }
154
214
 
155
- /**
156
- * Restart the gateway service (enables it first if not already enabled).
157
- */
158
215
  function restartGateway() {
159
216
  ensureUserSession();
160
217
  try {
161
- execSync(`systemctl enable ${SERVICE_NAME}`, {
162
- encoding: 'utf8',
163
- timeout: 10000
164
- });
165
- execSync(`systemctl restart ${SERVICE_NAME}`, {
166
- encoding: 'utf8',
167
- timeout: 15000
168
- });
218
+ execSync(`systemctl --user enable ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 10000, env: userEnv() });
219
+ execSync(`systemctl --user restart ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 15000, env: userEnv() });
169
220
  console.log('[gateway] Service enabled and restarted');
221
+ // Sync token after restart — gateway writes its token on startup (~2s)
222
+ setTimeout(() => syncGatewayToken(), 3000);
170
223
  return { success: true };
171
224
  } catch (err) {
172
225
  console.error('[gateway] Failed to restart:', err.message);
@@ -174,18 +227,19 @@ function restartGateway() {
174
227
  }
175
228
  }
176
229
 
177
- /**
178
- * Get recent gateway logs from journalctl.
179
- */
180
230
  function getGatewayLogs(lines = 50) {
181
231
  try {
182
- const logs = execSync(`journalctl -u ${SERVICE_NAME} -n ${lines} --no-pager`, {
183
- encoding: 'utf8',
184
- timeout: 5000
185
- });
186
- return logs;
232
+ // Try user journal first, fall back to system journal
233
+ try {
234
+ return execSync(`journalctl --user -u ${SERVICE_NAME} -n ${lines} --no-pager`, {
235
+ encoding: 'utf8', timeout: 5000, env: userEnv(),
236
+ });
237
+ } catch {
238
+ return execSync(`journalctl -u ${SERVICE_NAME} -n ${lines} --no-pager`, {
239
+ encoding: 'utf8', timeout: 5000,
240
+ });
241
+ }
187
242
  } catch (err) {
188
- console.error('[gateway] Failed to get logs:', err.message);
189
243
  return `Error retrieving logs: ${err.message}`;
190
244
  }
191
245
  }
@@ -195,5 +249,8 @@ module.exports = {
195
249
  startGateway,
196
250
  stopGateway,
197
251
  restartGateway,
198
- getGatewayLogs
252
+ getGatewayLogs,
253
+ syncGatewayToken,
254
+ readGatewayToken,
255
+ persistGatewayToken,
199
256
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobstakit-cloud",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "LobstaKit Cloud — Setup wizard and management for LobstaCloud gateways",
5
5
  "main": "server.js",
6
6
  "bin": {