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