@kb-labs/gateway-app 0.2.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.
Files changed (46) hide show
  1. package/.kb/database/kb.sqlite-shm +0 -0
  2. package/.kb/database/kb.sqlite-wal +0 -0
  3. package/package.json +49 -0
  4. package/src/__tests__/auth-routes.test.ts +279 -0
  5. package/src/__tests__/execute-routes.test.ts +408 -0
  6. package/src/__tests__/execution-registry.test.ts +218 -0
  7. package/src/__tests__/health.test.ts +215 -0
  8. package/src/__tests__/live-gateway.e2e.test.ts +648 -0
  9. package/src/__tests__/llm-gateway.test.ts +361 -0
  10. package/src/__tests__/observability-collector.test.ts +59 -0
  11. package/src/__tests__/platform-api.test.ts +317 -0
  12. package/src/__tests__/registry.test.ts +546 -0
  13. package/src/__tests__/retry-executor.test.ts +244 -0
  14. package/src/__tests__/server.integration.test.ts +417 -0
  15. package/src/__tests__/subscription-registry.test.ts +308 -0
  16. package/src/__tests__/telemetry-ingest.test.ts +309 -0
  17. package/src/__tests__/tokens.test.ts +83 -0
  18. package/src/__tests__/ws-client-connect.e2e.test.ts +381 -0
  19. package/src/__tests__/ws-handshake.e2e.test.ts +288 -0
  20. package/src/auth/middleware.ts +50 -0
  21. package/src/auth/routes.ts +57 -0
  22. package/src/auth/tokens.ts +41 -0
  23. package/src/bootstrap.ts +98 -0
  24. package/src/clients/subscription-registry.ts +137 -0
  25. package/src/clients/ws-handler.ts +196 -0
  26. package/src/config.ts +20 -0
  27. package/src/docs/routes.ts +70 -0
  28. package/src/execute/errors.ts +21 -0
  29. package/src/execute/execution-registry.ts +84 -0
  30. package/src/execute/retry-executor.ts +159 -0
  31. package/src/execute/routes.ts +239 -0
  32. package/src/hosts/dispatcher.ts +2 -0
  33. package/src/hosts/registry.ts +305 -0
  34. package/src/hosts/ws-handler.ts +445 -0
  35. package/src/index.ts +7 -0
  36. package/src/llm/routes.ts +343 -0
  37. package/src/manifest.ts +21 -0
  38. package/src/observability/collector.ts +346 -0
  39. package/src/platform/routes.ts +195 -0
  40. package/src/server.ts +447 -0
  41. package/src/telemetry/routes.ts +89 -0
  42. package/src/ws/gateway-ws.ts +73 -0
  43. package/tsconfig.build.json +15 -0
  44. package/tsconfig.json +10 -0
  45. package/tsup.config.ts +8 -0
  46. package/vitest.config.ts +23 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @module Telemetry Ingestion — unified event collection endpoint.
3
+ *
4
+ * Exposes `POST /telemetry/v1/ingest` for external products to send events.
5
+ * Events are written to platform analytics via IAnalytics.track().
6
+ * Auth required — tenantId extracted from auth context.
7
+ */
8
+ import type { FastifyInstance } from 'fastify';
9
+ import { platform } from '@kb-labs/core-runtime';
10
+ import type { ILogger } from '@kb-labs/core-platform';
11
+ import {
12
+ TelemetryIngestRequestSchema,
13
+ type TelemetryIngestResponse,
14
+ } from '@kb-labs/gateway-contracts';
15
+
16
+ /**
17
+ * Register telemetry ingestion routes on the given Fastify scope.
18
+ * The scope is expected to have the auth middleware already applied.
19
+ */
20
+ export function registerTelemetryRoutes(app: FastifyInstance, logger: ILogger): void {
21
+ app.post('/telemetry/v1/ingest', { schema: { tags: ['Telemetry'], summary: 'Ingest telemetry events' } }, async (request, reply) => {
22
+ const auth = request.authContext;
23
+ if (!auth) {
24
+ return reply.code(401).send({ error: 'Unauthorized' });
25
+ }
26
+
27
+ const parsed = TelemetryIngestRequestSchema.safeParse(request.body);
28
+ if (!parsed.success) {
29
+ return reply.code(400).send({
30
+ error: 'Bad Request',
31
+ issues: parsed.error.issues,
32
+ });
33
+ }
34
+
35
+ const analytics = platform.analytics;
36
+ if (!analytics) {
37
+ return reply.code(503).send({
38
+ error: 'Analytics adapter not configured',
39
+ });
40
+ }
41
+
42
+ const { events } = parsed.data;
43
+ let accepted = 0;
44
+ let rejected = 0;
45
+ const errors: Array<{ index: number; message: string }> = [];
46
+
47
+ for (let i = 0; i < events.length; i++) {
48
+ const event = events[i]!;
49
+ try {
50
+ await analytics.track(event.type, {
51
+ // Source attribution — who sent this event
52
+ _source: event.source,
53
+ _tenantId: auth.namespaceId,
54
+ _ts: event.timestamp ?? new Date().toISOString(),
55
+ // Tags as flat properties for indexing
56
+ ...event.tags,
57
+ // Free-form payload
58
+ ...event.payload,
59
+ });
60
+ accepted++;
61
+ } catch (err) {
62
+ rejected++;
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ errors.push({ index: i, message });
65
+ logger.warn('Telemetry event rejected', {
66
+ index: i,
67
+ type: event.type,
68
+ source: event.source,
69
+ error: message,
70
+ });
71
+ }
72
+ }
73
+
74
+ logger.info('Telemetry ingest', {
75
+ tenantId: auth.namespaceId,
76
+ accepted,
77
+ rejected,
78
+ totalEvents: events.length,
79
+ });
80
+
81
+ const response: TelemetryIngestResponse = {
82
+ accepted,
83
+ rejected,
84
+ ...(errors.length > 0 ? { errors } : {}),
85
+ };
86
+
87
+ return reply.code(accepted > 0 ? 200 : 422).send(response);
88
+ });
89
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @module gateway-app/ws/gateway-ws
3
+ *
4
+ * Unified WebSocket upgrade handler for the gateway.
5
+ *
6
+ * Intercepts upgrade requests BEFORE @fastify/http-proxy:
7
+ * - Gateway-own WS paths (/hosts/connect, /clients/connect) → raw ws handlers
8
+ * - Everything else → delegated to @fastify/http-proxy for upstream WS proxy
9
+ *
10
+ * This eliminates the conflict between @fastify/websocket and @fastify/http-proxy
11
+ * which both try to attach 'upgrade' listeners and call assignSocket().
12
+ */
13
+
14
+ import type { Server, IncomingMessage } from 'node:http';
15
+ import type { Duplex } from 'node:stream';
16
+ import { WebSocketServer } from 'ws';
17
+ import type { ICache, ILogger } from '@kb-labs/core-platform';
18
+ import type { JwtConfig } from '@kb-labs/gateway-auth';
19
+ import { createWsHandler } from '../hosts/ws-handler.js';
20
+ import { createClientWsHandler } from '../clients/ws-handler.js';
21
+
22
+ const GATEWAY_WS_PATHS = new Set(['/hosts/connect', '/clients/connect']);
23
+
24
+ /**
25
+ * Attach gateway-own WebSocket endpoints using raw `ws` package.
26
+ *
27
+ * Must be called AFTER `app.ready()` (so http-proxy has registered its
28
+ * upgrade listener) but BEFORE `app.listen()`.
29
+ *
30
+ * Captures all existing 'upgrade' listeners, removes them, then installs
31
+ * a single unified handler that dispatches:
32
+ * - Gateway WS paths → raw ws handlers
33
+ * - All other paths → delegated to captured listeners (http-proxy)
34
+ */
35
+ export function attachGatewayWs(
36
+ server: Server,
37
+ cache: ICache,
38
+ jwtConfig: JwtConfig,
39
+ logger: ILogger,
40
+ hostRegistry?: import('../hosts/registry.js').HostRegistry,
41
+ ): void {
42
+ const wss = new WebSocketServer({ noServer: true });
43
+ const hostsHandler = createWsHandler(cache, jwtConfig, logger, hostRegistry);
44
+ const clientsHandler = createClientWsHandler(cache, jwtConfig, logger);
45
+
46
+ // Capture @fastify/http-proxy's upgrade listener(s)
47
+ const existingListeners = server.listeners('upgrade').slice() as Array<
48
+ (req: IncomingMessage, socket: Duplex, head: Buffer) => void
49
+ >;
50
+ server.removeAllListeners('upgrade');
51
+
52
+ server.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {
53
+ const pathname = new URL(req.url ?? '/', 'http://localhost').pathname;
54
+
55
+ if (GATEWAY_WS_PATHS.has(pathname)) {
56
+ // Gateway-own WS — handle directly with raw ws
57
+ wss.handleUpgrade(req, socket, head, (ws) => {
58
+ if (pathname === '/hosts/connect') {
59
+ hostsHandler(ws, req);
60
+ } else {
61
+ clientsHandler(ws, req);
62
+ }
63
+ });
64
+ } else {
65
+ // Delegate to @fastify/http-proxy for upstream WS proxy
66
+ for (const listener of existingListeners) {
67
+ listener.call(server, req, socket, head);
68
+ }
69
+ }
70
+ });
71
+
72
+ logger.info('Gateway WS endpoints attached', { paths: [...GATEWAY_WS_PATHS] });
73
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "baseUrl": ".",
6
+ "paths": {}
7
+ },
8
+ "include": [
9
+ "src/**/*"
10
+ ],
11
+ "exclude": [
12
+ "dist",
13
+ "node_modules"
14
+ ]
15
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@kb-labs/devkit/tsconfig/node.json",
3
+ "include": [
4
+ "src"
5
+ ],
6
+ "compilerOptions": {
7
+ "noEmit": true,
8
+ "types": ["node", "@fastify/swagger"]
9
+ }
10
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'tsup';
2
+ import nodePreset from '@kb-labs/devkit/tsup/node';
3
+
4
+ export default defineConfig({
5
+ ...nodePreset,
6
+ tsconfig: 'tsconfig.build.json',
7
+ entry: ['src/index.ts'],
8
+ });
@@ -0,0 +1,23 @@
1
+ import { mergeConfig } from 'vitest/config';
2
+ import base from '@kb-labs/devkit/vitest/node.js';
3
+
4
+ /**
5
+ * Gateway app vitest config.
6
+ *
7
+ * live-gateway.e2e.test.ts is excluded from the default test run because it
8
+ * requires a standalone Gateway instance (no upstream proxy for /api/v1).
9
+ * In the standard dev setup the gateway proxies /api/v1 to the REST API,
10
+ * which shadows the gateway's own execute routes.
11
+ *
12
+ * To run the live tests: vitest run src/__tests__/live-gateway.e2e.test.ts
13
+ */
14
+ export default mergeConfig(base, {
15
+ test: {
16
+ exclude: [
17
+ '**/node_modules/**',
18
+ '**/dist/**',
19
+ '**/*.live.test.ts',
20
+ '**/live-gateway.e2e.test.ts',
21
+ ],
22
+ },
23
+ });