appmesh 1.2.3 → 1.3.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/README.md +64 -48
- package/dist/appmesh.esm.js +1 -1
- package/package.json +6 -1
- package/src/appmesh.js +310 -153
- package/src/appmesh_tcp.js +408 -272
- package/src/server.js +175 -81
package/src/appmesh.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// appmesh.js
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
|
|
4
|
-
// Lazy-resolved Node.js https
|
|
4
|
+
// Lazy-resolved Node.js https/fs modules (null in browser)
|
|
5
5
|
let _https = null;
|
|
6
|
-
let
|
|
6
|
+
let _fs = null;
|
|
7
|
+
let _httpsReady = null; // Promise that resolves when https/fs are loaded
|
|
7
8
|
|
|
8
9
|
// Constants using Object.freeze to prevent modifications
|
|
9
10
|
const CONSTANTS = Object.freeze({
|
|
@@ -14,8 +15,6 @@ const CONSTANTS = Object.freeze({
|
|
|
14
15
|
DEFAULT_RUN_APP_TIMEOUT_SECONDS: "P2D",
|
|
15
16
|
DEFAULT_RUN_APP_LIFECYCLE_SECONDS: "P2DT12H",
|
|
16
17
|
DEFAULT_JWT_AUDIENCE: "appmesh-service",
|
|
17
|
-
HTTP_HEADER_NAME_CSRF_TOKEN: "X-CSRF-Token",
|
|
18
|
-
HTTP_COOKIE_NAME_CSRF_TOKEN: "appmesh_csrf_token",
|
|
19
18
|
HTTP_HEADER_KEY_AUTH: "Authorization",
|
|
20
19
|
HTTP_HEADER_KEY_X_TARGET_HOST: "X-Target-Host",
|
|
21
20
|
HTTP_HEADER_KEY_X_FILE_PATH: "X-File-Path",
|
|
@@ -44,16 +43,68 @@ class AppMeshError extends Error {
|
|
|
44
43
|
* @param {string} message - Error message
|
|
45
44
|
* @param {number|null} statusCode - HTTP status code
|
|
46
45
|
* @param {any} responseData - Raw response data
|
|
46
|
+
* @param {string|null} errorCode - Machine-readable error code
|
|
47
47
|
*/
|
|
48
|
-
constructor(message, statusCode = null, responseData = null) {
|
|
48
|
+
constructor(message, statusCode = null, responseData = null, errorCode = null) {
|
|
49
49
|
super(message);
|
|
50
50
|
this.name = 'AppMeshError';
|
|
51
51
|
this.statusCode = statusCode;
|
|
52
52
|
this.responseData = responseData;
|
|
53
|
+
this.errorCode = errorCode;
|
|
53
54
|
this.timestamp = new Date().toISOString();
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Error thrown when the server requires a TOTP code to complete login (HTTP 428).
|
|
60
|
+
*
|
|
61
|
+
* Subclasses AppMeshError with `statusCode` fixed to 428, so existing catch-based
|
|
62
|
+
* callers keep working. Carries the server-issued challenge as `totpChallenge`,
|
|
63
|
+
* ready to pass to `validate_totp(username, totpChallenge, totpCode)`.
|
|
64
|
+
*/
|
|
65
|
+
class TotpRequiredError extends AppMeshError {
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} message - Error message
|
|
68
|
+
* @param {any} responseData - Raw 428 response body (object or JSON string containing `totp_challenge`)
|
|
69
|
+
*/
|
|
70
|
+
constructor(message, responseData = null) {
|
|
71
|
+
super(message, 428, responseData, 'TOTP_REQUIRED');
|
|
72
|
+
this.name = 'TotpRequiredError';
|
|
73
|
+
let data = responseData;
|
|
74
|
+
if (typeof data === 'string') {
|
|
75
|
+
try { data = JSON.parse(data); } catch (_) { data = null; }
|
|
76
|
+
}
|
|
77
|
+
/** @type {string|null} Server-issued TOTP challenge for validate_totp() */
|
|
78
|
+
this.totpChallenge = (data && typeof data === 'object' && typeof data.totp_challenge === 'string')
|
|
79
|
+
? data.totp_challenge
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* App removed before its async-run exit was observed. Typed replacement for the
|
|
86
|
+
* old -1 sentinel, keeping real (possibly negative) exit codes unambiguous.
|
|
87
|
+
*/
|
|
88
|
+
class AppRemovedError extends AppMeshError {
|
|
89
|
+
/** @param {string} message - Error message */
|
|
90
|
+
constructor(message) {
|
|
91
|
+
super(message, null, null, 'APP_REMOVED');
|
|
92
|
+
this.name = 'AppRemovedError';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Transport disconnected while waiting for an async run, or the daemon delivered
|
|
98
|
+
* an unparseable exit code. Typed replacement for the old -2 sentinel exit code.
|
|
99
|
+
*/
|
|
100
|
+
class TransportDisconnectedError extends AppMeshError {
|
|
101
|
+
/** @param {string} message - Error message */
|
|
102
|
+
constructor(message) {
|
|
103
|
+
super(message, null, null, 'TRANSPORT_DISCONNECTED');
|
|
104
|
+
this.name = 'TransportDisconnectedError';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
57
108
|
// Default output handler
|
|
58
109
|
const defaultOutputHandler = (output, position) => {
|
|
59
110
|
if (ENV.isNode) {
|
|
@@ -141,6 +192,20 @@ function parseDuration(duration) {
|
|
|
141
192
|
return seconds;
|
|
142
193
|
}
|
|
143
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Case-insensitive header lookup (axios lowercases names; the TCP transport keeps the daemon's case).
|
|
197
|
+
* @param {Object} headers - Response headers
|
|
198
|
+
* @param {string} name - Header name (any case)
|
|
199
|
+
* @returns {*} Header value or undefined
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
function _getHeader(headers, name) {
|
|
203
|
+
if (!headers) return undefined;
|
|
204
|
+
const lower = name.toLowerCase();
|
|
205
|
+
const key = Object.keys(headers).find(k => k.toLowerCase() === lower);
|
|
206
|
+
return key === undefined ? undefined : headers[key];
|
|
207
|
+
}
|
|
208
|
+
|
|
144
209
|
/**
|
|
145
210
|
* Resolve a user name or numeric string to a UID (Node.js/Unix only).
|
|
146
211
|
* @param {string} user - User name or numeric UID
|
|
@@ -199,6 +264,31 @@ function _decodeJwtExp(token) {
|
|
|
199
264
|
}
|
|
200
265
|
}
|
|
201
266
|
|
|
267
|
+
// Default App Mesh CA bundle, preferred when no sslConfig is given and the file exists
|
|
268
|
+
const DEFAULT_CA_FILE = '/opt/appmesh/ssl/ca.pem';
|
|
269
|
+
|
|
270
|
+
// PEM material: Buffers/inline PEM pass through; any other string is a file path —
|
|
271
|
+
// a missing path is a hard error, never a silent no-verify fallback.
|
|
272
|
+
function _loadPem(value) {
|
|
273
|
+
return (typeof value === 'string' && !value.includes('-----BEGIN')) ? _fs.readFileSync(value) : value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Resolve the sslConfig tri-state into https.Agent TLS options (Node.js only):
|
|
277
|
+
// null/undefined = App Mesh default CA if installed else system CAs; false = explicit
|
|
278
|
+
// insecure; object = custom ca/cert/key (rejectUnauthorized defaults to true).
|
|
279
|
+
function _resolveSslOptions(sslConfig) {
|
|
280
|
+
if (sslConfig === false) return { rejectUnauthorized: false };
|
|
281
|
+
if (!sslConfig) {
|
|
282
|
+
return _fs.existsSync(DEFAULT_CA_FILE)
|
|
283
|
+
? { ca: _fs.readFileSync(DEFAULT_CA_FILE), rejectUnauthorized: true }
|
|
284
|
+
: { rejectUnauthorized: true }; // fall back to system trust roots, verification stays on
|
|
285
|
+
}
|
|
286
|
+
const opts = { ...sslConfig };
|
|
287
|
+
for (const k of ['ca', 'cert', 'key']) if (opts[k]) opts[k] = _loadPem(opts[k]);
|
|
288
|
+
if (typeof opts.rejectUnauthorized !== 'boolean') opts.rejectUnauthorized = true;
|
|
289
|
+
return opts;
|
|
290
|
+
}
|
|
291
|
+
|
|
202
292
|
/**
|
|
203
293
|
* AppMesh REST Service client
|
|
204
294
|
*/
|
|
@@ -206,7 +296,10 @@ class AppMeshClient {
|
|
|
206
296
|
/**
|
|
207
297
|
* Initialize AppMesh client
|
|
208
298
|
* @param {string} baseURL - Service URL
|
|
209
|
-
* @param {Object} [sslConfig] - SSL
|
|
299
|
+
* @param {Object|false|null} [sslConfig] - SSL tri-state: `null`/`undefined` (default) verifies
|
|
300
|
+
* against the App Mesh default CA (/opt/appmesh/ssl/ca.pem) if installed, else the system CAs;
|
|
301
|
+
* `false` disables verification; an object supplies custom `ca`/`cert`/`key` (Buffer, inline
|
|
302
|
+
* PEM, or file path — a missing path is a hard error).
|
|
210
303
|
* @example
|
|
211
304
|
* const sslConfig = {
|
|
212
305
|
* cert: fs.readFileSync("client.pem"),
|
|
@@ -222,6 +315,10 @@ class AppMeshClient {
|
|
|
222
315
|
// Host to forward requests to
|
|
223
316
|
this.forwardingHost = null;
|
|
224
317
|
|
|
318
|
+
// Current JWT token known to this client (single token store; transports
|
|
319
|
+
// sync from it via _handleTokenUpdate/_getAccessToken)
|
|
320
|
+
this._token = null;
|
|
321
|
+
|
|
225
322
|
// Configure axios instance
|
|
226
323
|
const axiosConfig = {
|
|
227
324
|
baseURL,
|
|
@@ -233,10 +330,11 @@ class AppMeshClient {
|
|
|
233
330
|
this._sslConfig = sslConfig;
|
|
234
331
|
this._client = axios.create(axiosConfig);
|
|
235
332
|
|
|
236
|
-
// Node.js only: start loading https
|
|
333
|
+
// Node.js only: start loading https/fs modules (resolved before first request)
|
|
237
334
|
if (ENV.isNode && !_httpsReady) {
|
|
238
|
-
_httpsReady = import('https').then(
|
|
239
|
-
_https =
|
|
335
|
+
_httpsReady = Promise.all([import('https'), import('fs')]).then(([h, f]) => {
|
|
336
|
+
_https = h.default || h;
|
|
337
|
+
_fs = f.default || f;
|
|
240
338
|
}).catch(() => { /* browser bundle — ignore */ });
|
|
241
339
|
}
|
|
242
340
|
|
|
@@ -258,9 +356,10 @@ class AppMeshClient {
|
|
|
258
356
|
this._client.interceptors.response.use(
|
|
259
357
|
response => response,
|
|
260
358
|
error => {
|
|
261
|
-
//
|
|
262
|
-
const err = new AppMeshError('Request failed: ' + (error.message || 'Unknown error'), error.response?.status, error.response?.data);
|
|
263
|
-
|
|
359
|
+
// Network-level failure (DNS, connection refused, timeout, ...)
|
|
360
|
+
const err = new AppMeshError('Request failed: ' + (error.message || 'Unknown error'), error.response?.status ?? null, error.response?.data ?? null);
|
|
361
|
+
err.cause = error;
|
|
362
|
+
return Promise.reject(err);
|
|
264
363
|
}
|
|
265
364
|
);
|
|
266
365
|
}
|
|
@@ -272,8 +371,20 @@ class AppMeshClient {
|
|
|
272
371
|
* @param {string} [totpCode] - TOTP code if 2FA is enabled
|
|
273
372
|
* @param {string|number} [tokenExpire] - Token expiry (integer seconds or ISO 8601 string)
|
|
274
373
|
* @param {string} [audience] - JWT audience
|
|
275
|
-
* @returns {Promise<void>} Resolves when login succeeds.
|
|
276
|
-
*
|
|
374
|
+
* @returns {Promise<void>} Resolves when login succeeds.
|
|
375
|
+
* @throws {TotpRequiredError} When the server requires TOTP (HTTP 428) and no valid code was
|
|
376
|
+
* supplied. Catch it, read `error.totpChallenge`, then complete the login with
|
|
377
|
+
* `validate_totp(username, error.totpChallenge, totpCode)`:
|
|
378
|
+
* @example
|
|
379
|
+
* try {
|
|
380
|
+
* await client.login(user, pass);
|
|
381
|
+
* } catch (err) {
|
|
382
|
+
* if (err instanceof TotpRequiredError) {
|
|
383
|
+
* await client.validate_totp(user, err.totpChallenge, totpCode);
|
|
384
|
+
* } else {
|
|
385
|
+
* throw err;
|
|
386
|
+
* }
|
|
387
|
+
* }
|
|
277
388
|
*/
|
|
278
389
|
async login(username, password, totpCode = null, tokenExpire = CONSTANTS.DEFAULT_TOKEN_EXPIRE_SECONDS, audience = CONSTANTS.DEFAULT_JWT_AUDIENCE) {
|
|
279
390
|
// Validate inputs
|
|
@@ -302,21 +413,17 @@ class AppMeshClient {
|
|
|
302
413
|
* @param {boolean} [updateSession=true] - When true, updates this client session with the
|
|
303
414
|
* verified token and persists local auth state on success. When false, the token is only
|
|
304
415
|
* verified and local state is unchanged.
|
|
305
|
-
* @returns {Promise<{success: boolean, responseText: string}>} Verification result
|
|
416
|
+
* @returns {Promise<{success: boolean, responseText: string}>} Verification result (success is always true).
|
|
417
|
+
* @throws {AppMeshError} If verification fails (invalid token, permission or audience mismatch, network error).
|
|
306
418
|
*/
|
|
307
419
|
async authenticate(token, permission = null, audience = CONSTANTS.DEFAULT_JWT_AUDIENCE, updateSession = true) {
|
|
308
420
|
const headers = { Authorization: `Bearer ${token}` };
|
|
309
421
|
if (permission) headers["X-Permission"] = permission;
|
|
310
422
|
if (audience) headers["X-Audience"] = audience;
|
|
311
423
|
if (updateSession) headers["X-Set-Cookie"] = "true";
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return { success: true, responseText };
|
|
316
|
-
} catch (error) {
|
|
317
|
-
const responseText = error instanceof AppMeshError ? error.message : String(error);
|
|
318
|
-
return { success: false, responseText };
|
|
319
|
-
}
|
|
424
|
+
const response = await this._request("post", "/appmesh/auth", null, { headers });
|
|
425
|
+
const responseText = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
|
|
426
|
+
return { success: true, responseText };
|
|
320
427
|
}
|
|
321
428
|
|
|
322
429
|
/**
|
|
@@ -327,20 +434,8 @@ class AppMeshClient {
|
|
|
327
434
|
* header; in browsers it cannot replace the HttpOnly auth cookie.
|
|
328
435
|
*/
|
|
329
436
|
set_token(token) {
|
|
330
|
-
const COOKIE_NAME = 'appmesh_auth_token';
|
|
331
437
|
if (ENV.isNode) {
|
|
332
|
-
|
|
333
|
-
const cookies = existingCookies.split('; ').filter(c =>
|
|
334
|
-
c && !c.startsWith(COOKIE_NAME + '=')
|
|
335
|
-
);
|
|
336
|
-
cookies.push(`${COOKIE_NAME}=${token}`);
|
|
337
|
-
this._client.defaults.headers.Cookie = cookies.join('; ');
|
|
338
|
-
this._autoRefreshJwt = token;
|
|
339
|
-
if (this._autoRefreshEnabled) {
|
|
340
|
-
this._stopAutoRefresh();
|
|
341
|
-
this._autoRefreshEnabled = true;
|
|
342
|
-
this._scheduleTokenRefresh();
|
|
343
|
-
}
|
|
438
|
+
this._handleTokenUpdate(token);
|
|
344
439
|
} else {
|
|
345
440
|
// Browser: auth token is HttpOnly (set by server via Set-Cookie),
|
|
346
441
|
// document.cookie cannot access or override HttpOnly cookies.
|
|
@@ -349,6 +444,41 @@ class AppMeshClient {
|
|
|
349
444
|
}
|
|
350
445
|
}
|
|
351
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Get the current access token known to this client.
|
|
449
|
+
* @returns {string|null} Current JWT token or null
|
|
450
|
+
* @protected
|
|
451
|
+
*/
|
|
452
|
+
_getAccessToken() {
|
|
453
|
+
return this._token || null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Store a new access token and propagate it to the transport layer.
|
|
458
|
+
* Called after login/renew/authenticate responses and by set_token().
|
|
459
|
+
* Base implementation keeps the token in memory, syncs the Node.js outgoing
|
|
460
|
+
* Cookie header, and reschedules auto-refresh when enabled.
|
|
461
|
+
* Subclasses using other transports (e.g. TCP) override this to sync their own store.
|
|
462
|
+
* @param {string|null} token - New JWT token (falsy clears the in-memory token)
|
|
463
|
+
* @protected
|
|
464
|
+
*/
|
|
465
|
+
_handleTokenUpdate(token) {
|
|
466
|
+
const COOKIE_NAME = 'appmesh_auth_token';
|
|
467
|
+
this._token = token || null;
|
|
468
|
+
if (ENV.isNode && this._token) {
|
|
469
|
+
const existingCookies = this._client.defaults.headers.Cookie || '';
|
|
470
|
+
const cookies = existingCookies.split('; ').filter(c =>
|
|
471
|
+
c && !c.startsWith(COOKIE_NAME + '=')
|
|
472
|
+
);
|
|
473
|
+
cookies.push(`${COOKIE_NAME}=${this._token}`);
|
|
474
|
+
this._client.defaults.headers.Cookie = cookies.join('; ');
|
|
475
|
+
}
|
|
476
|
+
this._autoRefreshJwt = this._token;
|
|
477
|
+
if (this._autoRefreshEnabled) {
|
|
478
|
+
this._scheduleTokenRefresh();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
352
482
|
/**
|
|
353
483
|
* Logout from the current session.
|
|
354
484
|
*/
|
|
@@ -364,12 +494,12 @@ class AppMeshClient {
|
|
|
364
494
|
}
|
|
365
495
|
|
|
366
496
|
this._stopAutoRefresh();
|
|
497
|
+
this._token = null;
|
|
367
498
|
|
|
368
|
-
//
|
|
499
|
+
// Clear the outgoing auth cookie (Node); the browser's HttpOnly auth cookie is cleared
|
|
500
|
+
// server-side on logoff.
|
|
369
501
|
if (ENV.isNode) {
|
|
370
502
|
this._client.defaults.headers.Cookie = null;
|
|
371
|
-
} else {
|
|
372
|
-
document.cookie = `${CONSTANTS.HTTP_COOKIE_NAME_CSRF_TOKEN}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Strict; Secure`;
|
|
373
503
|
}
|
|
374
504
|
}
|
|
375
505
|
}
|
|
@@ -379,11 +509,11 @@ class AppMeshClient {
|
|
|
379
509
|
* @param {boolean} enable - true to start, false to stop
|
|
380
510
|
* @param {string} [jwtToken] - Optional token used only to calculate the first refresh delay
|
|
381
511
|
*/
|
|
382
|
-
|
|
512
|
+
set_auto_refresh_token(enable, jwtToken = null) {
|
|
383
513
|
this._stopAutoRefresh();
|
|
384
514
|
this._autoRefreshEnabled = enable;
|
|
385
515
|
if (enable) {
|
|
386
|
-
this._autoRefreshJwt = jwtToken || this._getAccessToken
|
|
516
|
+
this._autoRefreshJwt = jwtToken || this._getAccessToken() || null;
|
|
387
517
|
this._scheduleTokenRefresh();
|
|
388
518
|
}
|
|
389
519
|
}
|
|
@@ -401,13 +531,19 @@ class AppMeshClient {
|
|
|
401
531
|
_scheduleTokenRefresh() {
|
|
402
532
|
if (!this._autoRefreshEnabled) return;
|
|
403
533
|
|
|
534
|
+
// Replace any pending timer so re-scheduling (e.g. from _handleTokenUpdate) never stacks timers
|
|
535
|
+
if (this._refreshTimer) {
|
|
536
|
+
clearTimeout(this._refreshTimer);
|
|
537
|
+
this._refreshTimer = null;
|
|
538
|
+
}
|
|
539
|
+
|
|
404
540
|
const REFRESH_INTERVAL = 300; // 5 min default check
|
|
405
541
|
const REFRESH_MARGIN = 30; // refresh 30s before expiry
|
|
406
542
|
|
|
407
543
|
let delaySec = REFRESH_INTERVAL;
|
|
408
544
|
|
|
409
545
|
// Try to compute precise delay from JWT exp
|
|
410
|
-
const token = this._autoRefreshJwt || this._getAccessToken
|
|
546
|
+
const token = this._autoRefreshJwt || this._getAccessToken();
|
|
411
547
|
if (token) {
|
|
412
548
|
const exp = _decodeJwtExp(token);
|
|
413
549
|
if (exp) {
|
|
@@ -424,14 +560,9 @@ class AppMeshClient {
|
|
|
424
560
|
this._refreshTimer = setTimeout(async () => {
|
|
425
561
|
if (!this._autoRefreshEnabled) return;
|
|
426
562
|
try {
|
|
563
|
+
// _request captures the renewed token cookie and routes it through
|
|
564
|
+
// _handleTokenUpdate, which updates _autoRefreshJwt for precise delays
|
|
427
565
|
await this.renew_token();
|
|
428
|
-
// Update stored token from cookie for precise delay calculation (Node.js only;
|
|
429
|
-
// in browsers the auth cookie is HttpOnly and not accessible via JS)
|
|
430
|
-
if (ENV.isNode) {
|
|
431
|
-
const cookieStr = this._client?.defaults?.headers?.Cookie || '';
|
|
432
|
-
const match = cookieStr.split('; ').find(c => c.startsWith('appmesh_auth_token='));
|
|
433
|
-
if (match) this._autoRefreshJwt = match.split('=').slice(1).join('=');
|
|
434
|
-
}
|
|
435
566
|
} catch (err) {
|
|
436
567
|
console.warn("Auto-refresh: token renewal failed:", err.message);
|
|
437
568
|
}
|
|
@@ -460,7 +591,7 @@ class AppMeshClient {
|
|
|
460
591
|
* Get the decoded OTP provisioning URI for the current user.
|
|
461
592
|
* @returns {Promise<string>} Decoded `otpauth://...` URI, not just the raw secret field
|
|
462
593
|
*/
|
|
463
|
-
async
|
|
594
|
+
async get_totp_uri() {
|
|
464
595
|
const response = await this._request("post", "/appmesh/totp/secret");
|
|
465
596
|
return base64Utils.decode(response.data["mfa_uri"]);
|
|
466
597
|
}
|
|
@@ -497,11 +628,11 @@ class AppMeshClient {
|
|
|
497
628
|
/**
|
|
498
629
|
* Disable TOTP for user
|
|
499
630
|
* @param {string} [user='self'] - Username
|
|
500
|
-
* @returns {boolean}
|
|
631
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
501
632
|
*/
|
|
502
633
|
async disable_totp(user = "self") {
|
|
503
|
-
|
|
504
|
-
return
|
|
634
|
+
await this._request("post", `/appmesh/totp/${user}/disable`);
|
|
635
|
+
return true;
|
|
505
636
|
}
|
|
506
637
|
|
|
507
638
|
/**
|
|
@@ -603,21 +734,21 @@ class AppMeshClient {
|
|
|
603
734
|
/**
|
|
604
735
|
* Enable application
|
|
605
736
|
* @param {string} name - App name
|
|
606
|
-
* @returns {Promise<boolean>}
|
|
737
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
607
738
|
*/
|
|
608
739
|
async enable_app(name) {
|
|
609
|
-
|
|
610
|
-
return
|
|
740
|
+
await this._request("post", `/appmesh/app/${name}/enable`);
|
|
741
|
+
return true;
|
|
611
742
|
}
|
|
612
743
|
|
|
613
744
|
/**
|
|
614
745
|
* Disable application
|
|
615
746
|
* @param {string} name - App name
|
|
616
|
-
* @returns {Promise<boolean>}
|
|
747
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
617
748
|
*/
|
|
618
749
|
async disable_app(name) {
|
|
619
|
-
|
|
620
|
-
return
|
|
750
|
+
await this._request("post", `/appmesh/app/${name}/disable`);
|
|
751
|
+
return true;
|
|
621
752
|
}
|
|
622
753
|
|
|
623
754
|
/**
|
|
@@ -641,9 +772,11 @@ class AppMeshClient {
|
|
|
641
772
|
};
|
|
642
773
|
|
|
643
774
|
const response = await this._request("get", `/appmesh/app/${app_name}/output`, null, { params });
|
|
644
|
-
// axios
|
|
645
|
-
const
|
|
646
|
-
const
|
|
775
|
+
// axios lowercases header names; the TCP transport keeps the daemon's exact case
|
|
776
|
+
const outPositionHeader = _getHeader(response.headers, "X-Output-Position");
|
|
777
|
+
const exitCodeHeader = _getHeader(response.headers, "X-Exit-Code");
|
|
778
|
+
const outPosition = outPositionHeader ? parseInt(outPositionHeader, 10) : null;
|
|
779
|
+
const exitCode = exitCodeHeader ? parseInt(exitCodeHeader, 10) : null;
|
|
647
780
|
return new AppOutput(response.status, response.data, outPosition, exitCode);
|
|
648
781
|
}
|
|
649
782
|
|
|
@@ -668,9 +801,10 @@ class AppMeshClient {
|
|
|
668
801
|
if (stdoutHandler) {
|
|
669
802
|
stdoutHandler(response.data, 0);
|
|
670
803
|
}
|
|
671
|
-
// axios
|
|
672
|
-
|
|
673
|
-
|
|
804
|
+
// axios lowercases header names; the TCP transport keeps the daemon's exact case
|
|
805
|
+
const exitCodeHeader = _getHeader(response.headers, "X-Exit-Code");
|
|
806
|
+
if (exitCodeHeader) {
|
|
807
|
+
exitCode = parseInt(exitCodeHeader, 10);
|
|
674
808
|
}
|
|
675
809
|
} else if (stdoutHandler) {
|
|
676
810
|
stdoutHandler(response.data, 0);
|
|
@@ -701,8 +835,9 @@ class AppMeshClient {
|
|
|
701
835
|
* @param {AppRun} run - AppRun object
|
|
702
836
|
* @param {Function} [stdoutHandler=defaultOutputHandler] - Stdout handler callback(data, position)
|
|
703
837
|
* @param {number} [timeout=0] - Max wait time
|
|
704
|
-
* @returns {Promise<number|null>} Exit code, or `null` on timeout
|
|
838
|
+
* @returns {Promise<number|null>} Exit code, or `null` only on timeout. On success
|
|
705
839
|
* the SDK also attempts to delete the temporary run app.
|
|
840
|
+
* @throws {AppMeshError} If polling the app output fails.
|
|
706
841
|
*/
|
|
707
842
|
async wait_for_async_run(run, stdoutHandler = defaultOutputHandler, timeout = 0) {
|
|
708
843
|
if (run) {
|
|
@@ -711,37 +846,27 @@ class AppMeshClient {
|
|
|
711
846
|
const interval = 1;
|
|
712
847
|
|
|
713
848
|
while (run.procUid.length > 0) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (appOut.outPosition !== null) {
|
|
721
|
-
lastOutputPosition = appOut.outPosition;
|
|
722
|
-
}
|
|
849
|
+
const appOut = await this.get_app_output(run.appName, lastOutputPosition, 0, 20480, run.procUid, interval);
|
|
850
|
+
if (appOut.output && stdoutHandler) {
|
|
851
|
+
stdoutHandler(appOut.output, lastOutputPosition);
|
|
852
|
+
}
|
|
723
853
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
return appOut.exitCode;
|
|
728
|
-
}
|
|
854
|
+
if (appOut.outPosition !== null) {
|
|
855
|
+
lastOutputPosition = appOut.outPosition;
|
|
856
|
+
}
|
|
729
857
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
858
|
+
if (appOut.exitCode !== null) {
|
|
859
|
+
// Process finished
|
|
860
|
+
await this.delete_app(run.appName);
|
|
861
|
+
return appOut.exitCode;
|
|
862
|
+
}
|
|
734
863
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
break;
|
|
738
|
-
}
|
|
739
|
-
// Small delay to prevent tight looping
|
|
740
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
741
|
-
} catch (error) {
|
|
742
|
-
console.error(error);
|
|
864
|
+
if (timeout > 0 && (new Date() - start) / 1000 > timeout) {
|
|
865
|
+
// Timeout reached
|
|
743
866
|
break;
|
|
744
867
|
}
|
|
868
|
+
// Small delay to prevent tight looping
|
|
869
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
745
870
|
}
|
|
746
871
|
}
|
|
747
872
|
return null;
|
|
@@ -767,19 +892,27 @@ class AppMeshClient {
|
|
|
767
892
|
/**
|
|
768
893
|
* Cancel running task
|
|
769
894
|
* @param {string} appName - App name
|
|
770
|
-
* @returns {Promise<boolean>}
|
|
895
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
771
896
|
*/
|
|
772
897
|
async cancel_task(appName) {
|
|
773
|
-
|
|
774
|
-
return
|
|
898
|
+
await this._request("delete", `/appmesh/app/${appName}/task`);
|
|
899
|
+
return true;
|
|
775
900
|
}
|
|
776
901
|
|
|
777
902
|
/**
|
|
778
|
-
* Download a remote file.
|
|
903
|
+
* Download a remote file. Behavior differs by environment:
|
|
904
|
+
*
|
|
905
|
+
* - **Node.js**: `localFile` is a filesystem path — the response body is written to it,
|
|
906
|
+
* and when `applyAttrs` is true the returned mode and best-effort owner/group metadata
|
|
907
|
+
* are applied on non-Windows platforms.
|
|
908
|
+
* - **Browser**: `localFile` is only the *suggested download filename* (its basename is
|
|
909
|
+
* used); no path is honored — the browser's download UI decides where the file goes,
|
|
910
|
+
* and `applyAttrs` has no effect.
|
|
911
|
+
*
|
|
779
912
|
* @param {string} filePath - Remote file path
|
|
780
|
-
* @param {string} localFile - Local file path
|
|
781
|
-
* @param {boolean} [applyAttrs=true] -
|
|
782
|
-
* metadata on non-Windows platforms
|
|
913
|
+
* @param {string} localFile - Local file path (Node.js) or suggested filename (browser)
|
|
914
|
+
* @param {boolean} [applyAttrs=true] - Node.js only: apply returned mode and best-effort
|
|
915
|
+
* owner/group metadata on non-Windows platforms
|
|
783
916
|
*/
|
|
784
917
|
async download_file(filePath, localFile, applyAttrs = true) {
|
|
785
918
|
const headers = { [CONSTANTS.HTTP_HEADER_KEY_X_FILE_PATH]: encodeURIComponent(filePath) };
|
|
@@ -948,22 +1081,22 @@ class AppMeshClient {
|
|
|
948
1081
|
* Add label to server
|
|
949
1082
|
* @param {string} labelName - Label name
|
|
950
1083
|
* @param {string} labelValue - Label value
|
|
951
|
-
* @returns {boolean}
|
|
1084
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
952
1085
|
*/
|
|
953
1086
|
async add_label(labelName, labelValue) {
|
|
954
|
-
|
|
955
|
-
return
|
|
1087
|
+
await this._request("put", `/appmesh/label/${labelName}`, null, { params: { value: labelValue } });
|
|
1088
|
+
return true;
|
|
956
1089
|
}
|
|
957
1090
|
|
|
958
1091
|
|
|
959
1092
|
/**
|
|
960
1093
|
* Delete label from server
|
|
961
1094
|
* @param {string} labelName - Label name
|
|
962
|
-
* @returns {boolean}
|
|
1095
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
963
1096
|
*/
|
|
964
1097
|
async delete_label(labelName) {
|
|
965
|
-
|
|
966
|
-
return
|
|
1098
|
+
await this._request("delete", `/appmesh/label/${labelName}`);
|
|
1099
|
+
return true;
|
|
967
1100
|
}
|
|
968
1101
|
|
|
969
1102
|
|
|
@@ -982,56 +1115,56 @@ class AppMeshClient {
|
|
|
982
1115
|
* @param {string} oldPassword - Old password
|
|
983
1116
|
* @param {string} newPassword - New password
|
|
984
1117
|
* @param {string} [username="self"] - Username
|
|
985
|
-
* @returns {Promise<boolean>}
|
|
1118
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
986
1119
|
*/
|
|
987
1120
|
async update_password(oldPassword, newPassword, username = "self") {
|
|
988
1121
|
const body = {
|
|
989
1122
|
"old_password": base64Utils.encode(oldPassword),
|
|
990
1123
|
"new_password": base64Utils.encode(newPassword)
|
|
991
1124
|
};
|
|
992
|
-
|
|
993
|
-
return
|
|
1125
|
+
await this._request("post", `/appmesh/user/${username}/passwd`, body);
|
|
1126
|
+
return true;
|
|
994
1127
|
}
|
|
995
1128
|
|
|
996
1129
|
/**
|
|
997
1130
|
* Add new user
|
|
998
1131
|
* @param {string} username - Username
|
|
999
1132
|
* @param {Object} userData - User definition
|
|
1000
|
-
* @returns {Promise<boolean>}
|
|
1133
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1001
1134
|
*/
|
|
1002
1135
|
async add_user(username, userData) {
|
|
1003
|
-
|
|
1004
|
-
return
|
|
1136
|
+
await this._request("put", `/appmesh/user/${username}`, userData);
|
|
1137
|
+
return true;
|
|
1005
1138
|
}
|
|
1006
1139
|
|
|
1007
1140
|
/**
|
|
1008
1141
|
* Delete user
|
|
1009
1142
|
* @param {string} username - Username
|
|
1010
|
-
* @returns {Promise<boolean>}
|
|
1143
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1011
1144
|
*/
|
|
1012
1145
|
async delete_user(username) {
|
|
1013
|
-
|
|
1014
|
-
return
|
|
1146
|
+
await this._request("delete", `/appmesh/user/${username}`);
|
|
1147
|
+
return true;
|
|
1015
1148
|
}
|
|
1016
1149
|
|
|
1017
1150
|
/**
|
|
1018
1151
|
* Lock user account
|
|
1019
1152
|
* @param {string} username - Username
|
|
1020
|
-
* @returns {Promise<boolean>}
|
|
1153
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1021
1154
|
*/
|
|
1022
1155
|
async lock_user(username) {
|
|
1023
|
-
|
|
1024
|
-
return
|
|
1156
|
+
await this._request("post", `/appmesh/user/${username}/lock`);
|
|
1157
|
+
return true;
|
|
1025
1158
|
}
|
|
1026
1159
|
|
|
1027
1160
|
/**
|
|
1028
1161
|
* Unlock user account
|
|
1029
1162
|
* @param {string} username - Username
|
|
1030
|
-
* @returns {Promise<boolean>}
|
|
1163
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1031
1164
|
*/
|
|
1032
1165
|
async unlock_user(username) {
|
|
1033
|
-
|
|
1034
|
-
return
|
|
1166
|
+
await this._request("post", `/appmesh/user/${username}/unlock`);
|
|
1167
|
+
return true;
|
|
1035
1168
|
}
|
|
1036
1169
|
|
|
1037
1170
|
/**
|
|
@@ -1092,34 +1225,64 @@ class AppMeshClient {
|
|
|
1092
1225
|
* Update or add role
|
|
1093
1226
|
* @param {string} roleName - Role name
|
|
1094
1227
|
* @param {Object} rolePermissionJson - Permission IDs
|
|
1095
|
-
* @returns {Promise<boolean>}
|
|
1228
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1096
1229
|
*/
|
|
1097
1230
|
async update_role(roleName, rolePermissionJson) {
|
|
1098
|
-
|
|
1099
|
-
return
|
|
1231
|
+
await this._request("post", `/appmesh/role/${roleName}`, rolePermissionJson);
|
|
1232
|
+
return true;
|
|
1100
1233
|
}
|
|
1101
1234
|
|
|
1102
1235
|
/**
|
|
1103
1236
|
* Delete role
|
|
1104
1237
|
* @param {string} roleName - Role name
|
|
1105
|
-
* @returns {boolean}
|
|
1238
|
+
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1106
1239
|
*/
|
|
1107
1240
|
async delete_role(roleName) {
|
|
1108
|
-
|
|
1109
|
-
return
|
|
1241
|
+
await this._request("delete", `/appmesh/role/${roleName}`);
|
|
1242
|
+
return true;
|
|
1110
1243
|
}
|
|
1111
1244
|
|
|
1112
1245
|
/**
|
|
1113
1246
|
* Get raw Prometheus metrics text from the server.
|
|
1114
1247
|
* @returns {Promise<string>} Metrics text
|
|
1115
1248
|
*/
|
|
1116
|
-
async
|
|
1249
|
+
async get_metrics() {
|
|
1117
1250
|
const response = await this._request("get", "/appmesh/metrics", null, {
|
|
1118
1251
|
config: { responseType: "text" }
|
|
1119
1252
|
});
|
|
1120
1253
|
return response.data;
|
|
1121
1254
|
}
|
|
1122
1255
|
|
|
1256
|
+
/**
|
|
1257
|
+
* Event subscription is only available over the TCP transport.
|
|
1258
|
+
* @throws {AppMeshError} Always — use AppMeshClientTCP for event subscriptions.
|
|
1259
|
+
*/
|
|
1260
|
+
async subscribe() {
|
|
1261
|
+
throw new AppMeshError("subscribe requires the TCP client (AppMeshClientTCP, imported from 'appmesh/tcp'); the HTTP client does not support event subscriptions");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Event subscription is only available over the TCP transport.
|
|
1266
|
+
* @throws {AppMeshError} Always — use AppMeshClientTCP for event subscriptions.
|
|
1267
|
+
*/
|
|
1268
|
+
async unsubscribe() {
|
|
1269
|
+
throw new AppMeshError("unsubscribe requires the TCP client (AppMeshClientTCP, imported from 'appmesh/tcp'); the HTTP client does not support event subscriptions");
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Perform a raw App Mesh REST request using this client's transport, auth and error handling.
|
|
1274
|
+
* Stable seam for worker-role wrappers (AppMeshWorker) and advanced callers.
|
|
1275
|
+
* @param {string} method - HTTP method (get, post, put, delete, ...)
|
|
1276
|
+
* @param {string} path - Endpoint path (e.g. "/appmesh/applications")
|
|
1277
|
+
* @param {Object|string|Buffer} [body=null] - Request payload
|
|
1278
|
+
* @param {Object} [options={}] - Request options: { headers, params, config }
|
|
1279
|
+
* @returns {Promise<any>} Response object ({ status, headers, data, ... })
|
|
1280
|
+
* @throws {AppMeshError} If the request fails (non-2xx)
|
|
1281
|
+
*/
|
|
1282
|
+
async request(method, path, body = null, options = {}) {
|
|
1283
|
+
return this._request(method, path, body, options);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1123
1286
|
/**
|
|
1124
1287
|
* Generate common request headers
|
|
1125
1288
|
* @private
|
|
@@ -1132,20 +1295,6 @@ class AppMeshClient {
|
|
|
1132
1295
|
headers[CONSTANTS.HTTP_USER_AGENT_HEADER_NAME] = CONSTANTS.HTTP_USER_AGENT;
|
|
1133
1296
|
}
|
|
1134
1297
|
|
|
1135
|
-
// Add CSRF token from cookies to headers
|
|
1136
|
-
const getCsrfToken = (cookieStr) => {
|
|
1137
|
-
if (!cookieStr) return null;
|
|
1138
|
-
const match = cookieStr.split('; ').find(c => c.startsWith(CONSTANTS.HTTP_COOKIE_NAME_CSRF_TOKEN + '='));
|
|
1139
|
-
return match ? match.split('=')[1] : null;
|
|
1140
|
-
};
|
|
1141
|
-
if (ENV.isNode) {
|
|
1142
|
-
const token = getCsrfToken(this._client.defaults.headers.Cookie);
|
|
1143
|
-
if (token) headers[CONSTANTS.HTTP_HEADER_NAME_CSRF_TOKEN] = token;
|
|
1144
|
-
} else {
|
|
1145
|
-
const token = getCsrfToken(document.cookie);
|
|
1146
|
-
if (token) headers[CONSTANTS.HTTP_HEADER_NAME_CSRF_TOKEN] = token;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
1298
|
// Add forwarding host if specified
|
|
1150
1299
|
if (this.forwardingHost) {
|
|
1151
1300
|
if (this.forwardingHost.includes(":")) {
|
|
@@ -1177,7 +1326,7 @@ class AppMeshClient {
|
|
|
1177
1326
|
await _httpsReady;
|
|
1178
1327
|
if (_https) {
|
|
1179
1328
|
this._client.defaults.httpsAgent = new _https.Agent({
|
|
1180
|
-
...(this._sslConfig
|
|
1329
|
+
..._resolveSslOptions(this._sslConfig),
|
|
1181
1330
|
keepAlive: true,
|
|
1182
1331
|
keepAliveMsecs: 3000
|
|
1183
1332
|
});
|
|
@@ -1202,10 +1351,10 @@ class AppMeshClient {
|
|
|
1202
1351
|
|
|
1203
1352
|
const response = await this._client(requestConfig);
|
|
1204
1353
|
if (response.status !== 200) {
|
|
1205
|
-
if (path === "/appmesh/self/logoff") {
|
|
1206
|
-
return response; // Allow logoff to "fail" gracefully
|
|
1207
|
-
}
|
|
1208
1354
|
const errMsg = this._extractErrorMessage(response.data);
|
|
1355
|
+
if (response.status === CONSTANTS.HTTP_STATUS_PRECONDITION_REQUIRED) {
|
|
1356
|
+
throw new TotpRequiredError(errMsg, response.data);
|
|
1357
|
+
}
|
|
1209
1358
|
throw new AppMeshError(errMsg, response.status, response.data);
|
|
1210
1359
|
}
|
|
1211
1360
|
|
|
@@ -1218,6 +1367,15 @@ class AppMeshClient {
|
|
|
1218
1367
|
|
|
1219
1368
|
// Join all cookies with semicolon separator
|
|
1220
1369
|
this._client.defaults.headers.Cookie = cookies.join('; ');
|
|
1370
|
+
|
|
1371
|
+
// Keep the in-memory token store in sync with the server-issued cookie
|
|
1372
|
+
// (login, renew, authenticate, TOTP setup/validate)
|
|
1373
|
+
const authCookie = cookies
|
|
1374
|
+
.map(c => c.split(';')[0].trim())
|
|
1375
|
+
.find(c => c.startsWith('appmesh_auth_token='));
|
|
1376
|
+
if (authCookie) {
|
|
1377
|
+
this._handleTokenUpdate(authCookie.substring('appmesh_auth_token='.length));
|
|
1378
|
+
}
|
|
1221
1379
|
}
|
|
1222
1380
|
|
|
1223
1381
|
return response;
|
|
@@ -1225,10 +1383,6 @@ class AppMeshClient {
|
|
|
1225
1383
|
if (error instanceof AppMeshError && error.statusCode === CONSTANTS.HTTP_STATUS_PRECONDITION_REQUIRED) {
|
|
1226
1384
|
throw error;
|
|
1227
1385
|
}
|
|
1228
|
-
if (path === "/appmesh/self/logoff") {
|
|
1229
|
-
// console.log("Logoff error:", error.message);
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
1386
|
throw this.onError(error);
|
|
1233
1387
|
}
|
|
1234
1388
|
}
|
|
@@ -1332,11 +1486,14 @@ class AppRun {
|
|
|
1332
1486
|
}
|
|
1333
1487
|
|
|
1334
1488
|
/**
|
|
1335
|
-
* Context manager for forward host override to self._client
|
|
1489
|
+
* Context manager for forward host override to self._client.
|
|
1490
|
+
* Note: this temporarily mutates the shared client's forwardingHost for the duration of the
|
|
1491
|
+
* callback — do not run concurrent requests on the same client that rely on a different
|
|
1492
|
+
* forwarding host.
|
|
1336
1493
|
* @param {function} callback - Function to execute within the forward host context
|
|
1337
1494
|
* @returns {Promise<*>} Result of the callback function
|
|
1338
1495
|
*/
|
|
1339
|
-
async
|
|
1496
|
+
async with_forwarding_host(callback) {
|
|
1340
1497
|
const originalValue = this._client.forwardingHost;
|
|
1341
1498
|
this._client.forwardingHost = this._forwardingHost;
|
|
1342
1499
|
try {
|
|
@@ -1353,12 +1510,12 @@ class AppRun {
|
|
|
1353
1510
|
* @returns {Promise<number|null>} Return exit code if process finished, return null for timeout or exception
|
|
1354
1511
|
*/
|
|
1355
1512
|
async wait(stdoutHandler = defaultOutputHandler, timeout = 0) {
|
|
1356
|
-
return this.
|
|
1513
|
+
return this.with_forwarding_host(() =>
|
|
1357
1514
|
this._client.wait_for_async_run(this, stdoutHandler, timeout)
|
|
1358
1515
|
);
|
|
1359
1516
|
}
|
|
1360
1517
|
}
|
|
1361
1518
|
|
|
1362
1519
|
// Export the main classes
|
|
1363
|
-
export { AppMeshClient, AppOutput, AppRun, AppMeshError };
|
|
1520
|
+
export { AppMeshClient, AppOutput, AppRun, AppMeshError, TotpRequiredError, AppRemovedError, TransportDisconnectedError, DEFAULT_CA_FILE };
|
|
1364
1521
|
export default AppMeshClient;
|