appmesh 1.3.1 → 1.3.3

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.
@@ -0,0 +1,367 @@
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;
@@ -31,14 +31,7 @@ const LOGOFF_PATH = '/appmesh/self/logoff'
31
31
  /**
32
32
  * TCP transport for secure TLS connections and framed App Mesh messages.
33
33
  *
34
- * `sslConfig` is a tri-state:
35
- * - `null`/`undefined` (default): verify the server certificate against the App Mesh default
36
- * CA (/opt/appmesh/ssl/ca.pem) if installed, otherwise the system CAs.
37
- * - object: custom TLS material — `ca`/`cert`/`key` (Buffer or file path) with an optional
38
- * `rejectUnauthorized` flag (defaults to `true`). An unreadable CA/cert/key path is a hard
39
- * error (`fs.readFileSync` throws), never a silent fallback to no-verification.
40
- * - `false`: explicit insecure mode — server certificate verification is disabled.
41
- * This is the only way to disable verification besides `rejectUnauthorized: false`.
34
+ * @param {Object|false|null} [sslConfig=null] - TLS config; same tri-state as AppMeshClient (null=default CA, object=custom ca/cert/key, false=insecure).
42
35
  */
43
36
  class TCPTransport {
44
37
  /**
@@ -250,13 +243,15 @@ class RequestMessage {
250
243
  * Serialize message to msgpack format
251
244
  */
252
245
  serialize () {
246
+ // Coerce header/query values to strings; the daemon expects map<string,string>.
247
+ const strMap = (o) => Object.fromEntries(Object.entries(o || {}).map(([k, v]) => [k, String(v)]))
253
248
  const data = {
254
249
  uuid: this.uuid,
255
250
  http_method: this.httpMethod,
256
251
  request_uri: this.requestUri,
257
252
  client_addr: this.clientAddr,
258
- headers: this.headers,
259
- query: this.query,
253
+ headers: strMap(this.headers),
254
+ query: strMap(this.query),
260
255
  body: this.body
261
256
  }
262
257
  return msgpack.encode(data)
@@ -331,16 +326,7 @@ class AppMeshClientTCP extends AppMeshClient {
331
326
  * `new AppMeshClientTCP(sslConfig, {host, port})` — the address may be an explicit
332
327
  * `{host, port}` object in either position.
333
328
  *
334
- * `sslConfig` is a tri-state: `null`/`undefined` verifies the server certificate against the
335
- * App Mesh default CA (/opt/appmesh/ssl/ca.pem) if installed, otherwise the system CAs; an
336
- * object supplies custom TLS material (an unreadable ca/cert/key path is a hard error, never
337
- * a silent no-verify fallback); `false` is the explicit insecure no-verification mode.
338
- *
339
- * @param {Object|false|null} [sslConfig=null] - SSL configuration (or `{host, port}` address)
340
- * @param {Buffer|string} [sslConfig.ca] - CA certificate
341
- * @param {Buffer|string} [sslConfig.cert] - Client certificate
342
- * @param {Buffer|string} [sslConfig.key] - Client key
343
- * @param {boolean} [sslConfig.rejectUnauthorized=true] - Whether to verify SSL
329
+ * @param {Object|false|null} [sslConfig=null] - TLS config; same tri-state as AppMeshClient (null=default CA, object=custom ca/cert/key, false=insecure).
344
330
  * @param {Array<string, number>|{host: string, port: number}} [tcpAddress=['127.0.0.1', 6059]] - TCP server address
345
331
  */
346
332
  constructor (sslConfig = null, tcpAddress = ['127.0.0.1', 6059]) {
@@ -594,12 +580,13 @@ class AppMeshClientTCP extends AppMeshClient {
594
580
  * Download a file through the TCP file-socket side channel.
595
581
  *
596
582
  * @param {string} filePath - Remote file path
597
- * @param {string} localFile - Local destination path
583
+ * @param {string} [localFile=null] - Local destination path; defaults to the basename of filePath
598
584
  * @param {boolean} [applyAttrs=true] - Apply returned mode and best-effort owner/group metadata
599
585
  * on non-Windows Node.js hosts
600
586
  * @override
601
587
  */
602
- async download_file (filePath, localFile, applyAttrs = true) {
588
+ async download_file (filePath, localFile = null, applyAttrs = true) {
589
+ if (!localFile) localFile = filePath.split(/[\\/]/).pop()
603
590
  const headers = {
604
591
  [HTTP_HEADER_KEY_X_FILE_PATH]: encodeURIComponent(filePath),
605
592
  [HTTP_HEADER_KEY_X_RECV_FILE_SOCKET]: 'true'
@@ -631,7 +618,12 @@ class AppMeshClientTCP extends AppMeshClient {
631
618
  writeStream.write(chunk)
632
619
  }
633
620
  } finally {
634
- writeStream.end()
621
+ // Await the flush so the file is complete on disk before this resolves.
622
+ await new Promise((resolve, reject) => {
623
+ writeStream.once('finish', resolve)
624
+ writeStream.once('error', reject)
625
+ writeStream.end()
626
+ })
635
627
  }
636
628
 
637
629
  // Apply file attributes on Unix systems
@@ -660,12 +652,13 @@ class AppMeshClientTCP extends AppMeshClient {
660
652
  * Upload a file through the TCP file-socket side channel.
661
653
  *
662
654
  * @param {string} localFile - Local file path
663
- * @param {string} filePath - Remote destination path
655
+ * @param {string} [filePath=null] - Remote destination path; defaults to the basename of localFile
664
656
  * @param {boolean} [applyAttrs=true] - Send local mode/uid/gid metadata so the server can
665
657
  * recreate permissions and ownership when supported
666
658
  * @override
667
659
  */
668
- async upload_file (localFile, filePath, applyAttrs = true) {
660
+ async upload_file (localFile, filePath = null, applyAttrs = true) {
661
+ if (!filePath) filePath = localFile.split(/[\\/]/).pop()
669
662
  if (!fs.existsSync(localFile)) {
670
663
  throw new Error(`Local file not found: ${localFile}`)
671
664
  }
@@ -791,22 +784,13 @@ class AppMeshClientTCP extends AppMeshClient {
791
784
  }
792
785
 
793
786
  /**
794
- * Subscribe-based wait for async run (TCP override).
795
- *
796
- * Instead of polling get_app_output in a loop, subscribes to STDOUT/EXIT/REMOVED
797
- * events and does a one-shot backfill to cover output emitted before the subscribe
798
- * took effect. Deduplicates by byte-position offset.
799
- *
800
- * No sentinel exit codes (real exit codes may be negative for signal kills):
801
- * non-EXIT terminations throw typed errors; null means caller-side timeout only.
802
- *
787
+ * Wait for an async run to finish over TCP, streaming incremental output.
803
788
  * @param {AppRun} run - AppRun object
804
789
  * @param {Function} [stdoutHandler] - Stdout handler callback(data, position)
805
790
  * @param {number} [timeout=0] - Max wait time in seconds (0 = unlimited)
806
791
  * @returns {Promise<number|null>} Exit code, or null on timeout
807
792
  * @throws {AppRemovedError} If the app was removed before its exit was observed.
808
- * @throws {TransportDisconnectedError} If the demuxer disconnected while waiting,
809
- * or the daemon delivered an unparseable exit code.
793
+ * @throws {TransportDisconnectedError} If the transport disconnected while waiting, or the daemon delivered an unparseable exit code.
810
794
  */
811
795
  async wait_for_async_run (run, stdoutHandler, timeout = 0) {
812
796
  if (!run || !run.appName) return null
@@ -1,4 +1,4 @@
1
- // server.js - App Mesh Worker SDK for Node.js
1
+ // worker.js - App Mesh Worker SDK for Node.js
2
2
  // This is a Node.js-only module - not included in browser builds
3
3
 
4
4
  // Environment check - only works in Node.js
@@ -57,7 +57,7 @@ const _DEFER_CLIENT = Symbol('appmesh.deferClient')
57
57
  *
58
58
  * @example
59
59
  * // HTTP Worker Example
60
- * import { AppMeshWorker } from 'appmesh/server';
60
+ * import { AppMeshWorker } from 'appmesh/worker';
61
61
  *
62
62
  * const worker = new AppMeshWorker();
63
63
  * const payload = await worker.fetch_task();
@@ -66,7 +66,7 @@ const _DEFER_CLIENT = Symbol('appmesh.deferClient')
66
66
  *
67
67
  * @example
68
68
  * // TCP Worker Example
69
- * import { AppMeshWorkerTCP } from 'appmesh/server';
69
+ * import { AppMeshWorkerTCP } from 'appmesh/worker';
70
70
  *
71
71
  * const worker = new AppMeshWorkerTCP();
72
72
  * try {
@@ -176,11 +176,6 @@ class AppMeshWorker {
176
176
  * @throws {ProcessSupersededError} The daemon reported HTTP 412 — this process key was
177
177
  * superseded by a newer process instance; the caller should stop serving (an app
178
178
  * entry point typically catches this and exits).
179
- *
180
- * @example
181
- * const worker = new AppMeshWorker();
182
- * const payload = await worker.fetch_task();
183
- * console.log('Received payload:', payload);
184
179
  */
185
180
  async fetch_task () {
186
181
  const { processKey, appName } = this._getRuntimeEnv()
@@ -244,11 +239,6 @@ class AppMeshWorker {
244
239
  * @param {string|Buffer} result - Result payload to be delivered back to the client as-is
245
240
  * @returns {Promise<void>}
246
241
  * @throws {Error} If the task return fails
247
- *
248
- * @example
249
- * const worker = new AppMeshWorker();
250
- * const result = { status: 'success', data: processedData };
251
- * await worker.send_task_result(JSON.stringify(result));
252
242
  */
253
243
  async send_task_result (result) {
254
244
  const { processKey, appName } = this._getRuntimeEnv()
@@ -292,7 +282,7 @@ class AppMeshWorker {
292
282
  * @extends AppMeshWorker
293
283
  *
294
284
  * @example
295
- * import { AppMeshWorkerTCP } from 'appmesh/server';
285
+ * import { AppMeshWorkerTCP } from 'appmesh/worker';
296
286
  *
297
287
  * const worker = new AppMeshWorkerTCP();
298
288
  * try {
@@ -319,12 +309,7 @@ class AppMeshWorkerTCP extends AppMeshWorker {
319
309
  * @param {Array<string, number>|{host: string, port: number}} [tcpAddress=['127.0.0.1', 6059]] - TCP server address
320
310
  * @param {Object} [options={}] - Additional options
321
311
  * @param {Object} [options.logger=console] - Logger instance
322
- * @param {AppMeshClientTCP} [options.client] - Optional pre-built `AppMeshClientTCP` instance to reuse.
323
- * When supplied, `sslConfig` and `tcpAddress` are ignored and the worker shares the caller's
324
- * client (and therefore its token-refresh state). Use this when a single process needs both
325
- * client and worker roles to avoid two independent `/token/renew` loops fighting each other —
326
- * the daemon blacklists the previous token on every renew, so the slower refresher gets
327
- * 401 "Token has been revoked".
312
+ * @param {AppMeshClientTCP} [options.client] - Pre-built client to reuse (shares token-refresh state); see AppMeshWorker for why. When set, sslConfig/tcpAddress are ignored.
328
313
  *
329
314
  * @example
330
315
  * import fs from 'fs';
@@ -392,14 +377,6 @@ class AppMeshWorkerTCP extends AppMeshWorker {
392
377
  /**
393
378
  * Close the TCP connection and release resources.
394
379
  * Should be called when done using the server.
395
- *
396
- * @example
397
- * const worker = new AppMeshWorkerTCP();
398
- * try {
399
- * // Use worker...
400
- * } finally {
401
- * worker.close();
402
- * }
403
380
  */
404
381
  close () {
405
382
  if (this._client && typeof this._client.close === 'function') {
@@ -417,7 +394,7 @@ class AppMeshWorkerTCP extends AppMeshWorker {
417
394
  * @returns {Promise<any>} Result from callback
418
395
  *
419
396
  * @example
420
- * import { withServer, AppMeshWorkerTCP } from 'appmesh/server';
397
+ * import { withServer, AppMeshWorkerTCP } from 'appmesh/worker';
421
398
  *
422
399
  * await withServer(
423
400
  * () => new AppMeshWorkerTCP(),