@edgible-team/cli 1.0.1
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/LICENSE +136 -0
- package/README.md +450 -0
- package/dist/client/api-client.js +1057 -0
- package/dist/client/index.js +21 -0
- package/dist/commands/agent.js +1280 -0
- package/dist/commands/ai.js +608 -0
- package/dist/commands/application.js +885 -0
- package/dist/commands/auth.js +570 -0
- package/dist/commands/base/BaseCommand.js +93 -0
- package/dist/commands/base/CommandHandler.js +7 -0
- package/dist/commands/base/command-wrapper.js +58 -0
- package/dist/commands/base/middleware.js +77 -0
- package/dist/commands/config.js +116 -0
- package/dist/commands/connectivity.js +59 -0
- package/dist/commands/debug.js +98 -0
- package/dist/commands/discover.js +144 -0
- package/dist/commands/examples/migrated-command-example.js +180 -0
- package/dist/commands/gateway.js +494 -0
- package/dist/commands/managedGateway.js +787 -0
- package/dist/commands/utils/config-validator.js +76 -0
- package/dist/commands/utils/gateway-prompt.js +79 -0
- package/dist/commands/utils/input-parser.js +120 -0
- package/dist/commands/utils/output-formatter.js +109 -0
- package/dist/config/app-config.js +99 -0
- package/dist/detection/SystemCapabilityDetector.js +1244 -0
- package/dist/detection/ToolDetector.js +305 -0
- package/dist/detection/WorkloadDetector.js +314 -0
- package/dist/di/bindings.js +99 -0
- package/dist/di/container.js +88 -0
- package/dist/di/types.js +32 -0
- package/dist/index.js +52 -0
- package/dist/interfaces/IDaemonManager.js +3 -0
- package/dist/repositories/config-repository.js +62 -0
- package/dist/repositories/gateway-repository.js +35 -0
- package/dist/scripts/postinstall.js +101 -0
- package/dist/services/AgentStatusManager.js +299 -0
- package/dist/services/ConnectivityTester.js +271 -0
- package/dist/services/DependencyInstaller.js +475 -0
- package/dist/services/LocalAgentManager.js +2216 -0
- package/dist/services/application/ApplicationService.js +299 -0
- package/dist/services/auth/AuthService.js +214 -0
- package/dist/services/aws.js +644 -0
- package/dist/services/daemon/DaemonManagerFactory.js +65 -0
- package/dist/services/daemon/DockerDaemonManager.js +395 -0
- package/dist/services/daemon/LaunchdDaemonManager.js +257 -0
- package/dist/services/daemon/PodmanDaemonManager.js +369 -0
- package/dist/services/daemon/SystemdDaemonManager.js +221 -0
- package/dist/services/daemon/WindowsServiceDaemonManager.js +210 -0
- package/dist/services/daemon/index.js +16 -0
- package/dist/services/edgible.js +3060 -0
- package/dist/services/gateway/GatewayService.js +334 -0
- package/dist/state/config.js +146 -0
- package/dist/types/AgentConfig.js +5 -0
- package/dist/types/AgentStatus.js +5 -0
- package/dist/types/ApiClient.js +5 -0
- package/dist/types/ApiRequests.js +5 -0
- package/dist/types/ApiResponses.js +5 -0
- package/dist/types/Application.js +5 -0
- package/dist/types/CaddyJson.js +5 -0
- package/dist/types/UnifiedAgentStatus.js +56 -0
- package/dist/types/WireGuard.js +5 -0
- package/dist/types/Workload.js +5 -0
- package/dist/types/agent.js +5 -0
- package/dist/types/command-options.js +5 -0
- package/dist/types/connectivity.js +5 -0
- package/dist/types/errors.js +250 -0
- package/dist/types/gateway-types.js +5 -0
- package/dist/types/index.js +48 -0
- package/dist/types/models/ApplicationData.js +5 -0
- package/dist/types/models/CertificateData.js +5 -0
- package/dist/types/models/DeviceData.js +5 -0
- package/dist/types/models/DevicePoolData.js +5 -0
- package/dist/types/models/OrganizationData.js +5 -0
- package/dist/types/models/OrganizationInviteData.js +5 -0
- package/dist/types/models/ProviderConfiguration.js +5 -0
- package/dist/types/models/ResourceData.js +5 -0
- package/dist/types/models/ServiceResourceData.js +5 -0
- package/dist/types/models/UserData.js +5 -0
- package/dist/types/route.js +5 -0
- package/dist/types/validation/schemas.js +218 -0
- package/dist/types/validation.js +5 -0
- package/dist/utils/FileIntegrityManager.js +256 -0
- package/dist/utils/PathMigration.js +219 -0
- package/dist/utils/PathResolver.js +235 -0
- package/dist/utils/PlatformDetector.js +277 -0
- package/dist/utils/console-logger.js +130 -0
- package/dist/utils/docker-compose-parser.js +179 -0
- package/dist/utils/errors.js +130 -0
- package/dist/utils/health-checker.js +155 -0
- package/dist/utils/json-logger.js +72 -0
- package/dist/utils/log-formatter.js +293 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/network-utils.js +217 -0
- package/dist/utils/output.js +182 -0
- package/dist/utils/passwordValidation.js +91 -0
- package/dist/utils/progress.js +167 -0
- package/dist/utils/sudo-checker.js +22 -0
- package/dist/utils/urls.js +32 -0
- package/dist/utils/validation.js +31 -0
- package/dist/validation/schemas.js +175 -0
- package/dist/validation/validator.js +67 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// AUTO-GENERATED FILE - DO NOT EDIT
|
|
3
|
+
// This file is copied from backend during build. Changes will be overwritten.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.ApiClient = exports.ApiClientError = void 0;
|
|
6
|
+
exports.createApiClient = createApiClient;
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// DEBUG UTILITY
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Note: debugLog function removed - now using structured logging through log() method
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// BASE API CLIENT
|
|
13
|
+
// ============================================================================
|
|
14
|
+
class BaseApiClient {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.refreshPromise = null;
|
|
17
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
18
|
+
this.timeout = config.timeout || 30000;
|
|
19
|
+
this.disableCaching = false;
|
|
20
|
+
this.logFunction = config.logFunction;
|
|
21
|
+
this.apiClientLogger = config.apiClientLogger;
|
|
22
|
+
}
|
|
23
|
+
setAccessToken(token) {
|
|
24
|
+
this.accessToken = token;
|
|
25
|
+
}
|
|
26
|
+
setIdToken(token) {
|
|
27
|
+
this.idToken = token;
|
|
28
|
+
}
|
|
29
|
+
setRefreshToken(token) {
|
|
30
|
+
this.refreshTokenValue = token;
|
|
31
|
+
}
|
|
32
|
+
clearAccessToken() {
|
|
33
|
+
delete this.accessToken;
|
|
34
|
+
}
|
|
35
|
+
clearIdToken() {
|
|
36
|
+
delete this.idToken;
|
|
37
|
+
}
|
|
38
|
+
clearRefreshToken() {
|
|
39
|
+
delete this.refreshTokenValue;
|
|
40
|
+
}
|
|
41
|
+
clearTokens() {
|
|
42
|
+
delete this.accessToken;
|
|
43
|
+
delete this.idToken;
|
|
44
|
+
delete this.refreshTokenValue;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if tokens are expired or about to expire
|
|
48
|
+
*/
|
|
49
|
+
isTokenExpired() {
|
|
50
|
+
if (!this.idToken)
|
|
51
|
+
return true;
|
|
52
|
+
try {
|
|
53
|
+
// Basic JWT parsing to check expiration
|
|
54
|
+
const parts = this.idToken.split('.');
|
|
55
|
+
if (parts.length !== 3)
|
|
56
|
+
return true;
|
|
57
|
+
const payload = JSON.parse(atob(parts[1] || ''));
|
|
58
|
+
const now = Math.floor(Date.now() / 1000);
|
|
59
|
+
const buffer = 300; // 5 minute buffer
|
|
60
|
+
return payload.exp && (payload.exp - buffer) <= now;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// If we can't parse the token, consider it expired
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ensure tokens are valid before making a request
|
|
69
|
+
* Override in subclasses to implement token refresh logic
|
|
70
|
+
* @returns Promise<boolean> - true if tokens are valid, false otherwise
|
|
71
|
+
*/
|
|
72
|
+
async ensureValidTokens() {
|
|
73
|
+
// Default implementation: just check if token exists and is not expired
|
|
74
|
+
return !this.isTokenExpired() && !!this.idToken;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Log message (uses passed logger, log function, or falls back to console)
|
|
78
|
+
*/
|
|
79
|
+
log(level, message, data) {
|
|
80
|
+
if (this.apiClientLogger) {
|
|
81
|
+
// Use the passed logger instance (preferred for module-based logging)
|
|
82
|
+
this.apiClientLogger.log('api-client', level, message, data);
|
|
83
|
+
}
|
|
84
|
+
else if (this.logFunction) {
|
|
85
|
+
// Use the passed logging function (e.g., from agent)
|
|
86
|
+
this.logFunction(level, message, data);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return; // don't log anything
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async makeRequest(method, path, data, requiresAuth = true) {
|
|
93
|
+
const url = `${this.baseUrl}${path}`;
|
|
94
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
95
|
+
// Log request initiation
|
|
96
|
+
this.log('debug', `[${requestId}] Starting API request`, {
|
|
97
|
+
method,
|
|
98
|
+
path,
|
|
99
|
+
url,
|
|
100
|
+
requiresAuth,
|
|
101
|
+
hasData: !!data,
|
|
102
|
+
dataSize: data ? JSON.stringify(data).length : 0,
|
|
103
|
+
timestamp: new Date().toISOString()
|
|
104
|
+
});
|
|
105
|
+
const headers = {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
};
|
|
108
|
+
// Add cache prevention headers if caching is disabled
|
|
109
|
+
if (this.disableCaching) {
|
|
110
|
+
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
|
111
|
+
headers['Pragma'] = 'no-cache';
|
|
112
|
+
headers['Expires'] = '0';
|
|
113
|
+
}
|
|
114
|
+
if (requiresAuth) {
|
|
115
|
+
// Ensure tokens are valid before making the request
|
|
116
|
+
if (this.idToken || this.refreshTokenValue) {
|
|
117
|
+
const tokensValid = await this.ensureValidTokens();
|
|
118
|
+
if (!tokensValid) {
|
|
119
|
+
this.log('error', `[${requestId}] Tokens are not valid and could not be refreshed`, {
|
|
120
|
+
url,
|
|
121
|
+
method,
|
|
122
|
+
hasIdToken: !!this.idToken,
|
|
123
|
+
hasRefreshToken: !!this.refreshTokenValue,
|
|
124
|
+
tokenExpired: this.isTokenExpired(),
|
|
125
|
+
timestamp: new Date().toISOString()
|
|
126
|
+
});
|
|
127
|
+
throw new ApiClientError('Authentication failed - no valid tokens available', 401);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!this.idToken) {
|
|
131
|
+
this.log('error', `[${requestId}] Authentication required but no ID token available`, {
|
|
132
|
+
url,
|
|
133
|
+
method,
|
|
134
|
+
hasRefreshToken: !!this.refreshTokenValue,
|
|
135
|
+
timestamp: new Date().toISOString()
|
|
136
|
+
});
|
|
137
|
+
throw new ApiClientError('Authentication required but no token available', 401);
|
|
138
|
+
}
|
|
139
|
+
// Validate token format
|
|
140
|
+
if (!this.idToken.includes('.')) {
|
|
141
|
+
this.log('error', `[${requestId}] Invalid token format - token must be a valid JWT`, {
|
|
142
|
+
tokenPreview: this.idToken.substring(0, 20) + '...'
|
|
143
|
+
});
|
|
144
|
+
throw new Error('Invalid token format - token must be a valid JWT');
|
|
145
|
+
}
|
|
146
|
+
headers['Authorization'] = `${this.idToken}`;
|
|
147
|
+
// Log authentication details
|
|
148
|
+
this.log('debug', `[${requestId}] Request authenticated`, {
|
|
149
|
+
url,
|
|
150
|
+
method,
|
|
151
|
+
hasIdToken: !!this.idToken,
|
|
152
|
+
hasAccessToken: !!this.accessToken,
|
|
153
|
+
hasRefreshToken: !!this.refreshTokenValue,
|
|
154
|
+
tokenPreview: this.idToken.substring(0, 20) + '...',
|
|
155
|
+
authHeaderPreview: headers['Authorization'].substring(0, 30) + '...',
|
|
156
|
+
tokenExpired: this.isTokenExpired(),
|
|
157
|
+
timestamp: new Date().toISOString()
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Log unauthenticated request
|
|
162
|
+
this.log('debug', `[${requestId}] Unauthenticated request`, {
|
|
163
|
+
url,
|
|
164
|
+
method,
|
|
165
|
+
timestamp: new Date().toISOString()
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const config = {
|
|
169
|
+
method,
|
|
170
|
+
headers,
|
|
171
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
172
|
+
};
|
|
173
|
+
// Add cache prevention to fetch config if caching is disabled
|
|
174
|
+
if (this.disableCaching) {
|
|
175
|
+
config.cache = 'no-store';
|
|
176
|
+
}
|
|
177
|
+
if (data && (method === 'POST' || method === 'PUT')) {
|
|
178
|
+
config.body = JSON.stringify(data);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
// Log request execution
|
|
182
|
+
this.log('debug', `[${requestId}] Executing HTTP request`, {
|
|
183
|
+
method,
|
|
184
|
+
url,
|
|
185
|
+
headers: Object.keys(headers),
|
|
186
|
+
hasBody: !!config.body,
|
|
187
|
+
bodySize: config.body ? (typeof config.body === 'string' ? config.body.length : 'unknown') : 0,
|
|
188
|
+
timeout: this.timeout,
|
|
189
|
+
timestamp: new Date().toISOString()
|
|
190
|
+
});
|
|
191
|
+
const response = await fetch(url, config);
|
|
192
|
+
const responseData = await response.json();
|
|
193
|
+
// Log response details
|
|
194
|
+
this.log('debug', `[${requestId}] Received HTTP response`, {
|
|
195
|
+
method,
|
|
196
|
+
url,
|
|
197
|
+
status: response.status,
|
|
198
|
+
statusText: response.statusText,
|
|
199
|
+
responseSize: JSON.stringify(responseData).length,
|
|
200
|
+
hasResponseData: !!responseData,
|
|
201
|
+
timestamp: new Date().toISOString()
|
|
202
|
+
});
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
// Log failed response details
|
|
205
|
+
this.log('warn', `[${requestId}] Request failed with error status`, {
|
|
206
|
+
url,
|
|
207
|
+
method,
|
|
208
|
+
status: response.status,
|
|
209
|
+
statusText: response.statusText,
|
|
210
|
+
responseData,
|
|
211
|
+
hasRefreshToken: !!this.refreshTokenValue,
|
|
212
|
+
timestamp: new Date().toISOString()
|
|
213
|
+
});
|
|
214
|
+
// Handle 401 Unauthorized with automatic token refresh
|
|
215
|
+
if (response.status === 401 && this.refreshTokenValue && requiresAuth) {
|
|
216
|
+
this.log('info', `[${requestId}] ID token expired, attempting refresh`, {
|
|
217
|
+
url,
|
|
218
|
+
method,
|
|
219
|
+
hasRefreshToken: !!this.refreshTokenValue,
|
|
220
|
+
refreshTokenPreview: this.refreshTokenValue.substring(0, 20) + '...',
|
|
221
|
+
timestamp: new Date().toISOString()
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
const refreshResponse = await this.post('/auth/refresh', {
|
|
225
|
+
refreshToken: this.refreshTokenValue
|
|
226
|
+
}, false);
|
|
227
|
+
this.setAccessToken(refreshResponse.tokens.accessToken);
|
|
228
|
+
this.setIdToken(refreshResponse.tokens.idToken);
|
|
229
|
+
this.log('info', `[${requestId}] Tokens refreshed successfully, retrying request`, {
|
|
230
|
+
url,
|
|
231
|
+
method,
|
|
232
|
+
newTokenPreview: this.idToken?.substring(0, 20) + '...',
|
|
233
|
+
timestamp: new Date().toISOString()
|
|
234
|
+
});
|
|
235
|
+
// Retry the original request with new ID token
|
|
236
|
+
const retryHeaders = { ...headers };
|
|
237
|
+
retryHeaders['Authorization'] = `${this.idToken}`;
|
|
238
|
+
const retryConfig = {
|
|
239
|
+
...config,
|
|
240
|
+
headers: retryHeaders
|
|
241
|
+
};
|
|
242
|
+
this.log('debug', `[${requestId}] Retrying original request with new token`, {
|
|
243
|
+
url,
|
|
244
|
+
method,
|
|
245
|
+
newAuthHeaderPreview: retryHeaders['Authorization'].substring(0, 30) + '...',
|
|
246
|
+
timestamp: new Date().toISOString()
|
|
247
|
+
});
|
|
248
|
+
const retryResponse = await fetch(url, retryConfig);
|
|
249
|
+
const retryData = await retryResponse.json();
|
|
250
|
+
if (!retryResponse.ok) {
|
|
251
|
+
this.log('error', `[${requestId}] Retry request failed after token refresh`, {
|
|
252
|
+
url,
|
|
253
|
+
method,
|
|
254
|
+
status: retryResponse.status,
|
|
255
|
+
statusText: retryResponse.statusText,
|
|
256
|
+
responseData: retryData,
|
|
257
|
+
timestamp: new Date().toISOString()
|
|
258
|
+
});
|
|
259
|
+
throw new ApiClientError(retryData.message || retryData.error || 'Request failed after token refresh', retryResponse.status, retryData);
|
|
260
|
+
}
|
|
261
|
+
this.log('info', `[${requestId}] Retry request successful after token refresh`, {
|
|
262
|
+
url,
|
|
263
|
+
method,
|
|
264
|
+
status: retryResponse.status,
|
|
265
|
+
responseSize: JSON.stringify(retryData).length,
|
|
266
|
+
timestamp: new Date().toISOString()
|
|
267
|
+
});
|
|
268
|
+
return retryData;
|
|
269
|
+
}
|
|
270
|
+
catch (refreshError) {
|
|
271
|
+
this.log('error', `[${requestId}] Token refresh failed`, {
|
|
272
|
+
url,
|
|
273
|
+
method,
|
|
274
|
+
error: refreshError instanceof Error ? refreshError.message : String(refreshError),
|
|
275
|
+
hasRefreshToken: !!this.refreshTokenValue,
|
|
276
|
+
refreshTokenPreview: this.refreshTokenValue ? this.refreshTokenValue.substring(0, 20) + '...' : 'none',
|
|
277
|
+
timestamp: new Date().toISOString()
|
|
278
|
+
});
|
|
279
|
+
this.clearTokens();
|
|
280
|
+
throw new ApiClientError('Session expired. Please log in again.', 401, {
|
|
281
|
+
error: 'Token refresh failed',
|
|
282
|
+
originalError: refreshError instanceof Error ? refreshError.message : String(refreshError),
|
|
283
|
+
requestId
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Log other error responses
|
|
288
|
+
this.log('error', `[${requestId}] Request failed with status ${response.status}`, {
|
|
289
|
+
url,
|
|
290
|
+
method,
|
|
291
|
+
status: response.status,
|
|
292
|
+
statusText: response.statusText,
|
|
293
|
+
responseData,
|
|
294
|
+
timestamp: new Date().toISOString()
|
|
295
|
+
});
|
|
296
|
+
throw new ApiClientError(responseData.details ? JSON.stringify(responseData.details) : responseData.message || responseData.error || 'Request failed', response.status, { ...responseData, requestId });
|
|
297
|
+
}
|
|
298
|
+
// Log successful response
|
|
299
|
+
this.log('debug', `[${requestId}] Request completed successfully`, {
|
|
300
|
+
url,
|
|
301
|
+
method,
|
|
302
|
+
status: response.status,
|
|
303
|
+
statusText: response.statusText,
|
|
304
|
+
responseSize: JSON.stringify(responseData).length,
|
|
305
|
+
hasResponseData: !!responseData,
|
|
306
|
+
timestamp: new Date().toISOString()
|
|
307
|
+
});
|
|
308
|
+
return responseData;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error instanceof ApiClientError) {
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
// Log network/request errors
|
|
315
|
+
this.log('error', `[${requestId}] Request error occurred`, {
|
|
316
|
+
url,
|
|
317
|
+
method,
|
|
318
|
+
error: error instanceof Error ? error.message : String(error),
|
|
319
|
+
errorName: error && typeof error === 'object' && 'name' in error ? error.name : 'Unknown',
|
|
320
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
321
|
+
timestamp: new Date().toISOString()
|
|
322
|
+
});
|
|
323
|
+
// Handle errors that have name and message properties (including custom fetch errors)
|
|
324
|
+
if (error && typeof error === 'object' && 'name' in error && 'message' in error) {
|
|
325
|
+
const errorName = error.name;
|
|
326
|
+
const errorMessage = error.message;
|
|
327
|
+
if (errorName === 'AbortError') {
|
|
328
|
+
throw new ApiClientError('Request timeout', 408, { requestId, url });
|
|
329
|
+
}
|
|
330
|
+
// Handle TypeError: fetch failed (common in Node.js environments)
|
|
331
|
+
if (errorName === 'TypeError' && errorMessage === 'fetch failed') {
|
|
332
|
+
// Check if there's a cause with more specific error information
|
|
333
|
+
const cause = error.cause;
|
|
334
|
+
if (cause && cause.code === 'ENOTFOUND') {
|
|
335
|
+
throw new ApiClientError(`DNS resolution failed: Cannot resolve hostname for ${url}`, 0, { requestId, cause: cause.code });
|
|
336
|
+
}
|
|
337
|
+
throw new ApiClientError(`Network error: Cannot connect to ${url} - check if the server is running and accessible`, 0, { requestId });
|
|
338
|
+
}
|
|
339
|
+
// Handle other TypeError cases (like DNS resolution failures)
|
|
340
|
+
if (errorName === 'TypeError') {
|
|
341
|
+
throw new ApiClientError(`Network error: ${errorMessage} for ${url}`, 0, { requestId });
|
|
342
|
+
}
|
|
343
|
+
// Handle network errors more specifically
|
|
344
|
+
if (errorMessage.includes('fetch')) {
|
|
345
|
+
throw new ApiClientError(`Network error: ${errorMessage}`, 0, { requestId });
|
|
346
|
+
}
|
|
347
|
+
if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('getaddrinfo ENOTFOUND')) {
|
|
348
|
+
throw new ApiClientError(`DNS resolution failed: Cannot resolve hostname for ${url}`, 0, { requestId });
|
|
349
|
+
}
|
|
350
|
+
if (errorMessage.includes('ECONNREFUSED')) {
|
|
351
|
+
throw new ApiClientError(`Connection refused: Server at ${url} is not accepting connections`, 0, { requestId });
|
|
352
|
+
}
|
|
353
|
+
if (errorMessage.includes('ETIMEDOUT')) {
|
|
354
|
+
throw new ApiClientError(`Connection timeout: Server at ${url} did not respond in time`, 0, { requestId });
|
|
355
|
+
}
|
|
356
|
+
throw new ApiClientError(`Network error: ${errorMessage}`, 0, { requestId });
|
|
357
|
+
}
|
|
358
|
+
// Fallback for standard Error instances
|
|
359
|
+
if (error instanceof Error) {
|
|
360
|
+
if (error.name === 'AbortError') {
|
|
361
|
+
throw new ApiClientError('Request timeout', 408);
|
|
362
|
+
}
|
|
363
|
+
// Handle TypeError: fetch failed (common in Node.js environments)
|
|
364
|
+
if (error.name === 'TypeError' && error.message === 'fetch failed') {
|
|
365
|
+
// Check if there's a cause with more specific error information
|
|
366
|
+
const cause = error.cause;
|
|
367
|
+
if (cause && cause.code === 'ENOTFOUND') {
|
|
368
|
+
throw new ApiClientError(`DNS resolution failed: Cannot resolve hostname for ${url}`, 0);
|
|
369
|
+
}
|
|
370
|
+
throw new ApiClientError(`Network error: Cannot connect to ${url} - check if the server is running and accessible`, 0);
|
|
371
|
+
}
|
|
372
|
+
// Handle other TypeError cases (like DNS resolution failures)
|
|
373
|
+
if (error.name === 'TypeError') {
|
|
374
|
+
throw new ApiClientError(`Network error: ${error.message} for ${url}`, 0);
|
|
375
|
+
}
|
|
376
|
+
// Handle network errors more specifically
|
|
377
|
+
if (error.message.includes('fetch')) {
|
|
378
|
+
throw new ApiClientError(`Network error: ${error.message}`, 0);
|
|
379
|
+
}
|
|
380
|
+
if (error.message.includes('ENOTFOUND') || error.message.includes('getaddrinfo ENOTFOUND')) {
|
|
381
|
+
throw new ApiClientError(`DNS resolution failed: Cannot resolve hostname for ${url}`, 0);
|
|
382
|
+
}
|
|
383
|
+
if (error.message.includes('ECONNREFUSED')) {
|
|
384
|
+
throw new ApiClientError(`Connection refused: Server at ${url} is not accepting connections`, 0);
|
|
385
|
+
}
|
|
386
|
+
if (error.message.includes('ETIMEDOUT')) {
|
|
387
|
+
throw new ApiClientError(`Connection timeout: Server at ${url} did not respond in time`, 0);
|
|
388
|
+
}
|
|
389
|
+
throw new ApiClientError(`Network error: ${error.message}`, 0);
|
|
390
|
+
}
|
|
391
|
+
throw new ApiClientError(`Unknown error occurred: ${String(error)}`, 0);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async get(path, requiresAuth = true) {
|
|
395
|
+
// Add timestamp to prevent caching if caching is disabled
|
|
396
|
+
if (this.disableCaching) {
|
|
397
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
398
|
+
const cacheBuster = `${separator}_t=${Date.now()}`;
|
|
399
|
+
return this.makeRequest('GET', path + cacheBuster, undefined, requiresAuth);
|
|
400
|
+
}
|
|
401
|
+
return this.makeRequest('GET', path, undefined, requiresAuth);
|
|
402
|
+
}
|
|
403
|
+
async post(path, data, requiresAuth = true) {
|
|
404
|
+
return this.makeRequest('POST', path, data, requiresAuth);
|
|
405
|
+
}
|
|
406
|
+
async put(path, data, requiresAuth = true) {
|
|
407
|
+
return this.makeRequest('PUT', path, data, requiresAuth);
|
|
408
|
+
}
|
|
409
|
+
async delete(path, data, requiresAuth = true) {
|
|
410
|
+
return this.makeRequest('DELETE', path, data, requiresAuth);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ============================================================================
|
|
414
|
+
// ERROR CLASS
|
|
415
|
+
// ============================================================================
|
|
416
|
+
class ApiClientError extends Error {
|
|
417
|
+
constructor(message, statusCode, response) {
|
|
418
|
+
super(message);
|
|
419
|
+
this.name = 'ApiClientError';
|
|
420
|
+
this.statusCode = statusCode;
|
|
421
|
+
this.response = response;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
exports.ApiClientError = ApiClientError;
|
|
425
|
+
// ============================================================================
|
|
426
|
+
// MAIN API CLIENT
|
|
427
|
+
// ============================================================================
|
|
428
|
+
class ApiClient extends BaseApiClient {
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// AUTHENTICATION METHODS
|
|
431
|
+
// ============================================================================
|
|
432
|
+
/**
|
|
433
|
+
* Authenticate user with email and password
|
|
434
|
+
*/
|
|
435
|
+
async login(request) {
|
|
436
|
+
this.log('info', 'Starting user authentication', {
|
|
437
|
+
email: request.email,
|
|
438
|
+
hasPassword: !!request.password,
|
|
439
|
+
passwordLength: request.password?.length || 0,
|
|
440
|
+
timestamp: new Date().toISOString()
|
|
441
|
+
});
|
|
442
|
+
try {
|
|
443
|
+
const response = await this.post('/auth/authenticate', request, false);
|
|
444
|
+
this.log('info', 'Authentication successful, storing tokens', {
|
|
445
|
+
email: request.email,
|
|
446
|
+
hasAccessToken: !!response.tokens.accessToken,
|
|
447
|
+
hasIdToken: !!response.tokens.idToken,
|
|
448
|
+
hasRefreshToken: !!response.tokens.refreshToken,
|
|
449
|
+
accessTokenPreview: response.tokens.accessToken.substring(0, 20) + '...',
|
|
450
|
+
idTokenPreview: response.tokens.idToken.substring(0, 20) + '...',
|
|
451
|
+
refreshTokenPreview: response.tokens.refreshToken.substring(0, 20) + '...',
|
|
452
|
+
timestamp: new Date().toISOString()
|
|
453
|
+
});
|
|
454
|
+
this.setAccessToken(response.tokens.accessToken);
|
|
455
|
+
this.setIdToken(response.tokens.idToken);
|
|
456
|
+
this.setRefreshToken(response.tokens.refreshToken);
|
|
457
|
+
return response;
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
this.log('error', 'Authentication failed', {
|
|
461
|
+
email: request.email,
|
|
462
|
+
error: error instanceof Error ? error.message : String(error),
|
|
463
|
+
timestamp: new Date().toISOString()
|
|
464
|
+
});
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Refresh authentication token
|
|
470
|
+
*/
|
|
471
|
+
async refreshToken(request) {
|
|
472
|
+
this.log('info', 'Starting token refresh', {
|
|
473
|
+
hasRefreshToken: !!request.refreshToken,
|
|
474
|
+
refreshTokenPreview: request.refreshToken.substring(0, 20) + '...',
|
|
475
|
+
currentTokenStatus: {
|
|
476
|
+
hasIdToken: !!this.idToken,
|
|
477
|
+
hasAccessToken: !!this.accessToken,
|
|
478
|
+
tokenExpired: this.isTokenExpired()
|
|
479
|
+
},
|
|
480
|
+
timestamp: new Date().toISOString()
|
|
481
|
+
});
|
|
482
|
+
try {
|
|
483
|
+
const response = await this.post('/auth/refresh', request, false);
|
|
484
|
+
this.log('info', 'Token refresh successful, updating stored tokens', {
|
|
485
|
+
hasNewAccessToken: !!response.tokens.accessToken,
|
|
486
|
+
hasNewIdToken: !!response.tokens.idToken,
|
|
487
|
+
newAccessTokenPreview: response.tokens.accessToken.substring(0, 20) + '...',
|
|
488
|
+
newIdTokenPreview: response.tokens.idToken.substring(0, 20) + '...',
|
|
489
|
+
timestamp: new Date().toISOString()
|
|
490
|
+
});
|
|
491
|
+
this.setAccessToken(response.tokens.accessToken);
|
|
492
|
+
this.setIdToken(response.tokens.idToken);
|
|
493
|
+
// Note: refresh token is not returned on refresh, keep using the original
|
|
494
|
+
return response;
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
this.log('error', 'Token refresh failed', {
|
|
498
|
+
error: error instanceof Error ? error.message : String(error),
|
|
499
|
+
refreshTokenPreview: request.refreshToken.substring(0, 20) + '...',
|
|
500
|
+
timestamp: new Date().toISOString()
|
|
501
|
+
});
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Logout user and clear tokens
|
|
507
|
+
*/
|
|
508
|
+
async logout() {
|
|
509
|
+
this.log('info', 'Starting user logout', {
|
|
510
|
+
hasTokens: !!(this.idToken || this.accessToken || this.refreshTokenValue),
|
|
511
|
+
timestamp: new Date().toISOString()
|
|
512
|
+
});
|
|
513
|
+
try {
|
|
514
|
+
const response = await this.post('/auth/logout', {}, false);
|
|
515
|
+
this.clearTokens();
|
|
516
|
+
this.log('info', 'Logout successful, tokens cleared', {
|
|
517
|
+
timestamp: new Date().toISOString()
|
|
518
|
+
});
|
|
519
|
+
return response;
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.log('warn', 'Logout request failed, clearing tokens anyway', {
|
|
523
|
+
error: error instanceof Error ? error.message : String(error),
|
|
524
|
+
timestamp: new Date().toISOString()
|
|
525
|
+
});
|
|
526
|
+
this.clearTokens();
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Get challenge session for user with temporary password
|
|
532
|
+
*/
|
|
533
|
+
async getChallengeSession(request) {
|
|
534
|
+
return this.post('/auth/get-challenge-session', request, false);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Respond to NEW_PASSWORD_REQUIRED challenge
|
|
538
|
+
*/
|
|
539
|
+
async forceChangePassword(request) {
|
|
540
|
+
const response = await this.post('/auth/force-change-password', request, false);
|
|
541
|
+
// If successful, update stored tokens
|
|
542
|
+
if (response.tokens) {
|
|
543
|
+
this.setAccessToken(response.tokens.accessToken);
|
|
544
|
+
this.setIdToken(response.tokens.idToken);
|
|
545
|
+
this.setRefreshToken(response.tokens.refreshToken);
|
|
546
|
+
}
|
|
547
|
+
return response;
|
|
548
|
+
}
|
|
549
|
+
// ============================================================================
|
|
550
|
+
// ORGANIZATION METHODS
|
|
551
|
+
// ============================================================================
|
|
552
|
+
/**
|
|
553
|
+
* Create a new organization
|
|
554
|
+
*/
|
|
555
|
+
async createOrganization(request) {
|
|
556
|
+
return this.post('/organizations/createOrganization', request);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Get organization details
|
|
560
|
+
*/
|
|
561
|
+
async getOrganization(organizationId) {
|
|
562
|
+
return this.get(`/organizations/${organizationId}`);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Update organization information
|
|
566
|
+
*/
|
|
567
|
+
async updateOrganization(request) {
|
|
568
|
+
return this.put(`/organizations/${request.organizationId}`, request);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Delete an organization
|
|
572
|
+
*/
|
|
573
|
+
async deleteOrganization(organizationId) {
|
|
574
|
+
return this.delete(`/organizations/${organizationId}`, { organizationId });
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Get all users for an organization
|
|
578
|
+
*/
|
|
579
|
+
async getOrganizationUsers(organizationId) {
|
|
580
|
+
return this.get(`/organizations/${organizationId}/users`);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Add a user to an organization
|
|
584
|
+
*/
|
|
585
|
+
async addOrganizationUser(request) {
|
|
586
|
+
return this.post(`/organizations/${request.organizationId}/users`, request);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Remove a user from an organization
|
|
590
|
+
*/
|
|
591
|
+
async removeOrganizationUser(request) {
|
|
592
|
+
return this.delete(`/organizations/${request.organizationId}/users/${request.userEmail}`, request);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Update a user's role in an organization
|
|
596
|
+
*/
|
|
597
|
+
async updateOrganizationUserRole(request) {
|
|
598
|
+
return this.put(`/organizations/${request.organizationId}/users/${request.userEmail}/role`, request);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Adopt an orphaned organization
|
|
602
|
+
*/
|
|
603
|
+
async adoptOrganization(request) {
|
|
604
|
+
return this.post(`/organizations/${request.organizationId}/adopt`, request);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Get all orphaned organizations
|
|
608
|
+
*/
|
|
609
|
+
async getOrphanedOrganizations() {
|
|
610
|
+
return this.get('/organizations/orphaned');
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get all devices for an organization
|
|
614
|
+
*/
|
|
615
|
+
async getOrganizationDevices(organizationId) {
|
|
616
|
+
return this.get(`/organizations/${organizationId}/devices`);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Get all applications for an organization
|
|
620
|
+
*/
|
|
621
|
+
async getOrganizationApplications(organizationId) {
|
|
622
|
+
return this.get(`/organizations/${organizationId}/applications`);
|
|
623
|
+
}
|
|
624
|
+
// ============================================================================
|
|
625
|
+
// USER METHODS
|
|
626
|
+
// ============================================================================
|
|
627
|
+
/**
|
|
628
|
+
* Create a new user (public route for registration)
|
|
629
|
+
*/
|
|
630
|
+
async createUser(request) {
|
|
631
|
+
return this.post('/users/createUser', request, false);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Get user details
|
|
635
|
+
*/
|
|
636
|
+
async getUser(email) {
|
|
637
|
+
return this.get(`/users/${email}`);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Update user information
|
|
641
|
+
*/
|
|
642
|
+
async updateUser(request) {
|
|
643
|
+
return this.put(`/users/${request.email}`, request);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Delete a user
|
|
647
|
+
*/
|
|
648
|
+
async deleteUser(request) {
|
|
649
|
+
return this.delete(`/users/${request.email}`, request);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Get all organizations for a user
|
|
653
|
+
*/
|
|
654
|
+
async getUserOrganizations(email) {
|
|
655
|
+
return this.get(`/users/${email}/organizations`);
|
|
656
|
+
}
|
|
657
|
+
// ============================================================================
|
|
658
|
+
// APPLICATION METHODS
|
|
659
|
+
// ============================================================================
|
|
660
|
+
/**
|
|
661
|
+
* Create a new application
|
|
662
|
+
*/
|
|
663
|
+
async createApplication(request) {
|
|
664
|
+
return this.post('/applications/createApplication', request);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get application details
|
|
668
|
+
*/
|
|
669
|
+
async getApplication(applicationId) {
|
|
670
|
+
return this.get(`/applications/${applicationId}`);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Update application information
|
|
674
|
+
*/
|
|
675
|
+
async updateApplication(request) {
|
|
676
|
+
return this.put(`/applications/${request.applicationId}`, request);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Delete an application
|
|
680
|
+
*/
|
|
681
|
+
async deleteApplication(applicationId) {
|
|
682
|
+
return this.delete(`/applications/${applicationId}`);
|
|
683
|
+
}
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// CERTIFICATE METHODS
|
|
686
|
+
// ============================================================================
|
|
687
|
+
/**
|
|
688
|
+
* Get all certificates for an application
|
|
689
|
+
*/
|
|
690
|
+
async getCertificates(applicationId) {
|
|
691
|
+
return this.get(`/applications/${applicationId}/certificates`);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Get certificate for a specific hostname
|
|
695
|
+
*/
|
|
696
|
+
async getCertificate(applicationId, hostname) {
|
|
697
|
+
return this.get(`/applications/${applicationId}/certificates/hostname/${encodeURIComponent(hostname)}`);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Refresh/renew a certificate
|
|
701
|
+
*/
|
|
702
|
+
async refreshCertificate(applicationId, certificateId) {
|
|
703
|
+
return this.post(`/applications/${applicationId}/certificates/${certificateId}/refresh`);
|
|
704
|
+
}
|
|
705
|
+
// ============================================================================
|
|
706
|
+
// DEVICE METHODS
|
|
707
|
+
// ============================================================================
|
|
708
|
+
/**
|
|
709
|
+
* Create a new device
|
|
710
|
+
*/
|
|
711
|
+
async createDevice(request) {
|
|
712
|
+
return this.post('/devices/createDevice', request);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Get device details
|
|
716
|
+
*/
|
|
717
|
+
async getDevice(deviceId) {
|
|
718
|
+
return this.get(`/devices/${deviceId}`);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Update device information
|
|
722
|
+
*/
|
|
723
|
+
async updateDevice(request) {
|
|
724
|
+
return this.put(`/devices/${request.deviceId}`, request);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Delete a device
|
|
728
|
+
*/
|
|
729
|
+
async deleteDevice(deviceId) {
|
|
730
|
+
return this.delete(`/devices/${deviceId}`);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Create a device with an orphaned organization
|
|
734
|
+
*/
|
|
735
|
+
async createDeviceWithOrphanedOrganization(request) {
|
|
736
|
+
return this.post('/auth/createDeviceWithOrphanedOrg', request, false);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Get all applications for a device
|
|
740
|
+
*/
|
|
741
|
+
async getDeviceApplications(deviceId) {
|
|
742
|
+
return this.get(`/devices/${deviceId}/applications`);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Get device pool for an organization
|
|
746
|
+
*/
|
|
747
|
+
async getDevicePool(organizationId) {
|
|
748
|
+
return this.get(`/organizations/${organizationId}/pools`);
|
|
749
|
+
}
|
|
750
|
+
// WireGuard endpoints
|
|
751
|
+
async postDeviceWireGuard(applicationId, deviceId, body) {
|
|
752
|
+
return this.post(`/applications/${applicationId}/devices/${deviceId}/wireguard`, body);
|
|
753
|
+
}
|
|
754
|
+
async getDevicePeers(applicationId, deviceId) {
|
|
755
|
+
return this.get(`/applications/${applicationId}/devices/${deviceId}/peers`);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Get device's WireGuard config from pool
|
|
759
|
+
*/
|
|
760
|
+
async getPoolDeviceWireGuard(poolId, deviceId) {
|
|
761
|
+
try {
|
|
762
|
+
return await this.get(`/pools/${poolId}/devices/${deviceId}/wireguard`);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (error instanceof Error && 'statusCode' in error && error.statusCode === 404) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// ============================================================================
|
|
772
|
+
// ORGANIZATION INVITE METHODS
|
|
773
|
+
// ============================================================================
|
|
774
|
+
/**
|
|
775
|
+
* Create an organization invite
|
|
776
|
+
*/
|
|
777
|
+
async createOrganizationInvite(request) {
|
|
778
|
+
return this.post(`/organizations/${request.organizationId}/invites`, request);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Get all invites for an organization
|
|
782
|
+
*/
|
|
783
|
+
async getOrganizationInvites(organizationId, status) {
|
|
784
|
+
const queryParams = status ? `?status=${status}` : '';
|
|
785
|
+
return this.get(`/organizations/${organizationId}/invites${queryParams}`);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Accept an organization invite
|
|
789
|
+
*/
|
|
790
|
+
async acceptInvite(request) {
|
|
791
|
+
return this.post(`/invites/${request.inviteId}/accept`, request);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Decline an organization invite
|
|
795
|
+
*/
|
|
796
|
+
async declineInvite(request) {
|
|
797
|
+
return this.post(`/invites/${request.inviteId}/decline`, request);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Cancel an organization invite
|
|
801
|
+
*/
|
|
802
|
+
async cancelInvite(request) {
|
|
803
|
+
return this.delete(`/invites/${request.inviteId}`, request);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Get all invites for a user
|
|
807
|
+
*/
|
|
808
|
+
async getUserInvites(email, status) {
|
|
809
|
+
const queryParams = status ? `?status=${status}` : '';
|
|
810
|
+
return this.get(`/users/${email}/invites${queryParams}`);
|
|
811
|
+
}
|
|
812
|
+
// ============================================================================
|
|
813
|
+
// UTILITY METHODS
|
|
814
|
+
// ============================================================================
|
|
815
|
+
/**
|
|
816
|
+
* Test API connectivity
|
|
817
|
+
*/
|
|
818
|
+
async testConnection() {
|
|
819
|
+
return this.get('/test', false);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Check if user is authenticated
|
|
823
|
+
*/
|
|
824
|
+
isAuthenticated() {
|
|
825
|
+
return !!this.idToken;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Check if user has refresh token available
|
|
829
|
+
*/
|
|
830
|
+
hasRefreshToken() {
|
|
831
|
+
return !!this.refreshTokenValue;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Get current access token
|
|
835
|
+
*/
|
|
836
|
+
getAccessToken() {
|
|
837
|
+
return this.accessToken;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Get current ID token
|
|
841
|
+
*/
|
|
842
|
+
getIdToken() {
|
|
843
|
+
return this.idToken;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get current refresh token
|
|
847
|
+
*/
|
|
848
|
+
getRefreshToken() {
|
|
849
|
+
return this.refreshTokenValue;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Validate refresh token format
|
|
853
|
+
*/
|
|
854
|
+
isValidRefreshToken(token) {
|
|
855
|
+
if (!token || typeof token !== 'string') {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
// Basic validation - refresh tokens should be reasonably long
|
|
859
|
+
// Cognito refresh tokens can be JWT tokens or opaque tokens
|
|
860
|
+
// Accept tokens that are at least 20 characters (some Cognito tokens can be shorter)
|
|
861
|
+
if (token.length < 20) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
// Check for basic alphanumeric pattern (allowing for base64-like characters)
|
|
865
|
+
// Cognito tokens may contain dots (.) if they're JWT tokens with multiple parts
|
|
866
|
+
// Allow dots, plus, equals, underscores, hyphens, and alphanumeric
|
|
867
|
+
const validPattern = /^[A-Za-z0-9+/=_.-]+$/;
|
|
868
|
+
return validPattern.test(token);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Attempt to refresh tokens if they're expired or about to expire
|
|
872
|
+
*/
|
|
873
|
+
async ensureValidTokens() {
|
|
874
|
+
this.log('debug', 'Checking token validity', {
|
|
875
|
+
hasIdToken: !!this.idToken,
|
|
876
|
+
hasRefreshToken: !!this.refreshTokenValue,
|
|
877
|
+
tokenExpired: this.isTokenExpired(),
|
|
878
|
+
timestamp: new Date().toISOString()
|
|
879
|
+
});
|
|
880
|
+
if (!this.isTokenExpired() && this.idToken) {
|
|
881
|
+
this.log('debug', 'Tokens are still valid');
|
|
882
|
+
return true; // Tokens are still valid
|
|
883
|
+
}
|
|
884
|
+
if (!this.refreshTokenValue) {
|
|
885
|
+
this.log('warn', 'No refresh token available for token refresh');
|
|
886
|
+
return false; // No refresh token available
|
|
887
|
+
}
|
|
888
|
+
// Validate refresh token format before attempting refresh
|
|
889
|
+
if (!this.isValidRefreshToken(this.refreshTokenValue)) {
|
|
890
|
+
this.log('warn', 'Invalid refresh token format, clearing tokens', {
|
|
891
|
+
refreshTokenPreview: this.refreshTokenValue.substring(0, 20) + '...'
|
|
892
|
+
});
|
|
893
|
+
this.clearTokens();
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
// Prevent concurrent refresh attempts
|
|
897
|
+
if (this.refreshPromise) {
|
|
898
|
+
this.log('debug', 'Token refresh already in progress, waiting...');
|
|
899
|
+
return await this.refreshPromise;
|
|
900
|
+
}
|
|
901
|
+
this.log('info', 'Starting token refresh process');
|
|
902
|
+
this.refreshPromise = this.performTokenRefresh();
|
|
903
|
+
const result = await this.refreshPromise;
|
|
904
|
+
this.refreshPromise = null;
|
|
905
|
+
this.log('debug', 'Token refresh process completed', {
|
|
906
|
+
success: result,
|
|
907
|
+
timestamp: new Date().toISOString()
|
|
908
|
+
});
|
|
909
|
+
return result;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Perform the actual token refresh operation
|
|
913
|
+
*/
|
|
914
|
+
async performTokenRefresh() {
|
|
915
|
+
try {
|
|
916
|
+
this.log('debug', 'Starting token refresh...');
|
|
917
|
+
const refreshResponse = await this.post('/auth/refresh', {
|
|
918
|
+
refreshToken: this.refreshTokenValue
|
|
919
|
+
}, false);
|
|
920
|
+
this.setAccessToken(refreshResponse.tokens.accessToken);
|
|
921
|
+
this.setIdToken(refreshResponse.tokens.idToken);
|
|
922
|
+
this.log('debug', 'Token refresh successful');
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
// Clear tokens if refresh fails
|
|
927
|
+
this.log('warn', 'Token refresh failed, clearing tokens');
|
|
928
|
+
this.clearTokens();
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Execute a request with automatic retry and token refresh
|
|
934
|
+
*/
|
|
935
|
+
async executeWithRetry(operation, maxRetries = 3, baseDelay = 1000) {
|
|
936
|
+
let lastError = null;
|
|
937
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
938
|
+
try {
|
|
939
|
+
// Ensure we have valid tokens before making the request
|
|
940
|
+
if (!await this.ensureValidTokens()) {
|
|
941
|
+
throw new ApiClientError('Authentication failed - no valid tokens available', 401);
|
|
942
|
+
}
|
|
943
|
+
return await operation();
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
947
|
+
// If it's an authentication error and we have retries left, try to refresh tokens
|
|
948
|
+
if (error instanceof ApiClientError && error.statusCode === 401 && attempt < maxRetries) {
|
|
949
|
+
this.clearTokens();
|
|
950
|
+
// Wait before retrying with exponential backoff
|
|
951
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
952
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
// If it's not an auth error or we're out of retries, throw the error
|
|
956
|
+
if (attempt === maxRetries) {
|
|
957
|
+
throw lastError;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
throw lastError || new Error('Max retries exceeded');
|
|
962
|
+
}
|
|
963
|
+
// Gateway Management Methods
|
|
964
|
+
/**
|
|
965
|
+
* Create a gateway device and EC2 instance
|
|
966
|
+
*/
|
|
967
|
+
async createGateway(request) {
|
|
968
|
+
return this.post('/gateways', request);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Get a specific gateway
|
|
972
|
+
*/
|
|
973
|
+
async getGateway(gatewayId) {
|
|
974
|
+
return this.get(`/gateways/${gatewayId}`);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* List all gateways for an organization
|
|
978
|
+
*/
|
|
979
|
+
async listGateways(request) {
|
|
980
|
+
return this.get(`/organizations/${request.organizationId}/gateways?type=${request.type || 'gateway'}`);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Delete a gateway
|
|
984
|
+
*/
|
|
985
|
+
async deleteGateway(request) {
|
|
986
|
+
return this.delete(`/gateways/${request.gatewayId}`, request);
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Resync agent on a gateway
|
|
990
|
+
*/
|
|
991
|
+
async resyncGatewayAgent(request) {
|
|
992
|
+
return this.post(`/gateways/${request.gatewayId}/resync`, request);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Get applications for a specific gateway
|
|
996
|
+
*/
|
|
997
|
+
async getGatewayApplications(request) {
|
|
998
|
+
return this.get(`/gateways/${request.gatewayId}/applications`);
|
|
999
|
+
}
|
|
1000
|
+
// Managed Gateway Methods (admin only)
|
|
1001
|
+
/**
|
|
1002
|
+
* Create a managed gateway
|
|
1003
|
+
*/
|
|
1004
|
+
async createManagedGateway(request) {
|
|
1005
|
+
return this.post('/admin/managed-gateways', request);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* List all managed gateways
|
|
1009
|
+
*/
|
|
1010
|
+
async listManagedGateways() {
|
|
1011
|
+
return this.get('/admin/managed-gateways');
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Get managed gateway details
|
|
1015
|
+
*/
|
|
1016
|
+
async getManagedGateway(request) {
|
|
1017
|
+
return this.get(`/admin/managed-gateways/${request.gatewayId}`);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Delete a managed gateway
|
|
1021
|
+
*/
|
|
1022
|
+
async deleteManagedGateway(request) {
|
|
1023
|
+
return this.delete(`/admin/managed-gateways/${request.gatewayId}`, request);
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Get SSH key for a managed gateway (admin only)
|
|
1027
|
+
*/
|
|
1028
|
+
async getManagedGatewaySSHKey(request) {
|
|
1029
|
+
return this.get(`/admin/managed-gateways/${request.gatewayId}/ssh-key`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
exports.ApiClient = ApiClient;
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
// FACTORY FUNCTION
|
|
1035
|
+
// ============================================================================
|
|
1036
|
+
/**
|
|
1037
|
+
* Create a new API client instance
|
|
1038
|
+
* @param baseUrl - The base URL of the API (from environment variable)
|
|
1039
|
+
* @param timeout - Request timeout in milliseconds (optional, defaults to 30s)
|
|
1040
|
+
* @param disableCaching - Whether to disable caching (optional, defaults to true)
|
|
1041
|
+
* @param logFunction - Optional logging function for integration with external logging systems
|
|
1042
|
+
* @param apiClientLogger - Optional logger instance for module-based logging
|
|
1043
|
+
*/
|
|
1044
|
+
function createApiClient(baseUrl, timeout, disableCaching, logFunction, apiClientLogger) {
|
|
1045
|
+
return new ApiClient({
|
|
1046
|
+
baseUrl,
|
|
1047
|
+
timeout: timeout || 30000,
|
|
1048
|
+
disableCaching: disableCaching || false,
|
|
1049
|
+
logFunction,
|
|
1050
|
+
apiClientLogger
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
// ============================================================================
|
|
1054
|
+
// DEFAULT EXPORT
|
|
1055
|
+
// ============================================================================
|
|
1056
|
+
exports.default = ApiClient;
|
|
1057
|
+
//# sourceMappingURL=api-client.js.map
|