@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.
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +16 -6
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +67 -9
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +16 -6
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +67 -9
- package/package.json +1 -1
|
@@ -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;
|
|
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"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -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
|
-
*
|
|
1344
|
-
*
|
|
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`, `
|
|
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)
|
package/dist/cjs/serve.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
206
|
-
*
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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:
|
|
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;
|
|
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"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -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
|
-
*
|
|
1339
|
-
*
|
|
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`, `
|
|
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)
|
package/dist/esm/serve.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
167
|
-
*
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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:
|
|
305
|
+
code: unavailable
|
|
306
|
+
? grpc.status.UNAVAILABLE
|
|
307
|
+
: grpc.status.UNAUTHENTICATED,
|
|
250
308
|
details: message,
|
|
251
309
|
});
|
|
252
310
|
return;
|