appmesh 1.3.3 → 3.0.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 +3 -3
- package/dist/appmesh.esm.js +1 -1
- package/package.json +3 -13
- package/src/appmesh.js +46 -669
- package/src/appmesh_tcp.js +13 -66
- package/src/worker.js +7 -11
- package/src/appmesh_oauth.js +0 -367
package/src/appmesh.js
CHANGED
|
@@ -10,24 +10,11 @@ let _httpsReady = null; // Promise that resolves when https/fs are loaded
|
|
|
10
10
|
const CONSTANTS = Object.freeze({
|
|
11
11
|
HTTP_USER_AGENT_HEADER_NAME: "User-Agent",
|
|
12
12
|
HTTP_USER_AGENT: "appmesh/javascript",
|
|
13
|
-
HTTP_STATUS_PRECONDITION_REQUIRED: 428,
|
|
14
|
-
DEFAULT_TOKEN_EXPIRE_SECONDS: "P1W",
|
|
15
13
|
DEFAULT_RUN_APP_TIMEOUT_SECONDS: "P2D",
|
|
16
14
|
DEFAULT_RUN_APP_LIFECYCLE_SECONDS: "P2DT12H",
|
|
17
|
-
DEFAULT_JWT_AUDIENCE: "appmesh-service",
|
|
18
15
|
HTTP_HEADER_KEY_AUTH: "Authorization",
|
|
19
16
|
HTTP_HEADER_KEY_X_TARGET_HOST: "X-Target-Host",
|
|
20
17
|
HTTP_HEADER_KEY_X_FILE_PATH: "X-File-Path",
|
|
21
|
-
|
|
22
|
-
// Auto-refresh pacing: the loop wakes every TOKEN_REFRESH_POLL_SECONDS but renews only
|
|
23
|
-
// once the token has burned TOKEN_REFRESH_LIFETIME_RATIO of its own lifetime.
|
|
24
|
-
TOKEN_REFRESH_POLL_SECONDS: 300, // poll cap, NOT a renew interval
|
|
25
|
-
TOKEN_REFRESH_OFFSET_SECONDS: 30, // floor for the pre-expiry margin
|
|
26
|
-
TOKEN_REFRESH_LIFETIME_RATIO: 0.6, // rest is the retry budget
|
|
27
|
-
TOKEN_REFRESH_JITTER_RATIO: 0.1, // of the margin, so clients don't renew in lockstep
|
|
28
|
-
TOKEN_REFRESH_RETRY_BASE_SECONDS: 5,
|
|
29
|
-
TOKEN_REFRESH_RETRY_MAX_SECONDS: 60,
|
|
30
|
-
TOKEN_REFRESH_LOG_EVERY: 10, // log the 1st failure, then every Nth
|
|
31
18
|
});
|
|
32
19
|
|
|
33
20
|
// Environment detection
|
|
@@ -35,15 +22,6 @@ const ENV = Object.freeze({
|
|
|
35
22
|
isNode: !(typeof window !== 'undefined' && typeof window.document !== 'undefined')
|
|
36
23
|
});
|
|
37
24
|
|
|
38
|
-
// Base64 utilities for Node.js and browser
|
|
39
|
-
const base64Utils = ENV.isNode ? {
|
|
40
|
-
encode: str => Buffer.from(str).toString('base64'),
|
|
41
|
-
decode: str => Buffer.from(str, 'base64').toString()
|
|
42
|
-
} : {
|
|
43
|
-
encode: str => btoa(str),
|
|
44
|
-
decode: str => atob(str)
|
|
45
|
-
};
|
|
46
|
-
|
|
47
25
|
/**
|
|
48
26
|
* Custom error for AppMesh with enhanced debugging
|
|
49
27
|
*/
|
|
@@ -65,32 +43,6 @@ class AppMeshError extends Error {
|
|
|
65
43
|
}
|
|
66
44
|
}
|
|
67
45
|
|
|
68
|
-
/**
|
|
69
|
-
* Error thrown when the server requires a TOTP code to complete login (HTTP 428).
|
|
70
|
-
*
|
|
71
|
-
* Subclasses AppMeshError with `statusCode` fixed to 428, so existing catch-based
|
|
72
|
-
* callers keep working. Carries the server-issued challenge as `totpChallenge`,
|
|
73
|
-
* ready to pass to `validate_totp(username, totpChallenge, totpCode)`.
|
|
74
|
-
*/
|
|
75
|
-
class TotpRequiredError extends AppMeshError {
|
|
76
|
-
/**
|
|
77
|
-
* @param {string} message - Error message
|
|
78
|
-
* @param {any} responseData - Raw 428 response body (object or JSON string containing `totp_challenge`)
|
|
79
|
-
*/
|
|
80
|
-
constructor(message, responseData = null) {
|
|
81
|
-
super(message, 428, responseData, 'TOTP_REQUIRED');
|
|
82
|
-
this.name = 'TotpRequiredError';
|
|
83
|
-
let data = responseData;
|
|
84
|
-
if (typeof data === 'string') {
|
|
85
|
-
try { data = JSON.parse(data); } catch (_) { data = null; }
|
|
86
|
-
}
|
|
87
|
-
/** @type {string|null} Server-issued TOTP challenge for validate_totp() */
|
|
88
|
-
this.totpChallenge = (data && typeof data === 'object' && typeof data.totp_challenge === 'string')
|
|
89
|
-
? data.totp_challenge
|
|
90
|
-
: null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
46
|
/**
|
|
95
47
|
* App removed before its async-run exit was observed. Typed replacement for the
|
|
96
48
|
* old -1 sentinel, keeping real (possibly negative) exit codes unambiguous.
|
|
@@ -257,40 +209,6 @@ async function _resolveGid(group) {
|
|
|
257
209
|
}
|
|
258
210
|
}
|
|
259
211
|
|
|
260
|
-
/**
|
|
261
|
-
* Decode the `exp` and `iat` claims from a JWT without signature verification.
|
|
262
|
-
* @param {string} token - JWT string (header.payload.signature)
|
|
263
|
-
* @returns {{exp: number, iat: number}|null} Claims (`iat` is 0 when absent), or null when
|
|
264
|
-
* the token is not a decodable JWT carrying `exp`
|
|
265
|
-
* @private
|
|
266
|
-
*/
|
|
267
|
-
function _decodeJwtTimes(token) {
|
|
268
|
-
try {
|
|
269
|
-
const parts = token.split('.');
|
|
270
|
-
if (parts.length !== 3) return null;
|
|
271
|
-
const payload = JSON.parse(base64Utils.decode(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
272
|
-
if (typeof payload.exp !== 'number') return null;
|
|
273
|
-
return { exp: payload.exp, iat: typeof payload.iat === 'number' ? payload.iat : 0 };
|
|
274
|
-
} catch (_) {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* FNV-1a 32-bit hash, matching the Go SDK so both derive the same jitter from a token.
|
|
281
|
-
* @param {string} str - Input string (JWTs are ASCII, so code units are bytes)
|
|
282
|
-
* @returns {number} Unsigned 32-bit hash
|
|
283
|
-
* @private
|
|
284
|
-
*/
|
|
285
|
-
function _fnv1a32(str) {
|
|
286
|
-
let hash = 0x811c9dc5;
|
|
287
|
-
for (let i = 0; i < str.length; i++) {
|
|
288
|
-
hash ^= str.charCodeAt(i);
|
|
289
|
-
hash = Math.imul(hash, 0x01000193);
|
|
290
|
-
}
|
|
291
|
-
return hash >>> 0;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
212
|
// Default App Mesh CA bundle, preferred when no sslConfig is given and the file exists
|
|
295
213
|
const DEFAULT_CA_FILE = '/opt/appmesh/ssl/ca.pem';
|
|
296
214
|
|
|
@@ -327,8 +245,6 @@ class AppMeshClient {
|
|
|
327
245
|
* against the App Mesh default CA (/opt/appmesh/ssl/ca.pem) if installed, else the system CAs;
|
|
328
246
|
* `false` disables verification; an object supplies custom `ca`/`cert`/`key` (Buffer, inline
|
|
329
247
|
* PEM, or file path — a missing path is a hard error).
|
|
330
|
-
* @param {boolean|null} [useRefreshToken=null] - Ask the daemon for a refresh token
|
|
331
|
-
* (see {@link AppMeshClient#set_use_refresh_token} for the tri-state and the browser default).
|
|
332
248
|
* @example
|
|
333
249
|
* const sslConfig = {
|
|
334
250
|
* cert: fs.readFileSync("client.pem"),
|
|
@@ -337,7 +253,7 @@ class AppMeshClient {
|
|
|
337
253
|
* rejectUnauthorized: true
|
|
338
254
|
* };
|
|
339
255
|
*/
|
|
340
|
-
constructor(baseURL = ENV.isNode ? 'https://127.0.0.1:6060' : window.location.origin, sslConfig = null
|
|
256
|
+
constructor(baseURL = ENV.isNode ? 'https://127.0.0.1:6060' : window.location.origin, sslConfig = null) {
|
|
341
257
|
// Base URL for API requests
|
|
342
258
|
this.baseURL = baseURL;
|
|
343
259
|
|
|
@@ -348,34 +264,6 @@ class AppMeshClient {
|
|
|
348
264
|
// sync from it via _handleTokenUpdate/_getAccessToken)
|
|
349
265
|
this._token = null;
|
|
350
266
|
|
|
351
|
-
// Refresh token from the login/totp-validate/renew response body; issued by both
|
|
352
|
-
// Keycloak and local-JWT daemons, absent against an older daemon that returns none.
|
|
353
|
-
this._refreshToken = null;
|
|
354
|
-
|
|
355
|
-
// Expire seconds from login, replayed on renew so the caller's TTL is kept.
|
|
356
|
-
this._tokenExpireSeconds = null;
|
|
357
|
-
|
|
358
|
-
// {exp, iat} as reported by the daemon in the login/renew body. The only lifetime
|
|
359
|
-
// source available in a browser, where the auth cookie is HttpOnly and unreadable.
|
|
360
|
-
this._tokenTimes = null;
|
|
361
|
-
|
|
362
|
-
// Per-client jitter seed, used only when no token is readable: the session times
|
|
363
|
-
// alone are identical for every client that logs in during the same second.
|
|
364
|
-
this._jitterSeed = Math.random().toString(36).slice(2);
|
|
365
|
-
|
|
366
|
-
// Bumped by logout() so a response from a superseded session cannot revive it.
|
|
367
|
-
this._sessionEpoch = 0;
|
|
368
|
-
|
|
369
|
-
// Auto-refresh state: pending timer, consecutive renewal failures (drives the
|
|
370
|
-
// backoff), and the tail of the renewal queue that serializes renew_token().
|
|
371
|
-
this._autoRefreshEnabled = false;
|
|
372
|
-
this._autoRefreshJwt = null;
|
|
373
|
-
// Tri-state opt-in for being ISSUED a refresh token; null follows auto-refresh.
|
|
374
|
-
this._useRefreshToken = useRefreshToken;
|
|
375
|
-
this._refreshTimer = null;
|
|
376
|
-
this._refreshFailures = 0;
|
|
377
|
-
this._renewChain = null;
|
|
378
|
-
|
|
379
267
|
// Configure axios instance
|
|
380
268
|
const axiosConfig = {
|
|
381
269
|
baseURL,
|
|
@@ -421,470 +309,40 @@ class AppMeshClient {
|
|
|
421
309
|
);
|
|
422
310
|
}
|
|
423
311
|
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
* @param {string} [totpCode] - TOTP code if 2FA is enabled
|
|
429
|
-
* @param {string|number} [tokenExpire] - Token expiry (integer seconds or ISO 8601 string)
|
|
430
|
-
* @param {string} [audience] - JWT audience
|
|
431
|
-
* @returns {Promise<void>} Resolves when login succeeds.
|
|
432
|
-
* @throws {TotpRequiredError} When the server requires TOTP (HTTP 428) and no valid code was
|
|
433
|
-
* supplied. Catch it, read `error.totpChallenge`, then complete the login with
|
|
434
|
-
* `validate_totp(username, error.totpChallenge, totpCode)`:
|
|
435
|
-
* @example
|
|
436
|
-
* try {
|
|
437
|
-
* await client.login(user, pass);
|
|
438
|
-
* } catch (err) {
|
|
439
|
-
* if (err instanceof TotpRequiredError) {
|
|
440
|
-
* await client.validate_totp(user, err.totpChallenge, totpCode);
|
|
441
|
-
* } else {
|
|
442
|
-
* throw err;
|
|
443
|
-
* }
|
|
444
|
-
* }
|
|
445
|
-
*/
|
|
446
|
-
async login(username, password, totpCode = null, tokenExpire = CONSTANTS.DEFAULT_TOKEN_EXPIRE_SECONDS, audience = CONSTANTS.DEFAULT_JWT_AUDIENCE) {
|
|
447
|
-
// Validate inputs
|
|
448
|
-
if (!username || !password) {
|
|
449
|
-
throw new AppMeshError('Username and password are required', 400, null, 'INVALID_CREDENTIALS');
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const auth = base64Utils.encode(`${username}:${password}`);
|
|
453
|
-
const headers = {
|
|
454
|
-
[CONSTANTS.HTTP_HEADER_KEY_AUTH]: `Basic ${auth}`,
|
|
455
|
-
"X-Set-Cookie": "true"
|
|
456
|
-
};
|
|
457
|
-
// Omitted, not "false", when declined: the daemon only issues on an explicit opt-in.
|
|
458
|
-
if (this._wantRefreshToken()) headers["X-Refresh-Token-Request"] = "true";
|
|
459
|
-
if (totpCode) headers["X-Totp-Code"] = totpCode;
|
|
460
|
-
if (tokenExpire) headers["X-Expire-Seconds"] = parseDuration(tokenExpire);
|
|
461
|
-
if (audience) headers["X-Audience"] = audience;
|
|
462
|
-
|
|
463
|
-
const response = await this._request("post", "/appmesh/login", null, { headers });
|
|
464
|
-
this._captureRefreshToken(response);
|
|
465
|
-
this._tokenExpireSeconds = tokenExpire ? parseDuration(tokenExpire) : null;
|
|
312
|
+
/** Attach a caller-owned Dex access token in memory. */
|
|
313
|
+
set_bearer_token(token) {
|
|
314
|
+
const value = typeof token === 'string' ? token.trim() : '';
|
|
315
|
+
this._handleTokenUpdate(value || null);
|
|
466
316
|
}
|
|
467
317
|
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
* @param {string} token - JWT token to verify
|
|
472
|
-
* @param {string} [permission=null] - Permission to check
|
|
473
|
-
* @param {string} [audience] - JWT audience
|
|
474
|
-
* @param {boolean} [updateSession=true] - When true, updates this client session with the
|
|
475
|
-
* verified token and persists local auth state on success. When false, the token is only
|
|
476
|
-
* verified and local state is unchanged.
|
|
477
|
-
* @returns {Promise<{success: boolean, responseText: string}>} Verification result (success is always true).
|
|
478
|
-
* @throws {AppMeshError} If verification fails (invalid token, permission or audience mismatch, network error).
|
|
479
|
-
*/
|
|
480
|
-
async authenticate(token, permission = null, audience = CONSTANTS.DEFAULT_JWT_AUDIENCE, updateSession = true) {
|
|
481
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
482
|
-
if (permission) headers["X-Permission"] = permission;
|
|
483
|
-
if (audience) headers["X-Audience"] = audience;
|
|
484
|
-
if (updateSession) headers["X-Set-Cookie"] = "true";
|
|
485
|
-
const response = await this._request("post", "/appmesh/auth", null, { headers });
|
|
486
|
-
if (updateSession) {
|
|
487
|
-
// /appmesh/auth returns the same body as login. In a browser this is the only way to
|
|
488
|
-
// learn the lifetime — set_token() cannot replace an HttpOnly cookie — so without it
|
|
489
|
-
// auto-refresh would idle at the poll interval until the session silently died.
|
|
490
|
-
this._captureRefreshToken(response);
|
|
491
|
-
}
|
|
492
|
-
const responseText = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
|
|
493
|
-
return { success: true, responseText };
|
|
318
|
+
/** Source-compatible alias for set_bearer_token(). */
|
|
319
|
+
set_token(token) {
|
|
320
|
+
this.set_bearer_token(token);
|
|
494
321
|
}
|
|
495
322
|
|
|
496
|
-
/**
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
* For server-side verification, use authenticate() instead.
|
|
500
|
-
* @param {string} token - A valid JWT token string. In Node.js this updates the outgoing Cookie
|
|
501
|
-
* header; in browsers it cannot replace the HttpOnly auth cookie.
|
|
502
|
-
*/
|
|
503
|
-
set_token(token) {
|
|
504
|
-
if (ENV.isNode) {
|
|
505
|
-
this._handleTokenUpdate(token);
|
|
506
|
-
} else {
|
|
507
|
-
// Browser: auth token is HttpOnly (set by server via Set-Cookie),
|
|
508
|
-
// document.cookie cannot access or override HttpOnly cookies.
|
|
509
|
-
// Use authenticate() for browser-based token verification instead.
|
|
510
|
-
console.warn('set_token() is not supported in browser mode (auth cookie is HttpOnly). Use authenticate() instead.');
|
|
511
|
-
}
|
|
323
|
+
/** Remove the locally attached bearer without contacting Engine or Dex. */
|
|
324
|
+
clear_bearer_token() {
|
|
325
|
+
this._handleTokenUpdate(null);
|
|
512
326
|
}
|
|
513
327
|
|
|
514
|
-
/**
|
|
515
|
-
* Get the current access token known to this client.
|
|
516
|
-
* @returns {string|null} Current JWT token or null
|
|
517
|
-
* @protected
|
|
518
|
-
*/
|
|
328
|
+
/** Return the current in-memory Dex access token. */
|
|
519
329
|
_getAccessToken() {
|
|
520
330
|
return this._token || null;
|
|
521
331
|
}
|
|
522
332
|
|
|
523
|
-
/**
|
|
524
|
-
* Store a new access token and propagate it to the transport layer.
|
|
525
|
-
* Called after login/renew/authenticate responses and by set_token().
|
|
526
|
-
* Base implementation keeps the token in memory, syncs the Node.js outgoing
|
|
527
|
-
* Cookie header, and reschedules auto-refresh when enabled.
|
|
528
|
-
* Subclasses using other transports (e.g. TCP) override this to sync their own store.
|
|
529
|
-
* @param {string|null} token - New JWT token (falsy clears the in-memory token)
|
|
530
|
-
* @protected
|
|
531
|
-
*/
|
|
333
|
+
/** Store only the in-memory access token; never synthesize a Cookie header. */
|
|
532
334
|
_handleTokenUpdate(token) {
|
|
533
|
-
const COOKIE_NAME = 'appmesh_auth_token';
|
|
534
335
|
this._token = token || null;
|
|
535
|
-
if (ENV.isNode && this._token) {
|
|
536
|
-
const existingCookies = this._client.defaults.headers.Cookie || '';
|
|
537
|
-
const cookies = existingCookies.split('; ').filter(c =>
|
|
538
|
-
c && !c.startsWith(COOKIE_NAME + '=')
|
|
539
|
-
);
|
|
540
|
-
cookies.push(`${COOKIE_NAME}=${this._token}`);
|
|
541
|
-
this._client.defaults.headers.Cookie = cookies.join('; ');
|
|
542
|
-
}
|
|
543
|
-
this._autoRefreshJwt = this._token;
|
|
544
|
-
if (this._autoRefreshEnabled) {
|
|
545
|
-
this._scheduleTokenRefresh();
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Store the refresh token carried by a login/totp-validate/renew response body.
|
|
551
|
-
* Both modes rotate it on renew, so a present value replaces the stored one and an
|
|
552
|
-
* absent value keeps it.
|
|
553
|
-
* @param {Object} response - Response object from _request
|
|
554
|
-
* @private
|
|
555
|
-
*/
|
|
556
|
-
_captureRefreshToken(response, epoch = this._sessionEpoch) {
|
|
557
|
-
if (epoch !== this._sessionEpoch) return; // superseded by a logout
|
|
558
|
-
let data = response && response.data;
|
|
559
|
-
if (typeof data === 'string') {
|
|
560
|
-
try { data = JSON.parse(data); } catch (_) { return; }
|
|
561
|
-
}
|
|
562
|
-
if (!data) return;
|
|
563
|
-
if (typeof data.refresh_token === 'string' && data.refresh_token) {
|
|
564
|
-
this._refreshToken = data.refresh_token;
|
|
565
|
-
}
|
|
566
|
-
// The daemon reports the lifetime in the response body, which stays readable even when
|
|
567
|
-
// the token itself is an HttpOnly cookie. That is what lets a browser pace itself off
|
|
568
|
-
// the real lifetime instead of falling back to a fixed cadence.
|
|
569
|
-
if (Number.isFinite(data.expire_time) && Number.isFinite(data.issued_at)) {
|
|
570
|
-
this._tokenTimes = { exp: data.expire_time, iat: data.issued_at };
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Logout from the current session.
|
|
576
|
-
*/
|
|
577
|
-
async logout() {
|
|
578
|
-
try {
|
|
579
|
-
// OAuth2 (Keycloak) proxy mode: present the refresh token so the daemon can end the
|
|
580
|
-
// upstream session. Optional — logoff still succeeds (local revoke) without it.
|
|
581
|
-
const headers = this._refreshToken ? { "X-Refresh-Token": this._refreshToken } : {};
|
|
582
|
-
await this._request("post", "/appmesh/self/logoff", null, { headers });
|
|
583
|
-
} catch (error) {
|
|
584
|
-
console.error("Failed to logoff:", error.message);
|
|
585
|
-
} finally {
|
|
586
|
-
// Clean up keepAlive connections
|
|
587
|
-
if (ENV.isNode && this._client.defaults.httpsAgent) {
|
|
588
|
-
this._client.defaults.httpsAgent.destroy();
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
this._stopAutoRefresh();
|
|
592
|
-
// Supersede any renewal already in flight: its response would otherwise land after
|
|
593
|
-
// this block and re-populate the token, the refresh token and the times — leaving a
|
|
594
|
-
// logged-out client holding a credential pair the daemon minted after logoff.
|
|
595
|
-
this._sessionEpoch++;
|
|
596
|
-
this._token = null;
|
|
597
|
-
this._refreshToken = null;
|
|
598
|
-
this._tokenTimes = null;
|
|
599
|
-
|
|
600
|
-
// Clear the outgoing auth cookie (Node); the browser's HttpOnly auth cookie is cleared
|
|
601
|
-
// server-side on logoff.
|
|
602
|
-
if (ENV.isNode) {
|
|
603
|
-
this._client.defaults.headers.Cookie = null;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Enable or disable background token auto-refresh.
|
|
610
|
-
* @param {boolean} enable - true to start, false to stop
|
|
611
|
-
* @param {string} [jwtToken] - Optional token used only to calculate the first refresh delay
|
|
612
|
-
*/
|
|
613
|
-
set_auto_refresh_token(enable, jwtToken = null) {
|
|
614
|
-
this._stopAutoRefresh();
|
|
615
|
-
this._autoRefreshEnabled = enable;
|
|
616
|
-
if (enable) {
|
|
617
|
-
this._autoRefreshJwt = jwtToken || this._getAccessToken() || null;
|
|
618
|
-
this._scheduleTokenRefresh();
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* Choose whether login/totp-validate/renew ask the daemon to issue a refresh token.
|
|
624
|
-
*
|
|
625
|
-
* A refresh token is a long-lived credential (TTL `max(2*access, access+1h)`), so a one-shot
|
|
626
|
-
* script that logs in, makes a call and exits would be handed a multi-day credential it
|
|
627
|
-
* neither stores nor revokes. Hence the caller decides.
|
|
628
|
-
*
|
|
629
|
-
* @param {boolean|null} [value=null] - `true`/`false` force the choice; `null` (default) follows
|
|
630
|
-
* auto-refresh, which is already the "is this client long-lived?" signal — except in a browser.
|
|
631
|
-
*
|
|
632
|
-
* Browser asymmetry: the resolved default is `false` even with auto-refresh on. The auth cookie is
|
|
633
|
-
* HttpOnly precisely so XSS cannot reach it; a refresh token held in JS memory would hand that back
|
|
634
|
-
* to any injected script, and for far longer. Nothing in the browser needs it either — pacing reads
|
|
635
|
-
* `expire_time`/`issued_at` from the response body and renewal goes through the cookie. An explicit
|
|
636
|
-
* `true` is still honoured.
|
|
637
|
-
*/
|
|
638
|
-
set_use_refresh_token(value = null) {
|
|
639
|
-
this._useRefreshToken = value;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Resolve the refresh-token tri-state for the outgoing request.
|
|
644
|
-
* @returns {boolean} true to send the opt-in header; false to omit it entirely
|
|
645
|
-
* @private
|
|
646
|
-
*/
|
|
647
|
-
_wantRefreshToken() {
|
|
648
|
-
if (typeof this._useRefreshToken === 'boolean') return this._useRefreshToken;
|
|
649
|
-
return ENV.isNode && this._autoRefreshEnabled;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** @private */
|
|
653
|
-
_stopAutoRefresh() {
|
|
654
|
-
this._autoRefreshEnabled = false;
|
|
655
|
-
this._refreshFailures = 0;
|
|
656
|
-
if (this._refreshTimer) {
|
|
657
|
-
clearTimeout(this._refreshTimer);
|
|
658
|
-
this._refreshTimer = null;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Seconds before expiry at which to renew: a fraction of the token's own lifetime,
|
|
664
|
-
* floored at TOKEN_REFRESH_OFFSET_SECONDS.
|
|
665
|
-
* @param {string} token - JWT the margin is derived from (also seeds the jitter)
|
|
666
|
-
* @param {number} exp - Expiry claim, epoch seconds
|
|
667
|
-
* @param {number} iat - Issued-at claim, epoch seconds; 0 when absent
|
|
668
|
-
* @returns {number} Margin in seconds
|
|
669
|
-
* @private
|
|
670
|
-
*/
|
|
671
|
-
static _refreshMargin(token, exp, iat) {
|
|
672
|
-
const lifetime = (iat > 0 && exp > iat) ? exp - iat : exp - Date.now() / 1000;
|
|
673
|
-
let margin = Math.max(lifetime * (1 - CONSTANTS.TOKEN_REFRESH_LIFETIME_RATIO), CONSTANTS.TOKEN_REFRESH_OFFSET_SECONDS);
|
|
674
|
-
|
|
675
|
-
// Jitter derived from the token: stable across polls, distinct per client.
|
|
676
|
-
const spread = margin * CONSTANTS.TOKEN_REFRESH_JITTER_RATIO;
|
|
677
|
-
margin += ((_fnv1a32(token) % 2001) / 1000 - 1) * spread;
|
|
678
|
-
|
|
679
|
-
// Clamp last: the 30s floor (and its jitter) must never exceed the token's own life,
|
|
680
|
-
// or every renewal would land past the refresh point and the loop would spin at ~1Hz.
|
|
681
|
-
return lifetime > 0 ? Math.min(margin, lifetime / 2) : margin;
|
|
682
336
|
}
|
|
683
337
|
|
|
684
|
-
/**
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
* @returns {number} Delay in seconds
|
|
688
|
-
* @private
|
|
689
|
-
*/
|
|
690
|
-
static _refreshRetryDelay(failures) {
|
|
691
|
-
const shift = Math.min(Math.max(failures - 1, 0), 16);
|
|
692
|
-
return Math.min(CONSTANTS.TOKEN_REFRESH_RETRY_BASE_SECONDS * Math.pow(2, shift), CONSTANTS.TOKEN_REFRESH_RETRY_MAX_SECONDS);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Decide how long to sleep and whether a renewal is due when that sleep ends.
|
|
697
|
-
* The sleep is capped at the poll interval so a token replaced elsewhere is noticed;
|
|
698
|
-
* waking early is not by itself a reason to renew.
|
|
699
|
-
* @returns {{delaySec: number, due: boolean}}
|
|
700
|
-
* @private
|
|
701
|
-
*/
|
|
702
|
-
_computeRefreshPlan() {
|
|
703
|
-
const poll = CONSTANTS.TOKEN_REFRESH_POLL_SECONDS;
|
|
704
|
-
|
|
705
|
-
const token = this._autoRefreshJwt || this._getAccessToken();
|
|
706
|
-
const tokenTimes = token ? _decodeJwtTimes(token) : null;
|
|
707
|
-
|
|
708
|
-
// In a browser the auth cookie is HttpOnly, so the token is never readable here. Pace
|
|
709
|
-
// off the lifetime the daemon reported in the login/renew body instead — same 60% rule
|
|
710
|
-
// as every other SDK, rather than the fixed cadence this change exists to remove.
|
|
711
|
-
//
|
|
712
|
-
// Prefer whichever source expires later. _autoRefreshJwt is set once by
|
|
713
|
-
// set_auto_refresh_token and never updated in a browser (_handleTokenUpdate is
|
|
714
|
-
// Node-only), so a frozen copy would otherwise shadow the times every renewal
|
|
715
|
-
// refreshes — and once it aged past its refresh point the loop would renew once per
|
|
716
|
-
// second forever, which is far worse than the cadence this replaced.
|
|
717
|
-
const times = this._tokenTimes && (!tokenTimes || this._tokenTimes.exp > tokenTimes.exp)
|
|
718
|
-
? this._tokenTimes
|
|
719
|
-
: tokenTimes;
|
|
720
|
-
|
|
721
|
-
if (!token && !times) {
|
|
722
|
-
// A held refresh token can still mint a new access token, so an access token lost
|
|
723
|
-
// to an expired cookie is recoverable — but only if we actually try.
|
|
724
|
-
if (this._refreshToken) return { delaySec: 1, due: true };
|
|
725
|
-
return { delaySec: poll, due: false }; // nothing to renew, just idle
|
|
726
|
-
}
|
|
727
|
-
if (!times) return { delaySec: poll, due: true }; // unreadable lifetime: fixed cadence
|
|
728
|
-
|
|
729
|
-
const nowSec = Date.now() / 1000;
|
|
730
|
-
// Times saying the session is already dead cannot be acted on once per second: a
|
|
731
|
-
// successful renewal would have replaced them, so they are stale rather than urgent.
|
|
732
|
-
// Fall back to the fixed cadence, which bounds the worst case at the poll interval.
|
|
733
|
-
if (times.exp <= nowSec) return { delaySec: poll, due: true };
|
|
734
|
-
|
|
735
|
-
// Jitter needs a per-client seed; without a readable token the session times alone
|
|
736
|
-
// would collide for every client that logged in during the same second, so mix in
|
|
737
|
-
// this client's own random seed.
|
|
738
|
-
const seed = token || `${this._jitterSeed}.${times.iat}.${times.exp}`;
|
|
739
|
-
const wait = times.exp - AppMeshClient._refreshMargin(seed, times.exp, times.iat) - nowSec;
|
|
740
|
-
if (wait <= 0) return { delaySec: 1, due: true }; // at or past the refresh point
|
|
741
|
-
if (wait > poll) return { delaySec: poll, due: false }; // not due; wake only to re-evaluate
|
|
742
|
-
return { delaySec: wait, due: true };
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/** @private */
|
|
746
|
-
_scheduleTokenRefresh() {
|
|
747
|
-
if (!this._autoRefreshEnabled) return;
|
|
748
|
-
|
|
749
|
-
// Replace any pending timer so re-scheduling (e.g. from _handleTokenUpdate) never stacks timers
|
|
750
|
-
if (this._refreshTimer) {
|
|
751
|
-
clearTimeout(this._refreshTimer);
|
|
752
|
-
this._refreshTimer = null;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
let { delaySec, due } = this._computeRefreshPlan();
|
|
756
|
-
if (this._refreshFailures > 0) {
|
|
757
|
-
// Retry on the backoff schedule, not the stale plan.
|
|
758
|
-
delaySec = AppMeshClient._refreshRetryDelay(this._refreshFailures);
|
|
759
|
-
due = true;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
this._refreshTimer = setTimeout(async () => {
|
|
763
|
-
this._refreshTimer = null;
|
|
764
|
-
if (!this._autoRefreshEnabled) return;
|
|
765
|
-
if (due) {
|
|
766
|
-
try {
|
|
767
|
-
// _request captures the renewed token cookie and routes it through
|
|
768
|
-
// _handleTokenUpdate, which updates _autoRefreshJwt for precise delays
|
|
769
|
-
await this.renew_token();
|
|
770
|
-
if (this._refreshFailures > 0) {
|
|
771
|
-
console.info(`Auto-refresh: token renewal recovered after ${this._refreshFailures} failure(s)`);
|
|
772
|
-
}
|
|
773
|
-
this._refreshFailures = 0;
|
|
774
|
-
} catch (err) {
|
|
775
|
-
this._refreshFailures++;
|
|
776
|
-
// Log sparsely: a daemon outage must not flood at the backoff rate.
|
|
777
|
-
if (this._refreshFailures === 1 || this._refreshFailures % CONSTANTS.TOKEN_REFRESH_LOG_EVERY === 0) {
|
|
778
|
-
console.warn(`Auto-refresh: token renewal failed (attempt ${this._refreshFailures}):`, err.message);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
this._scheduleTokenRefresh(); // keep the loop armed, failure or not
|
|
783
|
-
}, delaySec * 1000);
|
|
784
|
-
|
|
785
|
-
// Don't block Node.js process exit
|
|
786
|
-
if (this._refreshTimer.unref) {
|
|
787
|
-
this._refreshTimer.unref();
|
|
788
|
-
}
|
|
338
|
+
/** Return Engine's public Dex/OIDC configuration. */
|
|
339
|
+
async get_auth_config() {
|
|
340
|
+
return (await this._request("get", "/appmesh/auth/config")).data;
|
|
789
341
|
}
|
|
790
342
|
|
|
791
|
-
/**
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
* A held refresh token is the sole credential the daemon needs, so renewal succeeds even
|
|
795
|
-
* after the access token expired — that is how a missed refresh window recovers without a
|
|
796
|
-
* re-login. Without one, the daemon authenticates the access token instead.
|
|
797
|
-
*
|
|
798
|
-
* @param {string|number} [tokenExpire] - Token expiry (integer seconds or ISO 8601 string).
|
|
799
|
-
* Defaults to the value used at login; the header is omitted when that is unknown, so the
|
|
800
|
-
* daemon applies its own default rather than silently extending a short-lived token.
|
|
801
|
-
*/
|
|
802
|
-
async renew_token(tokenExpire = null) {
|
|
803
|
-
// Serialize renewals: rotation makes a refresh token single-use, so two concurrent
|
|
804
|
-
// renewals present the same one and the loser is told it is revoked — permanently
|
|
805
|
-
// wedging a client that holds no other credential. (See also the AppMeshWorker
|
|
806
|
-
// constructor docs on sharing one client instead of running two renew loops.)
|
|
807
|
-
const run = (this._renewChain || Promise.resolve()).then(() => this._renewTokenOnce(tokenExpire));
|
|
808
|
-
this._renewChain = run.catch(() => { /* a failed renewal must not break the queue */ });
|
|
809
|
-
return run;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/** @private */
|
|
813
|
-
async _renewTokenOnce(tokenExpire) {
|
|
814
|
-
const expire = (tokenExpire === null || tokenExpire === undefined) ? this._tokenExpireSeconds : tokenExpire;
|
|
815
|
-
const headers = {};
|
|
816
|
-
if (this._wantRefreshToken()) headers["X-Refresh-Token-Request"] = "true";
|
|
817
|
-
if (expire !== null && expire !== undefined) {
|
|
818
|
-
headers["X-Expire-Seconds"] = parseDuration(expire);
|
|
819
|
-
}
|
|
820
|
-
if (this._refreshToken) {
|
|
821
|
-
headers["X-Refresh-Token"] = this._refreshToken;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
let response;
|
|
825
|
-
try {
|
|
826
|
-
response = await this._request("post", "/appmesh/token/renew", null, { headers });
|
|
827
|
-
} catch (error) {
|
|
828
|
-
// A rejected refresh token will never be accepted again (rotated away, revoked, or the
|
|
829
|
-
// session ended). Drop it so the next attempt presents the access token instead.
|
|
830
|
-
if (error && error.statusCode === 401) this._refreshToken = null;
|
|
831
|
-
throw error;
|
|
832
|
-
}
|
|
833
|
-
// Both modes rotate the refresh token on renew — store the new one for the next cycle.
|
|
834
|
-
this._captureRefreshToken(response);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* Get the decoded OTP provisioning URI for the current user.
|
|
839
|
-
* @returns {Promise<string>} Decoded `otpauth://...` URI, not just the raw secret field
|
|
840
|
-
*/
|
|
841
|
-
async get_totp_uri() {
|
|
842
|
-
const response = await this._request("post", "/appmesh/totp/secret");
|
|
843
|
-
return base64Utils.decode(response.data["mfa_uri"]);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Setup 2FA with a verification code and update the current session token cookie.
|
|
848
|
-
* @param {string} totpCode - TOTP verification code
|
|
849
|
-
*/
|
|
850
|
-
async enable_totp(totpCode) {
|
|
851
|
-
const headers = { "X-Totp-Code": totpCode };
|
|
852
|
-
await this._request("post", "/appmesh/totp/setup", null, { headers });
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Validate a TOTP login challenge and update the current session token cookie.
|
|
857
|
-
* @param {string} username - Username
|
|
858
|
-
* @param {string} totpChallenge - Server challenge
|
|
859
|
-
* @param {string} totpCode - TOTP code
|
|
860
|
-
* @param {string|number} [tokenExpire] - Token expiry in seconds or ISO8601 duration (e.g. "P1DT12H", 604800)
|
|
861
|
-
*/
|
|
862
|
-
async validate_totp(username, totpChallenge, totpCode, tokenExpire = CONSTANTS.DEFAULT_TOKEN_EXPIRE_SECONDS) {
|
|
863
|
-
const body = {
|
|
864
|
-
"user_name": username,
|
|
865
|
-
"totp_code": totpCode,
|
|
866
|
-
"totp_challenge": totpChallenge,
|
|
867
|
-
"expire_seconds": parseDuration(tokenExpire)
|
|
868
|
-
};
|
|
869
|
-
// Set cookie header for browser
|
|
870
|
-
const headers = { "X-Set-Cookie": "true" };
|
|
871
|
-
if (this._wantRefreshToken()) headers["X-Refresh-Token-Request"] = "true";
|
|
872
|
-
|
|
873
|
-
const response = await this._request("post", "/appmesh/totp/validate", body, { headers });
|
|
874
|
-
// A validated challenge completes the login, so it owes the same session setup as
|
|
875
|
-
// login(); auto-refresh itself is (re)armed by _handleTokenUpdate when the token lands.
|
|
876
|
-
this._captureRefreshToken(response);
|
|
877
|
-
this._tokenExpireSeconds = tokenExpire ? parseDuration(tokenExpire) : null;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Disable TOTP for user
|
|
882
|
-
* @param {string} [user='self'] - Username
|
|
883
|
-
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
884
|
-
*/
|
|
885
|
-
async disable_totp(user = "self") {
|
|
886
|
-
await this._request("post", `/appmesh/totp/${user}/disable`);
|
|
887
|
-
return true;
|
|
343
|
+
/** Return the verified Dex principal represented by the current bearer. */
|
|
344
|
+
async get_current_principal() {
|
|
345
|
+
return (await this._request("get", "/appmesh/principal/self")).data;
|
|
888
346
|
}
|
|
889
347
|
|
|
890
348
|
/**
|
|
@@ -1359,88 +817,26 @@ class AppMeshClient {
|
|
|
1359
817
|
}
|
|
1360
818
|
|
|
1361
819
|
|
|
1362
|
-
/**
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
* @param {string} newPassword - New password
|
|
1366
|
-
* @param {string} [username="self"] - Username
|
|
1367
|
-
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1368
|
-
*/
|
|
1369
|
-
async update_password(oldPassword, newPassword, username = "self") {
|
|
1370
|
-
const body = {
|
|
1371
|
-
"old_password": base64Utils.encode(oldPassword),
|
|
1372
|
-
"new_password": base64Utils.encode(newPassword)
|
|
1373
|
-
};
|
|
1374
|
-
await this._request("post", `/appmesh/user/${username}/passwd`, body);
|
|
1375
|
-
return true;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
/**
|
|
1379
|
-
* Add new user
|
|
1380
|
-
* @param {string} username - Username
|
|
1381
|
-
* @param {Object} userData - User definition
|
|
1382
|
-
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1383
|
-
*/
|
|
1384
|
-
async add_user(username, userData) {
|
|
1385
|
-
await this._request("put", `/appmesh/user/${username}`, userData);
|
|
1386
|
-
return true;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
/**
|
|
1390
|
-
* Delete user
|
|
1391
|
-
* @param {string} username - Username
|
|
1392
|
-
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1393
|
-
*/
|
|
1394
|
-
async delete_user(username) {
|
|
1395
|
-
await this._request("delete", `/appmesh/user/${username}`);
|
|
1396
|
-
return true;
|
|
820
|
+
/** List Engine authorization overlays keyed by immutable Dex principal ID. */
|
|
821
|
+
async list_principals() {
|
|
822
|
+
return (await this._request("get", "/appmesh/principals")).data;
|
|
1397
823
|
}
|
|
1398
824
|
|
|
1399
|
-
/**
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1403
|
-
*/
|
|
1404
|
-
async lock_user(username) {
|
|
1405
|
-
await this._request("post", `/appmesh/user/${username}/lock`);
|
|
825
|
+
/** Update an Engine authorization overlay; never mutate IdP account data. */
|
|
826
|
+
async update_principal(principalId, policy) {
|
|
827
|
+
await this._request("post", `/appmesh/principal/${encodeURIComponent(principalId)}`, policy);
|
|
1406
828
|
return true;
|
|
1407
829
|
}
|
|
1408
830
|
|
|
1409
|
-
/**
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
* @returns {Promise<boolean>} true on success; failures throw AppMeshError
|
|
1413
|
-
*/
|
|
1414
|
-
async unlock_user(username) {
|
|
1415
|
-
await this._request("post", `/appmesh/user/${username}/unlock`);
|
|
831
|
+
/** Delete only an Engine authorization overlay. */
|
|
832
|
+
async delete_principal(principalId) {
|
|
833
|
+
await this._request("delete", `/appmesh/principal/${encodeURIComponent(principalId)}`);
|
|
1416
834
|
return true;
|
|
1417
835
|
}
|
|
1418
836
|
|
|
1419
|
-
/**
|
|
1420
|
-
* Get user list
|
|
1421
|
-
* @returns {Object[]} User array
|
|
1422
|
-
*/
|
|
1423
|
-
async list_users() {
|
|
1424
|
-
const response = await this._request("get", "/appmesh/users");
|
|
1425
|
-
return response.data;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
/**
|
|
1429
|
-
* Get current user info
|
|
1430
|
-
* @returns {Object} User properties
|
|
1431
|
-
*/
|
|
837
|
+
/** Safe compatibility alias returning a Principal, not an IdP user. */
|
|
1432
838
|
async get_current_user() {
|
|
1433
|
-
|
|
1434
|
-
return response.data;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
/**
|
|
1438
|
-
* Get all user groups
|
|
1439
|
-
* @returns {Object[]} Group array
|
|
1440
|
-
*/
|
|
1441
|
-
async list_groups() {
|
|
1442
|
-
const response = await this._request("get", "/appmesh/user/groups");
|
|
1443
|
-
return response.data;
|
|
839
|
+
return this.get_current_principal();
|
|
1444
840
|
}
|
|
1445
841
|
|
|
1446
842
|
/**
|
|
@@ -1452,15 +848,17 @@ class AppMeshClient {
|
|
|
1452
848
|
return response.data;
|
|
1453
849
|
}
|
|
1454
850
|
|
|
1455
|
-
/**
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
*/
|
|
1459
|
-
async get_user_permissions() {
|
|
1460
|
-
const response = await this._request("get", "/appmesh/user/permissions");
|
|
851
|
+
/** Get effective permissions for the current verified principal. */
|
|
852
|
+
async get_principal_permissions() {
|
|
853
|
+
const response = await this._request("get", "/appmesh/principal/self/permissions");
|
|
1461
854
|
return response.data;
|
|
1462
855
|
}
|
|
1463
856
|
|
|
857
|
+
/** Safe compatibility alias for get_principal_permissions(). */
|
|
858
|
+
async get_user_permissions() {
|
|
859
|
+
return this.get_principal_permissions();
|
|
860
|
+
}
|
|
861
|
+
|
|
1464
862
|
/**
|
|
1465
863
|
* Get all roles and permissions
|
|
1466
864
|
* @returns {Object[]} Role array
|
|
@@ -1544,6 +942,11 @@ class AppMeshClient {
|
|
|
1544
942
|
headers[CONSTANTS.HTTP_USER_AGENT_HEADER_NAME] = CONSTANTS.HTTP_USER_AGENT;
|
|
1545
943
|
}
|
|
1546
944
|
|
|
945
|
+
const token = this._getAccessToken();
|
|
946
|
+
if (token) {
|
|
947
|
+
headers[CONSTANTS.HTTP_HEADER_KEY_AUTH] = `Bearer ${token}`;
|
|
948
|
+
}
|
|
949
|
+
|
|
1547
950
|
// Add forwarding host if specified
|
|
1548
951
|
if (this.forwardingHost) {
|
|
1549
952
|
if (this.forwardingHost.includes(":")) {
|
|
@@ -1588,7 +991,7 @@ class AppMeshClient {
|
|
|
1588
991
|
const requestConfig = {
|
|
1589
992
|
method,
|
|
1590
993
|
url: path,
|
|
1591
|
-
withCredentials:
|
|
994
|
+
withCredentials: false,
|
|
1592
995
|
...config,
|
|
1593
996
|
headers: { ...headers },
|
|
1594
997
|
params: { ...params }
|
|
@@ -1599,39 +1002,13 @@ class AppMeshClient {
|
|
|
1599
1002
|
}
|
|
1600
1003
|
|
|
1601
1004
|
const response = await this._client(requestConfig);
|
|
1602
|
-
if (response.status
|
|
1005
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1603
1006
|
const errMsg = this._extractErrorMessage(response.data);
|
|
1604
|
-
if (response.status === CONSTANTS.HTTP_STATUS_PRECONDITION_REQUIRED) {
|
|
1605
|
-
throw new TotpRequiredError(errMsg, response.data);
|
|
1606
|
-
}
|
|
1607
1007
|
throw new AppMeshError(errMsg, response.status, response.data);
|
|
1608
1008
|
}
|
|
1609
1009
|
|
|
1610
|
-
// axios response header use lower case
|
|
1611
|
-
if (ENV.isNode && response.headers['Set-Cookie'.toLowerCase()]) {
|
|
1612
|
-
// Handle array of cookies or single cookie
|
|
1613
|
-
const cookies = Array.isArray(response.headers['Set-Cookie'.toLowerCase()]) ?
|
|
1614
|
-
response.headers['Set-Cookie'.toLowerCase()] :
|
|
1615
|
-
[response.headers['Set-Cookie'.toLowerCase()]];
|
|
1616
|
-
|
|
1617
|
-
// Join all cookies with semicolon separator
|
|
1618
|
-
this._client.defaults.headers.Cookie = cookies.join('; ');
|
|
1619
|
-
|
|
1620
|
-
// Keep the in-memory token store in sync with the server-issued cookie
|
|
1621
|
-
// (login, renew, authenticate, TOTP setup/validate)
|
|
1622
|
-
const authCookie = cookies
|
|
1623
|
-
.map(c => c.split(';')[0].trim())
|
|
1624
|
-
.find(c => c.startsWith('appmesh_auth_token='));
|
|
1625
|
-
if (authCookie) {
|
|
1626
|
-
this._handleTokenUpdate(authCookie.substring('appmesh_auth_token='.length));
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
1010
|
return response;
|
|
1631
1011
|
} catch (error) {
|
|
1632
|
-
if (error instanceof AppMeshError && error.statusCode === CONSTANTS.HTTP_STATUS_PRECONDITION_REQUIRED) {
|
|
1633
|
-
throw error;
|
|
1634
|
-
}
|
|
1635
1012
|
throw this.onError(error);
|
|
1636
1013
|
}
|
|
1637
1014
|
}
|
|
@@ -1766,5 +1143,5 @@ class AppRun {
|
|
|
1766
1143
|
}
|
|
1767
1144
|
|
|
1768
1145
|
// Export the main classes
|
|
1769
|
-
export { AppMeshClient, AppOutput, AppRun, AppMeshError,
|
|
1146
|
+
export { AppMeshClient, AppOutput, AppRun, AppMeshError, AppRemovedError, TransportDisconnectedError, DEFAULT_CA_FILE };
|
|
1770
1147
|
export default AppMeshClient;
|