@stackfactor/agent-utils 1.2.8 → 1.2.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBA+jB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAusBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA3+B8B,GAAG,KAAG,MAAM;+CApjB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAokBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAkhCT,wBASE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBA+jB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAktBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CAt/B8B,GAAG,KAAG,MAAM;+CApjB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAokBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA6hCT,wBASE"}
@@ -1339,20 +1339,21 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options, usage
1339
1339
  * Google models do not support alpha channels. Validates the `aspectRatio` against
1340
1340
  * allowed values (`"1:1"`, `"3:4"`, `"4:3"`, `"9:16"`, `"16:9"`). Applies model-
1341
1341
  * specific generation config: Imagen models receive `numberOfImages`, `aspectRatio`,
1342
- * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`
1343
- * and `topP`. Safety settings are disabled for image generation. Throws an error if no
1344
- * images are returned.
1342
+ * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`,
1343
+ * `topP`, and an `imageConfig` carrying `aspectRatio` plus an optional `imageSize`
1344
+ * resolution tier (e.g. "1K"/"2K"/"4K", honoured by gemini-3-pro-image). Safety
1345
+ * settings are disabled for image generation. Throws an error if no images are returned.
1345
1346
  * @param modelName - The Google model identifier (e.g. `"imagen-4.0-generate-001"`,
1346
1347
  * `"gemini-3.0-pro-image"`)
1347
1348
  * @param config - Configuration object; must include `googleAPIKey`
1348
1349
  * @param prompt - The text prompt describing the image to generate
1349
- * @param options - Generation options including `aspectRatio`, `numberOfImages`, and
1350
- * optional `negativePrompt` (Imagen only)
1350
+ * @param options - Generation options including `aspectRatio`, optional `imageSize`
1351
+ * (Gemini resolution tier), `numberOfImages`, and optional `negativePrompt` (Imagen only)
1351
1352
  * @returns A single image descriptor `{ b64_json, mimeType }` when one image is
1352
1353
  * requested, or `{ images: [...] }` for multiple images
1353
1354
  */
1354
1355
  const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
1355
- const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
1356
+ const { aspectRatio = "1:1", imageSize, numberOfImages = 1, negativePrompt = "", } = options;
1356
1357
  if (!config.googleAPIKey) {
1357
1358
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
1358
1359
  }
@@ -1389,6 +1390,15 @@ const generateImageWithGoogle = async (modelName, config, prompt, options, usage
1389
1390
  // Gemini-specific config
1390
1391
  temperature: 1,
1391
1392
  topP: 0.95,
1393
+ // Gemini controls output dimensions through `imageConfig`, not the
1394
+ // top-level `aspectRatio` field that Imagen uses. `imageSize`
1395
+ // (e.g. "1K"/"2K"/"4K") is only honoured by resolution-capable
1396
+ // models such as gemini-3-pro-image, so it is passed through only
1397
+ // when the caller explicitly requests it.
1398
+ imageConfig: {
1399
+ aspectRatio: aspectRatio,
1400
+ ...(imageSize ? { imageSize: imageSize } : {}),
1401
+ },
1392
1402
  }),
1393
1403
  };
1394
1404
  // Safety settings (disable for image generation)
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAuKP,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA4KP,CAAC"}
package/dist/cjs/serve.js CHANGED
@@ -199,23 +199,78 @@ const runSelfCheck = async (checkCode, config, session) => {
199
199
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
200
200
  class UnauthenticatedError extends Error {
201
201
  }
202
+ /**
203
+ * Marks a *transient* failure to reach the StackFactor session-validation
204
+ * endpoint (5xx / request timeout / transport error). Unlike
205
+ * {@link UnauthenticatedError} the token was never actually rejected — the
206
+ * backend was momentarily unreachable — so the handler maps this to the
207
+ * retryable gRPC UNAVAILABLE instead of UNAUTHENTICATED, and the message no
208
+ * longer slanders a perfectly valid token.
209
+ */
210
+ class BackendUnavailableError extends Error {
211
+ }
212
+ /**
213
+ * Session-validation retry policy. A GKE front-door blip (a backend pod briefly
214
+ * refusing connections during a restart or an event-loop stall) surfaces here as
215
+ * a 502/503 or a transport error, not a 401 — and retrying on a fresh connection
216
+ * almost always lands on a healthy replica. Env-overridable for prod tuning.
217
+ */
218
+ const AUTH_MAX_ATTEMPTS = Math.max(1, Number(process.env.STACKFACTOR_AUTH_MAX_ATTEMPTS) || 4);
219
+ const AUTH_RETRY_BASE_MS = Math.max(0, Number(process.env.STACKFACTOR_AUTH_RETRY_BASE_MS) || 500);
220
+ const delay = (ms) => new Promise((res) => setTimeout(res, ms));
221
+ /** A 401/403 is the backend genuinely rejecting the token. */
222
+ const isAuthRejection = (error) => {
223
+ const status = error?.response?.status;
224
+ return status === 401 || status === 403;
225
+ };
226
+ /**
227
+ * True when the failure is the validation endpoint being unreachable rather than
228
+ * the token being bad: a 5xx/408/429 HTTP response, or a transport-level error
229
+ * (no HTTP response but an axios/errno signature). Plain programming errors
230
+ * (no response and no axios/errno marker) are NOT treated as transient.
231
+ */
232
+ const isTransient = (error) => {
233
+ const status = error?.response?.status;
234
+ if (status)
235
+ return status >= 500 || status === 408 || status === 429;
236
+ return Boolean(error?.isAxiosError || error?.code);
237
+ };
202
238
  /**
203
239
  * Default authenticator: require a token on the request and resolve it to a
204
240
  * session via the StackFactor API. Requires `BACKEND_URL` (or `REACT_APP_NODE_ENV`)
205
- * to point client-api at the right backend. Throws {@link UnauthenticatedError}
206
- * when the token is missing or rejected.
241
+ * to point client-api at the right backend.
242
+ *
243
+ * A 401/403 (or any non-transient error) throws {@link UnauthenticatedError}
244
+ * immediately. A transient failure (5xx / timeout / network) is retried with
245
+ * exponential backoff; if every attempt fails it throws
246
+ * {@link BackendUnavailableError} so the caller sees a retryable UNAVAILABLE
247
+ * rather than a misleading "invalid token".
207
248
  */
208
249
  const defaultAuthenticate = async (request) => {
209
250
  const token = request?.authToken ?? request?.authorization;
210
251
  if (!token) {
211
252
  throw new UnauthenticatedError("Missing StackFactor auth token");
212
253
  }
213
- try {
214
- return await client_api_1.session.getSession(token);
215
- }
216
- catch (error) {
217
- throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
254
+ let lastError;
255
+ for (let attempt = 1; attempt <= AUTH_MAX_ATTEMPTS; attempt++) {
256
+ try {
257
+ return await client_api_1.session.getSession(token);
258
+ }
259
+ catch (error) {
260
+ lastError = error;
261
+ // Real rejection, or an unexpected non-transient error: fail fast.
262
+ if (isAuthRejection(error) || !isTransient(error)) {
263
+ throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
264
+ }
265
+ // Transient: the endpoint is momentarily unreachable, not the token bad.
266
+ if (attempt < AUTH_MAX_ATTEMPTS) {
267
+ const backoff = AUTH_RETRY_BASE_MS * 2 ** (attempt - 1);
268
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `session validation transient failure (attempt ${attempt}/${AUTH_MAX_ATTEMPTS}); retrying in ${backoff}ms: ${error?.message ?? error}`);
269
+ await delay(backoff);
270
+ }
271
+ }
218
272
  }
273
+ throw new BackendUnavailableError(`StackFactor session validation unavailable after ${AUTH_MAX_ATTEMPTS} attempts: ${lastError?.message ?? lastError}`);
219
274
  };
220
275
  const safeParse = (value, fallback) => {
221
276
  if (!value)
@@ -283,9 +338,12 @@ const serve = (main, options = {}) => {
283
338
  }
284
339
  catch (error) {
285
340
  const message = error?.message ?? "Unauthenticated";
286
- logger_js_1.default.log(null, logger_js_1.default.levels.warn, `agent Execute refused: ${message}`);
341
+ const unavailable = error instanceof BackendUnavailableError;
342
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `agent Execute refused (${unavailable ? "unavailable" : "unauthenticated"}): ${message}`);
287
343
  call.emit("error", {
288
- code: grpc.status.UNAUTHENTICATED,
344
+ code: unavailable
345
+ ? grpc.status.UNAVAILABLE
346
+ : grpc.status.UNAUTHENTICATED,
289
347
  details: message,
290
348
  });
291
349
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBA+jB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAusBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA3+B8B,GAAG,KAAG,MAAM;+CApjB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAokBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAkhCT,wBASE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBA+jB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAktBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CAt/B8B,GAAG,KAAG,MAAM;+CApjB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAokBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA6hCT,wBASE"}
@@ -1334,20 +1334,21 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options, usage
1334
1334
  * Google models do not support alpha channels. Validates the `aspectRatio` against
1335
1335
  * allowed values (`"1:1"`, `"3:4"`, `"4:3"`, `"9:16"`, `"16:9"`). Applies model-
1336
1336
  * specific generation config: Imagen models receive `numberOfImages`, `aspectRatio`,
1337
- * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`
1338
- * and `topP`. Safety settings are disabled for image generation. Throws an error if no
1339
- * images are returned.
1337
+ * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`,
1338
+ * `topP`, and an `imageConfig` carrying `aspectRatio` plus an optional `imageSize`
1339
+ * resolution tier (e.g. "1K"/"2K"/"4K", honoured by gemini-3-pro-image). Safety
1340
+ * settings are disabled for image generation. Throws an error if no images are returned.
1340
1341
  * @param modelName - The Google model identifier (e.g. `"imagen-4.0-generate-001"`,
1341
1342
  * `"gemini-3.0-pro-image"`)
1342
1343
  * @param config - Configuration object; must include `googleAPIKey`
1343
1344
  * @param prompt - The text prompt describing the image to generate
1344
- * @param options - Generation options including `aspectRatio`, `numberOfImages`, and
1345
- * optional `negativePrompt` (Imagen only)
1345
+ * @param options - Generation options including `aspectRatio`, optional `imageSize`
1346
+ * (Gemini resolution tier), `numberOfImages`, and optional `negativePrompt` (Imagen only)
1346
1347
  * @returns A single image descriptor `{ b64_json, mimeType }` when one image is
1347
1348
  * requested, or `{ images: [...] }` for multiple images
1348
1349
  */
1349
1350
  const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
1350
- const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
1351
+ const { aspectRatio = "1:1", imageSize, numberOfImages = 1, negativePrompt = "", } = options;
1351
1352
  if (!config.googleAPIKey) {
1352
1353
  throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
1353
1354
  }
@@ -1384,6 +1385,15 @@ const generateImageWithGoogle = async (modelName, config, prompt, options, usage
1384
1385
  // Gemini-specific config
1385
1386
  temperature: 1,
1386
1387
  topP: 0.95,
1388
+ // Gemini controls output dimensions through `imageConfig`, not the
1389
+ // top-level `aspectRatio` field that Imagen uses. `imageSize`
1390
+ // (e.g. "1K"/"2K"/"4K") is only honoured by resolution-capable
1391
+ // models such as gemini-3-pro-image, so it is passed through only
1392
+ // when the caller explicitly requests it.
1393
+ imageConfig: {
1394
+ aspectRatio: aspectRatio,
1395
+ ...(imageSize ? { imageSize: imageSize } : {}),
1396
+ },
1387
1397
  }),
1388
1398
  };
1389
1399
  // Safety settings (disable for image generation)
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAuKP,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA4KP,CAAC"}
package/dist/esm/serve.js CHANGED
@@ -160,23 +160,78 @@ const runSelfCheck = async (checkCode, config, session) => {
160
160
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
161
161
  class UnauthenticatedError extends Error {
162
162
  }
163
+ /**
164
+ * Marks a *transient* failure to reach the StackFactor session-validation
165
+ * endpoint (5xx / request timeout / transport error). Unlike
166
+ * {@link UnauthenticatedError} the token was never actually rejected — the
167
+ * backend was momentarily unreachable — so the handler maps this to the
168
+ * retryable gRPC UNAVAILABLE instead of UNAUTHENTICATED, and the message no
169
+ * longer slanders a perfectly valid token.
170
+ */
171
+ class BackendUnavailableError extends Error {
172
+ }
173
+ /**
174
+ * Session-validation retry policy. A GKE front-door blip (a backend pod briefly
175
+ * refusing connections during a restart or an event-loop stall) surfaces here as
176
+ * a 502/503 or a transport error, not a 401 — and retrying on a fresh connection
177
+ * almost always lands on a healthy replica. Env-overridable for prod tuning.
178
+ */
179
+ const AUTH_MAX_ATTEMPTS = Math.max(1, Number(process.env.STACKFACTOR_AUTH_MAX_ATTEMPTS) || 4);
180
+ const AUTH_RETRY_BASE_MS = Math.max(0, Number(process.env.STACKFACTOR_AUTH_RETRY_BASE_MS) || 500);
181
+ const delay = (ms) => new Promise((res) => setTimeout(res, ms));
182
+ /** A 401/403 is the backend genuinely rejecting the token. */
183
+ const isAuthRejection = (error) => {
184
+ const status = error?.response?.status;
185
+ return status === 401 || status === 403;
186
+ };
187
+ /**
188
+ * True when the failure is the validation endpoint being unreachable rather than
189
+ * the token being bad: a 5xx/408/429 HTTP response, or a transport-level error
190
+ * (no HTTP response but an axios/errno signature). Plain programming errors
191
+ * (no response and no axios/errno marker) are NOT treated as transient.
192
+ */
193
+ const isTransient = (error) => {
194
+ const status = error?.response?.status;
195
+ if (status)
196
+ return status >= 500 || status === 408 || status === 429;
197
+ return Boolean(error?.isAxiosError || error?.code);
198
+ };
163
199
  /**
164
200
  * Default authenticator: require a token on the request and resolve it to a
165
201
  * session via the StackFactor API. Requires `BACKEND_URL` (or `REACT_APP_NODE_ENV`)
166
- * to point client-api at the right backend. Throws {@link UnauthenticatedError}
167
- * when the token is missing or rejected.
202
+ * to point client-api at the right backend.
203
+ *
204
+ * A 401/403 (or any non-transient error) throws {@link UnauthenticatedError}
205
+ * immediately. A transient failure (5xx / timeout / network) is retried with
206
+ * exponential backoff; if every attempt fails it throws
207
+ * {@link BackendUnavailableError} so the caller sees a retryable UNAVAILABLE
208
+ * rather than a misleading "invalid token".
168
209
  */
169
210
  const defaultAuthenticate = async (request) => {
170
211
  const token = request?.authToken ?? request?.authorization;
171
212
  if (!token) {
172
213
  throw new UnauthenticatedError("Missing StackFactor auth token");
173
214
  }
174
- try {
175
- return await clientSession.getSession(token);
176
- }
177
- catch (error) {
178
- throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
215
+ let lastError;
216
+ for (let attempt = 1; attempt <= AUTH_MAX_ATTEMPTS; attempt++) {
217
+ try {
218
+ return await clientSession.getSession(token);
219
+ }
220
+ catch (error) {
221
+ lastError = error;
222
+ // Real rejection, or an unexpected non-transient error: fail fast.
223
+ if (isAuthRejection(error) || !isTransient(error)) {
224
+ throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
225
+ }
226
+ // Transient: the endpoint is momentarily unreachable, not the token bad.
227
+ if (attempt < AUTH_MAX_ATTEMPTS) {
228
+ const backoff = AUTH_RETRY_BASE_MS * 2 ** (attempt - 1);
229
+ logger.log(null, logger.levels.warn, `session validation transient failure (attempt ${attempt}/${AUTH_MAX_ATTEMPTS}); retrying in ${backoff}ms: ${error?.message ?? error}`);
230
+ await delay(backoff);
231
+ }
232
+ }
179
233
  }
234
+ throw new BackendUnavailableError(`StackFactor session validation unavailable after ${AUTH_MAX_ATTEMPTS} attempts: ${lastError?.message ?? lastError}`);
180
235
  };
181
236
  const safeParse = (value, fallback) => {
182
237
  if (!value)
@@ -244,9 +299,12 @@ export const serve = (main, options = {}) => {
244
299
  }
245
300
  catch (error) {
246
301
  const message = error?.message ?? "Unauthenticated";
247
- logger.log(null, logger.levels.warn, `agent Execute refused: ${message}`);
302
+ const unavailable = error instanceof BackendUnavailableError;
303
+ logger.log(null, logger.levels.warn, `agent Execute refused (${unavailable ? "unavailable" : "unauthenticated"}): ${message}`);
248
304
  call.emit("error", {
249
- code: grpc.status.UNAUTHENTICATED,
305
+ code: unavailable
306
+ ? grpc.status.UNAVAILABLE
307
+ : grpc.status.UNAUTHENTICATED,
250
308
  details: message,
251
309
  });
252
310
  return;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.8",
6
+ "version": "1.2.10",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",