@kb-labs/rest-api-app 2.81.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import fastifyCors from '@fastify/cors';
27
27
  import fastifyRateLimit from '@fastify/rate-limit';
28
28
  import { ulid } from 'ulid';
29
29
  import { randomUUID, createHash } from 'crypto';
30
+ import { TenantRateLimiter, getDefaultTenantTier } from '@kb-labs/core-tenant';
30
31
  import { promises, existsSync, readFileSync } from 'fs';
31
32
  import { findRepoRoot } from '@kb-labs/core-sys';
32
33
 
@@ -2904,7 +2905,7 @@ async function registerCacheRoutes(server, config, registry) {
2904
2905
  }
2905
2906
  });
2906
2907
  }
2907
- async function registerObservabilityRoutes(fastify, config, repoRoot, historicalMetrics, platform16) {
2908
+ async function registerObservabilityRoutes(fastify, config, repoRoot, historicalMetrics, platform17) {
2908
2909
  const basePath = normalizeBasePath(config.basePath);
2909
2910
  const stateBrokerPaths = resolvePaths(basePath, "/observability/state-broker");
2910
2911
  const systemMetricsPaths = resolvePaths(basePath, "/observability/system-metrics");
@@ -2951,7 +2952,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
2951
2952
  }
2952
2953
  };
2953
2954
  } catch (error) {
2954
- platform16?.logger.error("Failed to fetch State Broker stats", error instanceof Error ? error : new Error(String(error)));
2955
+ platform17?.logger.error("Failed to fetch State Broker stats", error instanceof Error ? error : new Error(String(error)));
2955
2956
  const isTimeout = error instanceof Error && error.name === "AbortError";
2956
2957
  return reply.code(503).send({
2957
2958
  ok: false,
@@ -2969,7 +2970,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
2969
2970
  for (const path3 of systemMetricsPaths) {
2970
2971
  fastify.get(path3, { schema: { tags: ["Observability"], summary: "System resource metrics" } }, async (_request, reply) => {
2971
2972
  try {
2972
- if (!platform16?.cache) {
2973
+ if (!platform17?.cache) {
2973
2974
  return reply.code(503).send({
2974
2975
  ok: false,
2975
2976
  error: {
@@ -2981,17 +2982,17 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
2981
2982
  fastify.log.debug("Fetching system metrics from all instances");
2982
2983
  const allMetrics = [];
2983
2984
  try {
2984
- if ("scan" in platform16.cache && typeof platform16.cache.scan === "function") {
2985
- const keys = await platform16.cache.scan("system-metrics:*");
2985
+ if ("scan" in platform17.cache && typeof platform17.cache.scan === "function") {
2986
+ const keys = await platform17.cache.scan("system-metrics:*");
2986
2987
  for (const key of keys) {
2987
- const metrics = await platform16.cache.get(key);
2988
+ const metrics = await platform17.cache.get(key);
2988
2989
  if (metrics) {
2989
2990
  allMetrics.push(metrics);
2990
2991
  }
2991
2992
  }
2992
2993
  } else {
2993
2994
  const currentInstanceId = hostname();
2994
- const metrics = await platform16.cache.get(`system-metrics:${currentInstanceId}`);
2995
+ const metrics = await platform17.cache.get(`system-metrics:${currentInstanceId}`);
2995
2996
  if (metrics) {
2996
2997
  allMetrics.push(metrics);
2997
2998
  }
@@ -3000,7 +3001,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
3000
3001
  } catch (scanError) {
3001
3002
  fastify.log.warn({ err: scanError }, "Failed to scan platform.cache for system metrics");
3002
3003
  const currentInstanceId = hostname();
3003
- const metrics = await platform16.cache.get(`system-metrics:${currentInstanceId}`);
3004
+ const metrics = await platform17.cache.get(`system-metrics:${currentInstanceId}`);
3004
3005
  if (metrics) {
3005
3006
  allMetrics.push(metrics);
3006
3007
  }
@@ -3048,7 +3049,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
3048
3049
  }
3049
3050
  };
3050
3051
  } catch (error) {
3051
- platform16?.logger.error("Failed to fetch system metrics", error instanceof Error ? error : new Error(String(error)));
3052
+ platform17?.logger.error("Failed to fetch system metrics", error instanceof Error ? error : new Error(String(error)));
3052
3053
  return reply.code(500).send({
3053
3054
  ok: false,
3054
3055
  error: {
@@ -3146,7 +3147,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
3146
3147
  }
3147
3148
  };
3148
3149
  } catch (error) {
3149
- platform16?.logger.error("Failed to query historical metrics", error instanceof Error ? error : new Error(String(error)), { query });
3150
+ platform17?.logger.error("Failed to query historical metrics", error instanceof Error ? error : new Error(String(error)), { query });
3150
3151
  return reply.code(500).send({
3151
3152
  ok: false,
3152
3153
  error: {
@@ -3237,7 +3238,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
3237
3238
  }
3238
3239
  };
3239
3240
  } catch (error) {
3240
- platform16?.logger.error("Failed to query heatmap data", error instanceof Error ? error : new Error(String(error)), { query });
3241
+ platform17?.logger.error("Failed to query heatmap data", error instanceof Error ? error : new Error(String(error)), { query });
3241
3242
  return reply.code(500).send({
3242
3243
  ok: false,
3243
3244
  error: {
@@ -3271,7 +3272,7 @@ async function registerObservabilityRoutes(fastify, config, repoRoot, historical
3271
3272
  }
3272
3273
  }
3273
3274
  }, async (request, reply) => {
3274
- if (!platform16?.llm) {
3275
+ if (!platform17?.llm) {
3275
3276
  return reply.code(503).send({
3276
3277
  ok: false,
3277
3278
  error: {
@@ -3337,15 +3338,15 @@ Provide a clear, actionable response based on the data above. Include:
3337
3338
 
3338
3339
  Be concise but thorough. Use markdown formatting.`;
3339
3340
  fastify.log.debug({ question: body.question, contextLength: contextText.length }, "Calling LLM for insights");
3340
- const result = await platform16.llm.complete(prompt, {
3341
+ const result = await platform17.llm.complete(prompt, {
3341
3342
  systemPrompt: "You are a DevOps and SRE expert assistant. Analyze system metrics and provide actionable insights. Be concise, technical, and helpful.",
3342
3343
  temperature: 0.7,
3343
3344
  maxTokens: 1e3
3344
3345
  });
3345
3346
  const totalTokens = result.usage.promptTokens + result.usage.completionTokens;
3346
3347
  fastify.log.debug({ tokensUsed: totalTokens }, "LLM response received for insights");
3347
- if (platform16.analytics) {
3348
- platform16.analytics.track("ai_insights.chat", {
3348
+ if (platform17.analytics) {
3349
+ platform17.analytics.track("ai_insights.chat", {
3349
3350
  questionLength: body.question.length,
3350
3351
  contextIncluded: Object.keys(contextConfig).filter((k) => contextConfig[k]),
3351
3352
  timeRange: contextConfig.timeRange,
@@ -3374,9 +3375,9 @@ Be concise but thorough. Use markdown formatting.`;
3374
3375
  }
3375
3376
  };
3376
3377
  } catch (error) {
3377
- platform16?.logger.error("Failed to generate insights", error instanceof Error ? error : new Error(String(error)));
3378
- if (platform16?.analytics) {
3379
- platform16.analytics.track("ai_insights.error", {
3378
+ platform17?.logger.error("Failed to generate insights", error instanceof Error ? error : new Error(String(error)));
3379
+ if (platform17?.analytics) {
3380
+ platform17.analytics.track("ai_insights.error", {
3380
3381
  error: error instanceof Error ? error.message : "Unknown error",
3381
3382
  questionLength: request.body?.question?.length ?? 0
3382
3383
  }).catch(() => {
@@ -6224,11 +6225,46 @@ function registerRequestTimeoutGuard(server, config) {
6224
6225
  });
6225
6226
  }
6226
6227
 
6228
+ // src/middleware/rate-limit.ts
6229
+ function extractTenantId(request) {
6230
+ return request.headers["x-tenant-id"] || process.env.KB_TENANT_ID || "default";
6231
+ }
6232
+ function createRateLimitMiddleware(rateLimiter) {
6233
+ return async function rateLimitMiddleware(request, reply) {
6234
+ const tenantId = extractTenantId(request);
6235
+ const result = await rateLimiter.checkLimit(tenantId, "requests");
6236
+ reply.header("X-RateLimit-Limit", result.limit);
6237
+ reply.header("X-RateLimit-Remaining", result.remaining);
6238
+ reply.header("X-RateLimit-Reset", result.resetAt);
6239
+ if (!result.allowed) {
6240
+ const retryAfter = Math.ceil((result.resetAt - Date.now()) / 1e3);
6241
+ reply.header("Retry-After", retryAfter);
6242
+ return reply.code(429).send({
6243
+ error: "RATE_LIMIT_EXCEEDED",
6244
+ message: "Too many requests. Please try again later.",
6245
+ retryAfter,
6246
+ limit: result.limit,
6247
+ resetAt: new Date(result.resetAt).toISOString()
6248
+ });
6249
+ }
6250
+ request.tenantId = tenantId;
6251
+ };
6252
+ }
6253
+
6254
+ // src/middleware/tenant-rate-limit.ts
6255
+ function registerTenantRateLimitMiddleware(server, cache) {
6256
+ const limiter = new TenantRateLimiter(cache);
6257
+ limiter.setTier("default", getDefaultTenantTier());
6258
+ const handler = createRateLimitMiddleware(limiter);
6259
+ server.addHook("preHandler", handler);
6260
+ }
6261
+
6227
6262
  // src/middleware/index.ts
6228
6263
  function registerMiddleware(server, config) {
6229
6264
  registerStartupGuard(server, config);
6230
6265
  registerSecurityHeadersMiddleware(server);
6231
6266
  registerRequestIdMiddleware(server);
6267
+ registerTenantRateLimitMiddleware(server, platform.cache);
6232
6268
  registerMockModeMiddleware(server, config);
6233
6269
  registerCacheMiddleware(server);
6234
6270
  registerRequestTimeoutGuard(server, config);