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.
@@ -6,7 +6,7 @@ import fs from 'fs'
6
6
  import os from 'os'
7
7
  import { v1 as uuidv1 } from 'uuid'
8
8
  import msgpack from 'msgpack-lite'
9
- import AppMeshClient, { AppMeshError, TotpRequiredError, AppRemovedError, TransportDisconnectedError, DEFAULT_CA_FILE } from './appmesh.js'
9
+ import AppMeshClient, { AppMeshError, AppRemovedError, TransportDisconnectedError, DEFAULT_CA_FILE } from './appmesh.js'
10
10
 
11
11
  // Constants
12
12
  const TCP_BLOCK_SIZE = 16 * 1024 - 128 // TLS-optimized chunk size
@@ -20,14 +20,6 @@ const HTTP_HEADER_KEY_USER_AGENT = 'User-Agent'
20
20
  const HTTP_HEADER_KEY_X_FILE_PATH = 'X-File-Path'
21
21
  const HTTP_HEADER_KEY_AUTH = 'Authorization'
22
22
 
23
- // Auth endpoints returning a new access_token in the JSON body (allowlist per
24
- // docs/source/SDKContract.md "Auth Token Synchronization").
25
- // Login/auth/totp_validate: apply token only when the request carried X-Set-Cookie: true
26
- const AUTH_SET_COOKIE_PATHS = new Set(['/appmesh/login', '/appmesh/auth', '/appmesh/totp/validate'])
27
- // Renew/setup: always apply (client already has an active session)
28
- const AUTH_RENEW_PATHS = new Set(['/appmesh/token/renew', '/appmesh/totp/setup'])
29
- const LOGOFF_PATH = '/appmesh/self/logoff'
30
-
31
23
  /**
32
24
  * TCP transport for secure TLS connections and framed App Mesh messages.
33
25
  *
@@ -342,7 +334,7 @@ class AppMeshClientTCP extends AppMeshClient {
342
334
  super('https://127.0.0.1:6060', sslConfig)
343
335
  this.tcpTransport = new TCPTransport(tcpAddress, sslConfig)
344
336
 
345
- // Store JWT token explicitly since cookies don't work with TCP
337
+ // Store the caller-owned bearer explicitly for TCP requests.
346
338
  this._token = ''
347
339
  }
348
340
 
@@ -371,59 +363,17 @@ class AppMeshClientTCP extends AppMeshClient {
371
363
  }
372
364
 
373
365
  /**
374
- * Handle token updates for transports that cannot rely on cookies.
375
- *
376
- * This method is called after login, renew_token, and authenticate operations.
377
- * Since TCP doesn't support cookies, we store the token explicitly.
366
+ * Handle caller-owned bearer updates for TCP.
378
367
  *
379
368
  * @param {string} token - New JWT token
380
369
  * @private
381
370
  * @override
382
371
  */
383
372
  _handleTokenUpdate (token) {
384
- // Base implementation stores the token and handles refresh scheduling
385
373
  super._handleTokenUpdate(token)
386
374
  this._token = token || ''
387
375
  }
388
376
 
389
- /**
390
- * Extract and apply a token from auth endpoint responses. TCP has no Set-Cookie,
391
- * so the token comes from the JSON body — only on HTTP 200 and only for the
392
- * SDKContract.md auth endpoint allowlist.
393
- * @param {Object} response - Normalized response object from _request
394
- * @param {string} path - Request URI path
395
- * @param {Object} requestHeaders - Headers sent with the request
396
- * @private
397
- */
398
- _syncTransportToken (response, path, requestHeaders) {
399
- if (response.status !== 200) return
400
-
401
- if (path === LOGOFF_PATH) {
402
- this._handleTokenUpdate(null)
403
- return
404
- }
405
-
406
- // Login/auth/totp_validate: apply only when client requested cookie mode
407
- if (AUTH_SET_COOKIE_PATHS.has(path)) {
408
- if (!requestHeaders || requestHeaders['X-Set-Cookie'] !== 'true') return
409
- } else if (!AUTH_RENEW_PATHS.has(path)) {
410
- return
411
- }
412
-
413
- let data = response.data
414
- // Parse if it's a Buffer containing JSON
415
- if (Buffer.isBuffer(data)) {
416
- try {
417
- data = JSON.parse(data.toString(ENCODING_UTF8))
418
- } catch (e) {
419
- return // Not JSON, ignore
420
- }
421
- }
422
- if (data && data.access_token) {
423
- this._handleTokenUpdate(data.access_token)
424
- }
425
- }
426
-
427
377
  /**
428
378
  * Send an App Mesh request over TCP transport and normalize the response shape.
429
379
  * @private
@@ -447,7 +397,7 @@ class AppMeshClientTCP extends AppMeshClient {
447
397
  // Add authentication token if available
448
398
  const token = this._getAccessToken()
449
399
  if (token) {
450
- request.headers[HTTP_HEADER_KEY_AUTH] = token
400
+ request.headers[HTTP_HEADER_KEY_AUTH] = `Bearer ${token}`
451
401
  }
452
402
 
453
403
  if (options.headers) {
@@ -507,15 +457,9 @@ class AppMeshClientTCP extends AppMeshClient {
507
457
  config: options
508
458
  }
509
459
 
510
- // Sync token from auth endpoint responses (login, authenticate, renew_token, logoff)
511
- this._syncTransportToken(result, path, request.headers)
512
-
513
- // Mirror the HTTP transport: never swallow a non-200 into a success value
514
- if (result.status !== 200) {
460
+ // Mirror the HTTP transport: never swallow a non-2xx into a success value.
461
+ if (result.status < 200 || result.status >= 300) {
515
462
  const errMsg = this._extractErrorMessage(result.data)
516
- if (result.status === 428) {
517
- throw new TotpRequiredError(errMsg, result.data)
518
- }
519
463
  throw new AppMeshError(errMsg, result.status, result.data)
520
464
  }
521
465
 
@@ -709,7 +653,7 @@ class AppMeshClientTCP extends AppMeshClient {
709
653
  * @returns {Promise<Object>} Subscription result with subscription_id, app_name, events.
710
654
  */
711
655
  async subscribe (appName, events, callback) {
712
- this._enableDemuxer()
656
+ await this._enableDemuxer()
713
657
 
714
658
  let path = '/appmesh/subscribe'
715
659
  if (appName && appName !== '*') {
@@ -759,7 +703,7 @@ class AppMeshClientTCP extends AppMeshClient {
759
703
  async add_app (name, appJson, subscribeEvents, callback) {
760
704
  const options = {}
761
705
  if (subscribeEvents && subscribeEvents.length > 0) {
762
- this._enableDemuxer()
706
+ await this._enableDemuxer()
763
707
  options.params = { subscribe_events: subscribeEvents.join(',') }
764
708
  }
765
709
 
@@ -777,8 +721,11 @@ class AppMeshClientTCP extends AppMeshClient {
777
721
  * Enable demuxer for concurrent request-response and event routing.
778
722
  * @private
779
723
  */
780
- _enableDemuxer () {
781
- if (this._demuxer) return
724
+ async _enableDemuxer () {
725
+ if (this._demuxer && this._demuxer._running) return
726
+ if (!this.tcpTransport.connected()) {
727
+ await this.tcpTransport.connect()
728
+ }
782
729
  this._demuxer = new MessageDemuxer(this.tcpTransport)
783
730
  this._demuxer.start()
784
731
  }
package/src/worker.js CHANGED
@@ -91,10 +91,8 @@ class AppMeshWorker {
91
91
  * @param {Object} [options.logger=console] - Logger instance
92
92
  * @param {AppMeshClient} [options.client] - Optional pre-built `AppMeshClient` instance to reuse.
93
93
  * When supplied, `baseURL` and `sslConfig` are ignored and the worker shares the caller's
94
- * client (and therefore its token-refresh state). Use this when a single process needs both
95
- * client (outbound calls like `AddApp`) and worker (`fetch_task`/`send_task_result`) roles to
96
- * avoid two independent `/token/renew` loops fighting each other — the daemon blacklists
97
- * the previous token on every renew, so the slower refresher gets 401 "Token has been revoked".
94
+ * client (and therefore its caller-owned bearer). Use this when a single process needs both
95
+ * client (outbound calls like `AddApp`) and worker (`fetch_task`/`send_task_result`) roles.
98
96
  *
99
97
  * @example
100
98
  * import fs from 'fs';
@@ -109,7 +107,7 @@ class AppMeshWorker {
109
107
  * @example
110
108
  * // Share a single client between client-role and worker-role usage:
111
109
  * const client = new AppMeshClient('https://127.0.0.1:6060', sslConfig);
112
- * await client.login('user', 'pass');
110
+ * client.set_bearer_token(process.env.APPMESH_BEARER_TOKEN);
113
111
  * const worker = new AppMeshWorker('https://127.0.0.1:6060', sslConfig, { client });
114
112
  */
115
113
  constructor (
@@ -125,7 +123,6 @@ class AppMeshWorker {
125
123
  this._client = options.client
126
124
  } else {
127
125
  this._client = new AppMeshClient(baseURL, sslConfig)
128
- this._client.set_auto_refresh_token(false) // Task endpoints use APP_MESH_PROCESS_KEY; no JWT refresh needed.
129
126
  }
130
127
  this._logger = options.logger || console
131
128
  this._stopped = false
@@ -187,7 +184,7 @@ class AppMeshWorker {
187
184
  const attemptStart = Date.now()
188
185
  try {
189
186
  const response = await this._client.request('get', path, null, {
190
- params: { process_key: processKey }
187
+ headers: { 'X-AppMesh-Process-Key': processKey }
191
188
  })
192
189
 
193
190
  if (response.status === 200) {
@@ -247,7 +244,7 @@ class AppMeshWorker {
247
244
  let response
248
245
  try {
249
246
  response = await this._client.request('put', path, result, {
250
- params: { process_key: processKey }
247
+ headers: { 'X-AppMesh-Process-Key': processKey }
251
248
  })
252
249
  } catch (error) {
253
250
  // request() throws AppMeshError on non-200
@@ -309,7 +306,7 @@ class AppMeshWorkerTCP extends AppMeshWorker {
309
306
  * @param {Array<string, number>|{host: string, port: number}} [tcpAddress=['127.0.0.1', 6059]] - TCP server address
310
307
  * @param {Object} [options={}] - Additional options
311
308
  * @param {Object} [options.logger=console] - Logger instance
312
- * @param {AppMeshClientTCP} [options.client] - Pre-built client to reuse (shares token-refresh state); see AppMeshWorker for why. When set, sslConfig/tcpAddress are ignored.
309
+ * @param {AppMeshClientTCP} [options.client] - Pre-built client to reuse (shares its caller-owned bearer); when set, sslConfig/tcpAddress are ignored.
313
310
  *
314
311
  * @example
315
312
  * import fs from 'fs';
@@ -324,7 +321,7 @@ class AppMeshWorkerTCP extends AppMeshWorker {
324
321
  * // Share a single TCP client between client-role and worker-role usage:
325
322
  * import { AppMeshClientTCP } from 'appmesh/tcp';
326
323
  * const client = new AppMeshClientTCP(sslConfig, ['127.0.0.1', 6059]);
327
- * await client.login('user', 'pass');
324
+ * client.set_bearer_token(process.env.APPMESH_BEARER_TOKEN);
328
325
  * const worker = new AppMeshWorkerTCP(sslConfig, ['127.0.0.1', 6059], { client });
329
326
  */
330
327
  constructor (
@@ -353,7 +350,6 @@ class AppMeshWorkerTCP extends AppMeshWorker {
353
350
  if (!this._client) {
354
351
  const { AppMeshClientTCP } = await import('./appmesh_tcp.js')
355
352
  this._client = new AppMeshClientTCP(this._sslConfig, this._tcpAddress)
356
- this._client.set_auto_refresh_token(false) // Task endpoints use APP_MESH_PROCESS_KEY; no JWT refresh needed.
357
353
  }
358
354
  return this._client
359
355
  }
@@ -1,367 +0,0 @@
1
- // appmesh_oauth.js - App Mesh client with Keycloak OAuth2 authentication support.
2
- // Mirrors the Python SDK's AppMeshClientOAuth (client_http_oauth.py): tokens are obtained
3
- // directly from Keycloak and the access token is used as the Bearer token for daemon calls.
4
- import axios from "axios";
5
- import AppMeshClient, { AppMeshError } from "./appmesh.js";
6
-
7
- // OAuth2 scopes requesting identity claims so userinfo returns preferred_username/email
8
- const DEFAULT_OAUTH_SCOPE = "openid profile email";
9
-
10
- /**
11
- * AppMeshClient with Keycloak as the identity provider.
12
- *
13
- * Authentication (login, token renewal, logout, userinfo) is performed directly against
14
- * Keycloak's OpenID Connect endpoints; the Keycloak access token is then used as this
15
- * client's token for App Mesh daemon API calls (RBAC is enforced by the daemon).
16
- */
17
- class AppMeshClientOAuth extends AppMeshClient {
18
- /**
19
- * Initialize an App Mesh client with Keycloak support.
20
- * @param {Object} oauth2 - Keycloak configuration for oauth2 authentication:
21
- * - authServerUrl: Keycloak server URL (e.g. "https://keycloak.example.com/auth")
22
- * - realm: Keycloak realm
23
- * - clientId: Keycloak client ID
24
- * - clientSecret: Keycloak client secret (optional, confidential clients only)
25
- * @param {string} [baseURL] - App Mesh daemon URL (see AppMeshClient)
26
- * @param {Object|false|null} [sslConfig] - SSL tri-state for the daemon connection (see AppMeshClient)
27
- */
28
- constructor(oauth2, baseURL = undefined, sslConfig = null) {
29
- super(baseURL, sslConfig);
30
-
31
- if (!oauth2 || !oauth2.authServerUrl || !oauth2.realm || !oauth2.clientId) {
32
- throw new AppMeshError(
33
- "oauth2 config requires authServerUrl, realm and clientId",
34
- 400,
35
- null,
36
- "INVALID_OAUTH_CONFIG",
37
- );
38
- }
39
-
40
- /** @type {Object} Keycloak configuration @private */
41
- this._oauth2 = {
42
- authServerUrl: String(oauth2.authServerUrl).replace(/\/+$/, ""),
43
- realm: oauth2.realm,
44
- clientId: oauth2.clientId,
45
- clientSecret: oauth2.clientSecret || null,
46
- };
47
-
48
- /** @type {Object} Complete Keycloak token response (access_token, refresh_token, ...) @private */
49
- this._oauthToken = {};
50
-
51
- // Dedicated axios instance for Keycloak endpoints (the inherited this._client
52
- // targets the App Mesh daemon and injects daemon-specific headers/cookies)
53
- /** @private */
54
- this._oauthClient = axios.create({
55
- timeout: 60000,
56
- validateStatus: () => true,
57
- });
58
- }
59
-
60
- /**
61
- * Build a Keycloak OpenID Connect endpoint URL for the configured realm.
62
- * @param {string} name - Endpoint name (e.g. "token", "auth/device", "logout", "userinfo")
63
- * @returns {string} Full endpoint URL
64
- * @private
65
- */
66
- _oauthEndpoint(name) {
67
- return `${this._oauth2.authServerUrl}/realms/${encodeURIComponent(this._oauth2.realm)}/protocol/openid-connect/${name}`;
68
- }
69
-
70
- /**
71
- * POST x-www-form-urlencoded params to a Keycloak endpoint, adding client credentials.
72
- * @param {string} endpoint - Endpoint name for _oauthEndpoint()
73
- * @param {Object} params - Form fields (null/undefined values are skipped)
74
- * @returns {Promise<any>} Raw axios response (any status)
75
- * @private
76
- */
77
- async _oauthPost(endpoint, params) {
78
- const form = new URLSearchParams();
79
- form.append("client_id", this._oauth2.clientId);
80
- if (this._oauth2.clientSecret)
81
- form.append("client_secret", this._oauth2.clientSecret);
82
- for (const [key, value] of Object.entries(params)) {
83
- // Values are appended as-is (strings): TOTP codes keep leading zeros
84
- if (value !== null && value !== undefined)
85
- form.append(key, String(value));
86
- }
87
- return this._oauthClient.post(this._oauthEndpoint(endpoint), form);
88
- }
89
-
90
- /**
91
- * Extract the OAuth2 "error" code from a Keycloak error response body.
92
- * @param {any} data - Response body
93
- * @returns {string} OAuth2 error code or ""
94
- * @private
95
- */
96
- static _oauthErrorCode(data) {
97
- if (typeof data === "string") {
98
- try {
99
- data = JSON.parse(data);
100
- } catch (_) {
101
- return "";
102
- }
103
- }
104
- return data && typeof data === "object" && typeof data.error === "string"
105
- ? data.error
106
- : "";
107
- }
108
-
109
- /**
110
- * Store a full Keycloak token response and propagate the access token to the base client.
111
- * @param {Object} tokenResponse - Complete Keycloak token endpoint response
112
- * @private
113
- */
114
- _storeOauthToken(tokenResponse) {
115
- this._oauthToken = tokenResponse || {};
116
- this._handleTokenUpdate(this._oauthToken.access_token || null);
117
- }
118
-
119
- /**
120
- * Login with username and password using the OAuth2 Direct Access Grant (Resource
121
- * Owner Password Credentials) against Keycloak.
122
- * @param {string} username - The name of the user
123
- * @param {string} password - The password of the user
124
- * @param {string} [totpCode] - TOTP code if enabled for the user. Passed verbatim as a
125
- * string: leading zeros matter (e.g. "012345")
126
- * @param {string|number} [tokenExpire] - Ignored by the Keycloak login flow
127
- * @param {string} [audience] - Ignored by the Keycloak login flow
128
- * @returns {Promise<void>} Resolves when login succeeds
129
- * @throws {AppMeshError} If authentication fails
130
- */
131
- async login(
132
- username,
133
- password,
134
- totpCode = null,
135
- tokenExpire = null,
136
- audience = null,
137
- ) {
138
- if (tokenExpire || audience) {
139
- console.warn(
140
- "tokenExpire and audience are ignored by the Keycloak login flow",
141
- );
142
- }
143
- if (!username || !password) {
144
- throw new AppMeshError(
145
- "Username and password are required",
146
- 400,
147
- null,
148
- "INVALID_CREDENTIALS",
149
- );
150
- }
151
-
152
- const response = await this._oauthPost("token", {
153
- grant_type: "password",
154
- username,
155
- password,
156
- scope: DEFAULT_OAUTH_SCOPE,
157
- // Pass TOTP as-is (string): a numeric conversion would strip leading zeros
158
- totp: totpCode ? String(totpCode) : null,
159
- });
160
- if (response.status !== 200) {
161
- throw new AppMeshError(
162
- `Keycloak login failed: ${this._extractErrorMessage(response.data)}`,
163
- response.status,
164
- response.data,
165
- "OAUTH_LOGIN_FAILED",
166
- );
167
- }
168
- this._storeOauthToken(response.data);
169
- }
170
-
171
- /**
172
- * Login via the OAuth 2.0 Device Authorization Grant (RFC 8628).
173
- *
174
- * For browserless/input-constrained environments: the user opens
175
- * `verification_uri_complete` (or `verification_uri` + `user_code`) on another device,
176
- * and this call polls the token endpoint until approval, denial, or expiry.
177
- *
178
- * Requires "OAuth 2.0 Device Authorization Grant" enabled on the Keycloak client.
179
- *
180
- * @param {Function} [onPrompt] - Callback receiving the device authorization response
181
- * (keys per RFC 8628 §3.2: `user_code`, `verification_uri`, `verification_uri_complete`,
182
- * `expires_in`, `interval`) to present the instructions to the user. Defaults to
183
- * printing them to the console.
184
- * @param {string} [scope] - OAuth2 scopes to request
185
- * @returns {Promise<void>} Resolves once the user approved the request
186
- * @throws {AppMeshError} When the user denies the request, the device code expires,
187
- * or the token request fails for any other reason
188
- */
189
- async login_device_flow(onPrompt = null, scope = DEFAULT_OAUTH_SCOPE) {
190
- const deviceResponse = await this._oauthPost("auth/device", { scope });
191
- if (deviceResponse.status !== 200) {
192
- throw new AppMeshError(
193
- `Device authorization request failed: ${this._extractErrorMessage(deviceResponse.data)}`,
194
- deviceResponse.status,
195
- deviceResponse.data,
196
- "OAUTH_DEVICE_FLOW_FAILED",
197
- );
198
- }
199
- const device = deviceResponse.data;
200
-
201
- if (onPrompt) {
202
- onPrompt(device);
203
- } else {
204
- const uri = device.verification_uri_complete || device.verification_uri;
205
- console.log(
206
- `To sign in, open ${uri} and enter code: ${device.user_code}`,
207
- );
208
- }
209
-
210
- // RFC 8628 §3.5: poll no faster than "interval", stop once the device code expires
211
- let interval = parseInt(device.interval, 10) || 5;
212
- const deadline =
213
- Date.now() + (parseInt(device.expires_in, 10) || 600) * 1000;
214
-
215
- for (;;) {
216
- const remaining = (deadline - Date.now()) / 1000;
217
- if (remaining <= 0) {
218
- throw new AppMeshError(
219
- "Device authorization expired before the user approved the request",
220
- null,
221
- null,
222
- "OAUTH_DEVICE_FLOW_EXPIRED",
223
- );
224
- }
225
- await this._sleep(Math.min(interval, remaining));
226
-
227
- const response = await this._oauthPost("token", {
228
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
229
- device_code: device.device_code,
230
- });
231
- if (response.status === 200) {
232
- this._storeOauthToken(response.data);
233
- return;
234
- }
235
- const error = AppMeshClientOAuth._oauthErrorCode(response.data);
236
- if (error === "authorization_pending") {
237
- continue;
238
- }
239
- if (error === "slow_down") {
240
- interval += 5; // RFC 8628 §3.5: back off by 5 seconds
241
- continue;
242
- }
243
- throw new AppMeshError(
244
- `Device authorization failed: ${error || this._extractErrorMessage(response.data)}`,
245
- response.status,
246
- response.data,
247
- "OAUTH_DEVICE_FLOW_FAILED",
248
- );
249
- }
250
- }
251
-
252
- /**
253
- * Sleep helper used by the device-flow polling loop.
254
- * @param {number} seconds - Duration in seconds
255
- * @returns {Promise<void>}
256
- * @protected
257
- */
258
- _sleep(seconds) {
259
- return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
260
- }
261
-
262
- /**
263
- * Renew the current Keycloak token using the refresh token.
264
- * Keycloak rotates the refresh token: the complete new token response is stored.
265
- * @param {string|number} [tokenExpire] - Ignored by the Keycloak refresh flow
266
- * @returns {Promise<void>}
267
- * @throws {AppMeshError} If no refresh token is available or the renewal fails
268
- */
269
- async renew_token(tokenExpire = null) {
270
- // eslint-disable-line no-unused-vars
271
- const refreshToken = this._oauthToken && this._oauthToken.refresh_token;
272
- if (!refreshToken) {
273
- throw new AppMeshError(
274
- "No Keycloak refresh token available to renew",
275
- null,
276
- null,
277
- "OAUTH_NO_REFRESH_TOKEN",
278
- );
279
- }
280
-
281
- const response = await this._oauthPost("token", {
282
- grant_type: "refresh_token",
283
- refresh_token: refreshToken,
284
- });
285
- if (response.status !== 200) {
286
- throw new AppMeshError(
287
- `Keycloak token renewal failed: ${this._extractErrorMessage(response.data)}`,
288
- response.status,
289
- response.data,
290
- "OAUTH_RENEW_FAILED",
291
- );
292
- }
293
- // Keycloak rotates refresh tokens: replace the whole stored token response
294
- this._storeOauthToken(response.data);
295
- }
296
-
297
- /**
298
- * Log out of the current session from Keycloak and the App Mesh daemon.
299
- * The daemon logoff (super.logout) runs BEFORE the local token is cleared so it can
300
- * still authenticate; the Keycloak session is revoked via the refresh token.
301
- * @returns {Promise<boolean>} true when the Keycloak session was revoked
302
- */
303
- async logout() {
304
- let result = false;
305
- const refreshToken = this._oauthToken && this._oauthToken.refresh_token;
306
- if (refreshToken) {
307
- try {
308
- const response = await this._oauthPost("logout", {
309
- refresh_token: refreshToken,
310
- });
311
- if (response.status >= 200 && response.status < 300) {
312
- result = true;
313
- } else {
314
- console.warn(
315
- `Failed to logout from Keycloak: ${this._extractErrorMessage(response.data)}`,
316
- );
317
- }
318
- } catch (error) {
319
- console.warn(`Failed to logout from Keycloak: ${error.message}`);
320
- }
321
- }
322
-
323
- // Daemon-side logoff before clearing the local token (it still needs the token)
324
- await super.logout();
325
- this._oauthToken = {};
326
-
327
- return result;
328
- }
329
-
330
- /**
331
- * Get Keycloak OIDC userinfo for the current access token, directly from Keycloak.
332
- * Unlike get_current_user() (inherited), which asks the App Mesh daemon
333
- * /appmesh/user/self, this queries the Keycloak userinfo endpoint without the daemon.
334
- * @returns {Promise<Object>} Keycloak userinfo (OIDC claims such as `sub`,
335
- * `preferred_username`, `email`)
336
- * @throws {AppMeshError} If no access token is available or the request fails
337
- */
338
- async get_oauth_userinfo() {
339
- const accessToken = this._oauthToken && this._oauthToken.access_token;
340
- if (!accessToken) {
341
- throw new AppMeshError(
342
- "No Keycloak access token available",
343
- null,
344
- null,
345
- "OAUTH_NO_ACCESS_TOKEN",
346
- );
347
- }
348
- const response = await this._oauthClient.get(
349
- this._oauthEndpoint("userinfo"),
350
- {
351
- headers: { Authorization: `Bearer ${accessToken}` },
352
- },
353
- );
354
- if (response.status !== 200) {
355
- throw new AppMeshError(
356
- `Keycloak userinfo request failed: ${this._extractErrorMessage(response.data)}`,
357
- response.status,
358
- response.data,
359
- "OAUTH_USERINFO_FAILED",
360
- );
361
- }
362
- return response.data;
363
- }
364
- }
365
-
366
- export { AppMeshClientOAuth };
367
- export default AppMeshClientOAuth;