@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/.claude/settings.local.json +11 -0
- package/Dockerfile +12 -0
- package/LICENSE +29 -0
- package/README.md +434 -0
- package/auth.js +276 -0
- package/cli-commands.js +1185 -0
- package/containerManager.js +385 -0
- package/convert-to-esm.sh +62 -0
- package/docker-actions/apps.js +3256 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +346 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +188 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +60 -0
- package/index.js +988 -0
- package/package.json +49 -0
- package/setup/setupWizard.js +499 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +174 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +54 -0
- package/utils/errorHandler.js +327 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/websocket-server.js +364 -0
|
@@ -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
|
+
}
|
package/utils/ecrAuth.js
ADDED
|
@@ -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
|
+
}
|