@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.
@@ -0,0 +1,147 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const FENWAVE_DIR = path.join(os.homedir(), '.fenwave');
6
+ const SETUP_DIR = path.join(FENWAVE_DIR, 'setup');
7
+ const STATE_FILE = path.join(SETUP_DIR, 'state.json');
8
+
9
+ /**
10
+ * Setup steps enum
11
+ */
12
+ const SetupStep = {
13
+ PREREQUISITES: 'prerequisites',
14
+ REGISTRATION: 'registration',
15
+ DOCKER_REGISTRY: 'docker_registry',
16
+ PULL_IMAGE: 'pull_image',
17
+ NPM_CONFIG: 'npm_config',
18
+ START_AGENT: 'start_agent',
19
+ COMPLETE: 'complete',
20
+ };
21
+
22
+ /**
23
+ * Ensure setup directory exists
24
+ */
25
+ function ensureSetupDir() {
26
+ if (!fs.existsSync(FENWAVE_DIR)) {
27
+ fs.mkdirSync(FENWAVE_DIR, { recursive: true, mode: 0o700 });
28
+ }
29
+ if (!fs.existsSync(SETUP_DIR)) {
30
+ fs.mkdirSync(SETUP_DIR, { recursive: true, mode: 0o700 });
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Save setup progress
36
+ *
37
+ * @param {string} step - Current step
38
+ * @param {Object} data - Step data
39
+ */
40
+ function saveSetupProgress(step, data = {}) {
41
+ ensureSetupDir();
42
+
43
+ const state = loadSetupProgress() || {
44
+ startedAt: new Date().toISOString(),
45
+ steps: {},
46
+ };
47
+
48
+ state.steps[step] = {
49
+ completedAt: new Date().toISOString(),
50
+ data,
51
+ };
52
+
53
+ state.currentStep = step;
54
+ state.updatedAt = new Date().toISOString();
55
+
56
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), {
57
+ mode: 0o600,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Load setup progress
63
+ *
64
+ * @returns {Object|null} Setup state or null if not found
65
+ */
66
+ function loadSetupProgress() {
67
+ if (!fs.existsSync(STATE_FILE)) {
68
+ return null;
69
+ }
70
+
71
+ try {
72
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
73
+ } catch (error) {
74
+ console.error('Failed to load setup progress:', error.message);
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Mark step as complete
81
+ *
82
+ * @param {string} step - Step to mark as complete
83
+ */
84
+ function markStepComplete(step) {
85
+ saveSetupProgress(step);
86
+ }
87
+
88
+ /**
89
+ * Check if step is complete
90
+ *
91
+ * @param {string} step - Step to check
92
+ * @returns {boolean} True if step is complete
93
+ */
94
+ function isStepComplete(step) {
95
+ const state = loadSetupProgress();
96
+ return state?.steps?.[step] !== undefined;
97
+ }
98
+
99
+ /**
100
+ * Get current step
101
+ *
102
+ * @returns {string|null} Current step or null
103
+ */
104
+ function getCurrentStep() {
105
+ const state = loadSetupProgress();
106
+ return state?.currentStep || null;
107
+ }
108
+
109
+ /**
110
+ * Clear setup state
111
+ */
112
+ function clearSetupState() {
113
+ if (fs.existsSync(STATE_FILE)) {
114
+ fs.unlinkSync(STATE_FILE);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Check if setup is in progress
120
+ *
121
+ * @returns {boolean} True if setup state exists
122
+ */
123
+ function isSetupInProgress() {
124
+ return fs.existsSync(STATE_FILE);
125
+ }
126
+
127
+ /**
128
+ * Get completed steps
129
+ *
130
+ * @returns {Array<string>} List of completed step names
131
+ */
132
+ function getCompletedSteps() {
133
+ const state = loadSetupProgress();
134
+ return state?.steps ? Object.keys(state.steps) : [];
135
+ }
136
+
137
+ export {
138
+ SetupStep,
139
+ saveSetupProgress,
140
+ loadSetupProgress,
141
+ markStepComplete,
142
+ isStepComplete,
143
+ getCurrentStep,
144
+ clearSetupState,
145
+ isSetupInProgress,
146
+ getCompletedSteps,
147
+ };
@@ -0,0 +1,98 @@
1
+ import os from 'os';
2
+ import { execSync } from 'child_process';
3
+ import packageJson from '../package.json' with { type: 'json' };
4
+
5
+ /**
6
+ * Collect device information for registration
7
+ *
8
+ * @returns {Object} Device information
9
+ */
10
+ export function collectDeviceInfo() {
11
+ const platform = os.platform(); // darwin, linux, win32
12
+ const hostname = os.hostname();
13
+ const arch = os.arch(); // x64, arm64, etc.
14
+ const cpus = os.cpus();
15
+ const totalMemory = os.totalmem();
16
+ const osVersion = os.release();
17
+
18
+ // Get Docker version if available
19
+ let dockerVersion = null;
20
+ try {
21
+ dockerVersion = execSync('docker --version', { encoding: 'utf8' }).trim();
22
+ } catch (error) {
23
+ // Docker not installed or not available
24
+ dockerVersion = 'Not available';
25
+ }
26
+
27
+ // Get agent version from package.json
28
+ const agentVersion = packageJson.version;
29
+
30
+ return {
31
+ hostname,
32
+ platform,
33
+ arch,
34
+ osVersion,
35
+ cpuCount: cpus.length,
36
+ totalMemory: Math.round(totalMemory / (1024 * 1024 * 1024)), // Convert to GB
37
+ dockerVersion,
38
+ agentVersion,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Generate a human-readable device name
44
+ *
45
+ * @returns {string} Device name (e.g., "johns-macbook-darwin")
46
+ */
47
+ export function generateDeviceName() {
48
+ const hostname = os.hostname()
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9-]/g, '-') // Replace non-alphanumeric with dash
51
+ .replace(/-+/g, '-') // Replace multiple dashes with single dash
52
+ .replace(/^-|-$/g, ''); // Remove leading/trailing dashes
53
+
54
+ const platform = os.platform();
55
+
56
+ return `${hostname}-${platform}`;
57
+ }
58
+
59
+ /**
60
+ * Get device metadata for storage
61
+ *
62
+ * @returns {Object} Complete device metadata
63
+ */
64
+ export async function getDeviceMetadata() {
65
+ const info = collectDeviceInfo();
66
+ const deviceName = generateDeviceName();
67
+
68
+ return {
69
+ deviceName,
70
+ platform: info.platform,
71
+ osVersion: info.osVersion,
72
+ agentVersion: info.agentVersion,
73
+ metadata: {
74
+ hostname: info.hostname,
75
+ arch: info.arch,
76
+ cpuCount: info.cpuCount,
77
+ totalMemoryGB: info.totalMemory,
78
+ dockerVersion: info.dockerVersion,
79
+ },
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Display device information in a formatted way
85
+ *
86
+ * @param {Object} deviceInfo - Device information object
87
+ */
88
+ export function displayDeviceInfo(deviceInfo) {
89
+ console.log('\n📱 Device Information:');
90
+ console.log(` Name: ${deviceInfo.deviceName || 'N/A'}`);
91
+ console.log(` Platform: ${deviceInfo.platform}`);
92
+ console.log(` OS Version: ${deviceInfo.osVersion || 'N/A'}`);
93
+ console.log(` Architecture: ${deviceInfo.metadata?.arch || 'N/A'}`);
94
+ console.log(` CPUs: ${deviceInfo.metadata?.cpuCount || 'N/A'}`);
95
+ console.log(` Memory: ${deviceInfo.metadata?.totalMemoryGB || 'N/A'} GB`);
96
+ console.log(` Docker: ${deviceInfo.metadata?.dockerVersion || 'N/A'}`);
97
+ console.log(` Agent Version: ${deviceInfo.agentVersion || 'N/A'}\n`);
98
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * ECR Authentication Utility
3
+ *
4
+ * Handles requesting temporary AWS ECR credentials from Backstage
5
+ * and authenticating Docker with ECR.
6
+ */
7
+
8
+ import { loadDeviceCredential } from '../store/deviceCredentialStore.js';
9
+ import { execSync } from 'child_process';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+
14
+ /**
15
+ * Request temporary ECR credentials from Backstage
16
+ *
17
+ * @param {string} backstageUrl - Backstage backend URL
18
+ * @returns {Promise<Object>} ECR credentials object
19
+ */
20
+ export async function requestECRCredentials(backstageUrl) {
21
+ try {
22
+ // Load device credentials
23
+ const deviceCreds = await loadDeviceCredential();
24
+
25
+ if (!deviceCreds || !deviceCreds.deviceId || !deviceCreds.deviceCredential) {
26
+ throw new Error('Device not registered. Please run: fenwave init');
27
+ }
28
+
29
+ // Request credentials from Backstage
30
+ const url = new URL('/api/agent-cli/ecr-credentials', backstageUrl);
31
+ url.searchParams.append('deviceId', deviceCreds.deviceId);
32
+ url.searchParams.append('deviceCredential', deviceCreds.deviceCredential);
33
+
34
+ const response = await fetch(url.toString());
35
+
36
+ if (!response.ok) {
37
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
38
+ throw new Error(`Failed to get ECR credentials: ${error.error || response.statusText}`);
39
+ }
40
+
41
+ const credentials = await response.json();
42
+
43
+ return credentials;
44
+ } catch (error) {
45
+ throw new Error(`Failed to request ECR credentials: ${error.message}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Set AWS environment variables for temporary credentials
51
+ *
52
+ * @param {Object} credentials - ECR credentials object
53
+ */
54
+ export function setAWSCredentials(credentials) {
55
+ process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
56
+ process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
57
+ process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
58
+ process.env.AWS_DEFAULT_REGION = credentials.region;
59
+ }
60
+
61
+ /**
62
+ * Clear AWS environment variables
63
+ */
64
+ export function clearAWSCredentials() {
65
+ delete process.env.AWS_ACCESS_KEY_ID;
66
+ delete process.env.AWS_SECRET_ACCESS_KEY;
67
+ delete process.env.AWS_SESSION_TOKEN;
68
+ delete process.env.AWS_DEFAULT_REGION;
69
+ }
70
+
71
+ /**
72
+ * Authenticate Docker with ECR using temporary credentials
73
+ *
74
+ * @param {string} backstageUrl - Backstage backend URL
75
+ * @returns {Promise<Object>} Authentication result with registry info
76
+ */
77
+ export async function authenticateDockerWithECR(backstageUrl) {
78
+ try {
79
+ console.log('🔐 Requesting temporary ECR credentials from Backstage...');
80
+
81
+ // Request credentials
82
+ const credentials = await requestECRCredentials(backstageUrl);
83
+
84
+ console.log(`✅ Received temporary credentials (expires: ${credentials.expiration})`);
85
+
86
+ // Set AWS environment variables
87
+ setAWSCredentials(credentials);
88
+
89
+ // Authenticate with ECR
90
+ const ecrUri = `${credentials.accountId}.dkr.ecr.${credentials.region}.amazonaws.com`;
91
+ console.log(`🔑 Authenticating Docker with ECR: ${ecrUri}...`);
92
+
93
+ try {
94
+ // Get ECR login password
95
+ const loginPassword = execSync(
96
+ `aws ecr get-login-password --region ${credentials.region}`,
97
+ { encoding: 'utf-8' }
98
+ ).trim();
99
+
100
+ // Login to Docker
101
+ execSync(
102
+ `echo "${loginPassword}" | docker login --username AWS --password-stdin ${ecrUri}`,
103
+ { stdio: 'pipe' }
104
+ );
105
+
106
+ console.log('✅ Successfully authenticated with ECR');
107
+
108
+ // Return registry information
109
+ return {
110
+ success: true,
111
+ registryUri: ecrUri,
112
+ dockerImage: `${ecrUri}/${credentials.ecrRepository}:latest`,
113
+ region: credentials.region,
114
+ accountId: credentials.accountId,
115
+ repository: credentials.ecrRepository,
116
+ expiration: credentials.expiration,
117
+ };
118
+ } catch (dockerError) {
119
+ clearAWSCredentials();
120
+ throw new Error(`Docker authentication failed: ${dockerError.message}`);
121
+ }
122
+ } catch (error) {
123
+ clearAWSCredentials();
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Pull Docker image from ECR with authentication
130
+ *
131
+ * @param {string} backstageUrl - Backstage backend URL
132
+ * @returns {Promise<Object>} Pull result with image info
133
+ */
134
+ export async function pullDockerImageFromECR(backstageUrl) {
135
+ try {
136
+ // Authenticate with ECR
137
+ const authResult = await authenticateDockerWithECR(backstageUrl);
138
+
139
+ console.log(`📥 Pulling Docker image: ${authResult.dockerImage}...`);
140
+
141
+ // Pull the image
142
+ execSync(`docker pull ${authResult.dockerImage}`, { stdio: 'inherit' });
143
+
144
+ console.log('✅ Docker image pulled successfully');
145
+
146
+ // Clear credentials after use
147
+ clearAWSCredentials();
148
+
149
+ return {
150
+ success: true,
151
+ dockerImage: authResult.dockerImage,
152
+ registryUri: authResult.registryUri,
153
+ };
154
+ } catch (error) {
155
+ clearAWSCredentials();
156
+ throw new Error(`Failed to pull Docker image: ${error.message}`);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Check if credentials are expired or about to expire
162
+ *
163
+ * @param {string} expirationISO - ISO 8601 expiration timestamp
164
+ * @param {number} bufferMinutes - Minutes before expiration to consider expired (default: 5)
165
+ * @returns {boolean} True if credentials are expired or expiring soon
166
+ */
167
+ export function areCredentialsExpired(expirationISO, bufferMinutes = 5) {
168
+ const expirationTime = new Date(expirationISO).getTime();
169
+ const bufferMs = bufferMinutes * 60 * 1000;
170
+ const now = Date.now();
171
+
172
+ return now >= (expirationTime - bufferMs);
173
+ }
174
+
175
+ /**
176
+ * Cache credentials to disk (optional, for future use)
177
+ *
178
+ * @param {Object} credentials - ECR credentials object
179
+ */
180
+ export async function cacheCredentials(credentials) {
181
+ const cacheDir = path.join(os.homedir(), '.fenwave', 'cache');
182
+ const cacheFile = path.join(cacheDir, 'ecr-credentials.json');
183
+
184
+ try {
185
+ // Create cache directory if it doesn't exist
186
+ if (!fs.existsSync(cacheDir)) {
187
+ fs.mkdirSync(cacheDir, { recursive: true });
188
+ }
189
+
190
+ // Write credentials to cache
191
+ fs.writeFileSync(cacheFile, JSON.stringify(credentials, null, 2), 'utf-8');
192
+ } catch (error) {
193
+ // Ignore cache errors - not critical
194
+ console.warn('⚠️ Failed to cache credentials:', error.message);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Load cached credentials (optional, for future use)
200
+ *
201
+ * @returns {Object|null} Cached credentials or null if not found/expired
202
+ */
203
+ export async function loadCachedCredentials() {
204
+ const cacheFile = path.join(os.homedir(), '.fenwave', 'cache', 'ecr-credentials.json');
205
+
206
+ try {
207
+ if (!fs.existsSync(cacheFile)) {
208
+ return null;
209
+ }
210
+
211
+ const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
212
+
213
+ // Check if credentials are expired
214
+ if (areCredentialsExpired(cached.expiration)) {
215
+ // Delete expired cache
216
+ fs.unlinkSync(cacheFile);
217
+ return null;
218
+ }
219
+
220
+ return cached;
221
+ } catch (error) {
222
+ // Ignore cache errors
223
+ return null;
224
+ }
225
+ }
@@ -0,0 +1,112 @@
1
+ import crypto from 'crypto';
2
+ import os from 'os';
3
+ import { networkInterfaces } from 'os';
4
+
5
+ /**
6
+ * Generate a machine-specific encryption key
7
+ * Uses hostname and MAC address to create a unique key for this machine
8
+ *
9
+ * @returns {Buffer} Encryption key
10
+ */
11
+ function getMachineKey() {
12
+ const hostname = os.hostname();
13
+
14
+ // Get first MAC address
15
+ const interfaces = networkInterfaces();
16
+ let macAddress = 'default-mac';
17
+
18
+ for (const name of Object.keys(interfaces)) {
19
+ const iface = interfaces[name];
20
+ if (iface) {
21
+ for (const addr of iface) {
22
+ if (!addr.internal && addr.mac && addr.mac !== '00:00:00:00:00:00') {
23
+ macAddress = addr.mac;
24
+ break;
25
+ }
26
+ }
27
+ }
28
+ if (macAddress !== 'default-mac') break;
29
+ }
30
+
31
+ // Create key from hostname + MAC address
32
+ const keyMaterial = `${hostname}-${macAddress}`;
33
+
34
+ // Derive a 32-byte key using SHA-256
35
+ return crypto.createHash('sha256').update(keyMaterial).digest();
36
+ }
37
+
38
+ /**
39
+ * Encrypt data using AES-256-GCM with machine-specific key
40
+ *
41
+ * @param {string} plaintext - Data to encrypt
42
+ * @returns {string} Encrypted data (base64 encoded with IV and auth tag)
43
+ */
44
+ export function encrypt(plaintext) {
45
+ const key = getMachineKey();
46
+ const iv = crypto.randomBytes(16); // 128-bit IV for GCM
47
+
48
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
49
+
50
+ let encrypted = cipher.update(plaintext, 'utf8', 'base64');
51
+ encrypted += cipher.final('base64');
52
+
53
+ const authTag = cipher.getAuthTag();
54
+
55
+ // Combine IV + authTag + encrypted data
56
+ const combined = Buffer.concat([
57
+ iv,
58
+ authTag,
59
+ Buffer.from(encrypted, 'base64'),
60
+ ]);
61
+
62
+ return combined.toString('base64');
63
+ }
64
+
65
+ /**
66
+ * Decrypt data using AES-256-GCM with machine-specific key
67
+ *
68
+ * @param {string} encryptedData - Encrypted data (base64 encoded)
69
+ * @returns {string} Decrypted plaintext
70
+ */
71
+ export function decrypt(encryptedData) {
72
+ const key = getMachineKey();
73
+ const combined = Buffer.from(encryptedData, 'base64');
74
+
75
+ // Extract IV (first 16 bytes), authTag (next 16 bytes), and encrypted data
76
+ const iv = combined.slice(0, 16);
77
+ const authTag = combined.slice(16, 32);
78
+ const encrypted = combined.slice(32);
79
+
80
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
81
+ decipher.setAuthTag(authTag);
82
+
83
+ let decrypted = decipher.update(encrypted, undefined, 'utf8');
84
+ decrypted += decipher.final('utf8');
85
+
86
+ return decrypted;
87
+ }
88
+
89
+ /**
90
+ * Simple hash function for data integrity
91
+ *
92
+ * @param {string} data - Data to hash
93
+ * @returns {string} SHA-256 hash (hex)
94
+ */
95
+ export function hash(data) {
96
+ return crypto.createHash('sha256').update(data).digest('hex');
97
+ }
98
+
99
+ /**
100
+ * Verify if encrypted data can be decrypted (test encryption key)
101
+ *
102
+ * @param {string} encryptedData - Encrypted data to test
103
+ * @returns {boolean} True if decryption succeeds
104
+ */
105
+ export function canDecrypt(encryptedData) {
106
+ try {
107
+ decrypt(encryptedData);
108
+ return true;
109
+ } catch (error) {
110
+ return false;
111
+ }
112
+ }
@@ -0,0 +1,54 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+
5
+ /**
6
+ * Ensures environment files exist with default values
7
+ * @param {string} agentDir - Path to the agent directory
8
+ * @param {boolean} showFoundMessages - Whether to show messages when files already exist (default: false)
9
+ */
10
+ export function ensureEnvironmentFiles(agentDir, showFoundMessages = false) {
11
+ // Check and create .env.agent for agent if not exists
12
+ const agentEnvPath = path.join(agentDir, ".env.agent");
13
+ if (!fs.existsSync(agentEnvPath)) {
14
+ const defaultAgentEnv = `BACKEND_URL=http://localhost:7007
15
+ FRONTEND_URL=http://localhost:3000
16
+ WS_PORT=3001
17
+ CONTAINER_PORT=3003
18
+ AGENT_ROOT_DIR=.fenwave
19
+ REGISTRIES_DIR=registries
20
+ AWS_REGION=eu-west-1
21
+ AUTH_TIMEOUT_MS=60000
22
+ `;
23
+ fs.writeFileSync(agentEnvPath, defaultAgentEnv);
24
+ console.log(chalk.green("📄 Created .env.agent with default values"));
25
+ } else if (showFoundMessages) {
26
+ console.log(chalk.green("📄 Found existing .env.agent file"));
27
+ }
28
+
29
+ // Check and create ../.env for devapp if not exists
30
+ const appEnvPath = path.join(agentDir, "../fenwave-developer-app", ".env");
31
+ if (!fs.existsSync(appEnvPath)) {
32
+ const defaultAppEnv = `NEXT_PUBLIC_PORT=3001
33
+ NEXT_PUBLIC_WS_URL=http://localhost:3001
34
+ NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000
35
+ NEXT_PUBLIC_TOAST_LIMIT=4
36
+ NEXT_PUBLIC_TOAST_REMOVE_DELAY=5000
37
+ NEXT_PUBLIC_AGENT_ROOT_DIR=.fw
38
+ NEXT_PUBLIC_REGISTRIES_DIR=registries
39
+ NEXT_PUBLIC_REGISTRIES_FILE=credentials.json
40
+ `;
41
+ fs.writeFileSync(appEnvPath, defaultAppEnv);
42
+ console.log(
43
+ chalk.green(
44
+ "📄 Created ../fenwave-developer-app/.env with default values"
45
+ )
46
+ );
47
+ } else if (showFoundMessages) {
48
+ console.log(
49
+ chalk.green("📄 Found existing ../fenwave-developer-app/.env file")
50
+ );
51
+ }
52
+
53
+ return agentEnvPath;
54
+ }