@fenwave/agent 1.1.0

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/auth.js ADDED
@@ -0,0 +1,276 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import axios from 'axios';
5
+ import chalk from 'chalk';
6
+ import dotenv from 'dotenv';
7
+ dotenv.config();
8
+
9
+ const AGENT_ROOT_DIR = path.join(
10
+ os.homedir(),
11
+ process.env.AGENT_ROOT_DIR || '.fenwave'
12
+ );
13
+ const SESSION_DIR = process.env.SESSION_DIR || 'session';
14
+ const SESSION_FILE = process.env.SESSION_FILE || 'config.json';
15
+ const CONFIG_FILE = path.join(AGENT_ROOT_DIR, SESSION_DIR, SESSION_FILE);
16
+
17
+ /**
18
+ * Ensures the configuration directory exists
19
+ */
20
+ function ensureConfigDirectory() {
21
+ const sessionDir = path.dirname(CONFIG_FILE);
22
+ if (!fs.existsSync(sessionDir)) {
23
+ fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Saves session data to local config file
29
+ * @param {string} token - Session token
30
+ * @param {string} expiresAt - Expiration timestamp
31
+ * @param {string} userEntityRef - User entity reference
32
+ * @param {string} backendUrl - Optional backend URL to save
33
+ */
34
+ function saveSession(token, expiresAt, userEntityRef, backendUrl = null) {
35
+ ensureConfigDirectory();
36
+ const config = {
37
+ token,
38
+ expiresAt,
39
+ userEntityRef,
40
+ };
41
+
42
+ // Preserve existing backend URL or save new one
43
+ if (backendUrl) {
44
+ config.backendUrl = backendUrl;
45
+ } else {
46
+ const existingConfig = loadSession();
47
+ if (existingConfig && existingConfig.backendUrl) {
48
+ config.backendUrl = existingConfig.backendUrl;
49
+ }
50
+ }
51
+
52
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
53
+ mode: 0o600,
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Loads session data from local config file
59
+ * @returns {Object|null} Session data or null if not found
60
+ */
61
+ function loadSession() {
62
+ if (!fs.existsSync(CONFIG_FILE)) {
63
+ return null;
64
+ }
65
+ try {
66
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
67
+ return config;
68
+ } catch (error) {
69
+ console.error(chalk.red('❌ Error loading session config:'), error.message);
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Checks if a session is still valid (not expired)
76
+ * @param {Object} session - Session data
77
+ * @returns {boolean} - True if session is valid
78
+ */
79
+ function isSessionValid(session) {
80
+ if (!session || !session.token || !session.expiresAt) {
81
+ return false;
82
+ }
83
+ const now = new Date();
84
+ const expiresAt = new Date(session.expiresAt);
85
+ return now < expiresAt;
86
+ }
87
+
88
+ /**
89
+ * Handles session expiry by clearing session and exiting process
90
+ * @param {string} message - Optional message to display
91
+ * @param {Object} server - Optional WebSocket server to close
92
+ * @param {Object} wss - Optional WebSocket server instance to close
93
+ */
94
+ function handleSessionExpiry(
95
+ message = 'Session has expired!',
96
+ server = null,
97
+ wss = null
98
+ ) {
99
+ console.log(chalk.red(`❌ ${message}`));
100
+ console.log(chalk.red('🔒 Fenwave Agent shutting down gracefully...'));
101
+
102
+ // Close WebSocket connections and server if provided
103
+ if (wss) {
104
+ wss.clients.forEach((client) => {
105
+ client.send(
106
+ JSON.stringify({
107
+ type: 'session_expired',
108
+ message: 'Session has expired. Please re-authenticate.',
109
+ })
110
+ );
111
+ client.close();
112
+ });
113
+ wss.close();
114
+ }
115
+
116
+ if (server) {
117
+ server.close();
118
+ }
119
+
120
+ clearSession();
121
+ console.log(chalk.yellow("💡 Please run 'fenwave login' to authenticate again."));
122
+ process.exit(1);
123
+ }
124
+
125
+ /**
126
+ * Creates a new session with the Backstage backend
127
+ * @param {string} jwt - Token for authentication
128
+ * @param {string} backendUrl - Backend URL
129
+ * @returns {Object} Session data with token, userEntityRef and expiresAt
130
+ */
131
+ async function createSession(jwt, backendUrl) {
132
+ try {
133
+ const response = await axios.post(
134
+ `${backendUrl}/api/agent-cli/create-session`,
135
+ {},
136
+ {
137
+ headers: {
138
+ Authorization: `Bearer ${jwt}`,
139
+ 'Content-Type': 'application/json',
140
+ },
141
+ }
142
+ );
143
+ console.log(chalk.green('✅ Session created successfully!'));
144
+ const { token, expiresAt } = response.data;
145
+ return { token, expiresAt };
146
+ } catch (error) {
147
+ console.error(
148
+ chalk.red('❌ Failed to create session: '),
149
+ error.response?.data || error.message
150
+ );
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Clears the stored session
157
+ */
158
+ function clearSession() {
159
+ if (fs.existsSync(CONFIG_FILE)) {
160
+ fs.unlinkSync(CONFIG_FILE);
161
+ return true;
162
+ }
163
+ return false;
164
+ }
165
+
166
+ /**
167
+ * Sets up a file watcher to monitor session file changes
168
+ * @param {Function} onSessionExpired - Callback function to handle session expiry
169
+ * @param {Object} server - WebSocket server instance
170
+ * @param {Object} wss - WebSocket server instance
171
+ * @returns {Object} File watcher instance
172
+ */
173
+ function setupSessionWatcher(onSessionExpired, server = null, wss = null) {
174
+ let watcher = null;
175
+ let expiryTimer = null;
176
+
177
+ // Set up timer for natural expiry
178
+ const session = loadSession();
179
+ if (session && session.expiresAt) {
180
+ const expiryTime = new Date(session.expiresAt).getTime();
181
+ const currentTime = Date.now();
182
+ const timeUntilExpiry = expiryTime - currentTime;
183
+
184
+ if (timeUntilExpiry > 0) {
185
+ expiryTimer = setTimeout(() => {
186
+ clearSession();
187
+ onSessionExpired('Session expired !', server, wss);
188
+ }, timeUntilExpiry);
189
+ }
190
+ }
191
+
192
+ try {
193
+ // Watch the config file for changes/deletion
194
+ watcher = fs.watch(CONFIG_FILE, (eventType, filename) => {
195
+ // Clear the natural expiry timer since file changed
196
+ if (expiryTimer) {
197
+ clearTimeout(expiryTimer);
198
+ expiryTimer = null;
199
+ }
200
+
201
+ // Check if file was deleted or renamed (session revoked by logout)
202
+ if (eventType === 'rename' || !fs.existsSync(CONFIG_FILE)) {
203
+ if (watcher) {
204
+ watcher.close();
205
+ }
206
+ onSessionExpired('Session revoked !', server, wss);
207
+ return;
208
+ }
209
+ });
210
+ return {
211
+ close: () => {
212
+ if (watcher) watcher.close();
213
+ if (expiryTimer) clearTimeout(expiryTimer);
214
+ },
215
+ };
216
+ } catch (error) {
217
+ // Clear expiry timer if file watcher fails
218
+ if (expiryTimer) {
219
+ clearTimeout(expiryTimer);
220
+ expiryTimer = null;
221
+ }
222
+
223
+ // Fallback to periodic checking if file watching fails
224
+ const intervalId = setInterval(() => {
225
+ if (!fs.existsSync(CONFIG_FILE)) {
226
+ clearInterval(intervalId);
227
+ onSessionExpired('Session revoked !', server, wss);
228
+ return;
229
+ }
230
+
231
+ const currentSession = loadSession();
232
+ if (!currentSession || !isSessionValid(currentSession)) {
233
+ clearInterval(intervalId);
234
+ onSessionExpired('Session expired !', server, wss);
235
+ }
236
+ }, 30 * 1000); // Check every 30 seconds as fallback
237
+
238
+ return { close: () => clearInterval(intervalId) };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Loads backend URL from agent config or environment variable
244
+ * NOTE: This function loads synchronously from the config file
245
+ * @returns {string|undefined} Backend URL
246
+ */
247
+ function loadBackendUrl() {
248
+ // Load directly from config file to avoid circular dependencies
249
+ const configPath = path.join(os.homedir(), '.fenwave', 'config', 'agent.json');
250
+
251
+ // Try to load from config file
252
+ if (fs.existsSync(configPath)) {
253
+ try {
254
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
255
+ if (config.backendUrl) {
256
+ return config.backendUrl;
257
+ }
258
+ } catch (error) {
259
+ // Fall through to env variable
260
+ }
261
+ }
262
+
263
+ // Fall back to environment variable
264
+ return process.env.BACKEND_URL || 'http://localhost:7007';
265
+ }
266
+
267
+ export {
268
+ saveSession,
269
+ loadSession,
270
+ isSessionValid,
271
+ handleSessionExpiry,
272
+ createSession,
273
+ clearSession,
274
+ setupSessionWatcher,
275
+ loadBackendUrl,
276
+ };