@agentick/gateway 0.0.1

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +477 -0
  3. package/dist/agent-registry.d.ts +51 -0
  4. package/dist/agent-registry.d.ts.map +1 -0
  5. package/dist/agent-registry.js +78 -0
  6. package/dist/agent-registry.js.map +1 -0
  7. package/dist/app-registry.d.ts +51 -0
  8. package/dist/app-registry.d.ts.map +1 -0
  9. package/dist/app-registry.js +78 -0
  10. package/dist/app-registry.js.map +1 -0
  11. package/dist/bin.d.ts +8 -0
  12. package/dist/bin.d.ts.map +1 -0
  13. package/dist/bin.js +37 -0
  14. package/dist/bin.js.map +1 -0
  15. package/dist/gateway.d.ts +165 -0
  16. package/dist/gateway.d.ts.map +1 -0
  17. package/dist/gateway.js +1339 -0
  18. package/dist/gateway.js.map +1 -0
  19. package/dist/http-transport.d.ts +65 -0
  20. package/dist/http-transport.d.ts.map +1 -0
  21. package/dist/http-transport.js +517 -0
  22. package/dist/http-transport.js.map +1 -0
  23. package/dist/index.d.ts +16 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/protocol.d.ts +162 -0
  28. package/dist/protocol.d.ts.map +1 -0
  29. package/dist/protocol.js +16 -0
  30. package/dist/protocol.js.map +1 -0
  31. package/dist/session-manager.d.ts +101 -0
  32. package/dist/session-manager.d.ts.map +1 -0
  33. package/dist/session-manager.js +208 -0
  34. package/dist/session-manager.js.map +1 -0
  35. package/dist/testing.d.ts +92 -0
  36. package/dist/testing.d.ts.map +1 -0
  37. package/dist/testing.js +129 -0
  38. package/dist/testing.js.map +1 -0
  39. package/dist/transport-protocol.d.ts +162 -0
  40. package/dist/transport-protocol.d.ts.map +1 -0
  41. package/dist/transport-protocol.js +16 -0
  42. package/dist/transport-protocol.js.map +1 -0
  43. package/dist/transport.d.ts +115 -0
  44. package/dist/transport.d.ts.map +1 -0
  45. package/dist/transport.js +56 -0
  46. package/dist/transport.js.map +1 -0
  47. package/dist/types.d.ts +314 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +37 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/websocket-server.d.ts +87 -0
  52. package/dist/websocket-server.d.ts.map +1 -0
  53. package/dist/websocket-server.js +245 -0
  54. package/dist/websocket-server.js.map +1 -0
  55. package/dist/ws-transport.d.ts +17 -0
  56. package/dist/ws-transport.d.ts.map +1 -0
  57. package/dist/ws-transport.js +174 -0
  58. package/dist/ws-transport.js.map +1 -0
  59. package/package.json +51 -0
  60. package/src/__tests__/custom-methods.spec.ts +220 -0
  61. package/src/__tests__/gateway-methods.spec.ts +262 -0
  62. package/src/__tests__/gateway.spec.ts +404 -0
  63. package/src/__tests__/guards.spec.ts +235 -0
  64. package/src/__tests__/protocol.spec.ts +58 -0
  65. package/src/__tests__/session-manager.spec.ts +220 -0
  66. package/src/__tests__/ws-transport.spec.ts +246 -0
  67. package/src/app-registry.ts +103 -0
  68. package/src/bin.ts +38 -0
  69. package/src/gateway.ts +1712 -0
  70. package/src/http-transport.ts +623 -0
  71. package/src/index.ts +94 -0
  72. package/src/session-manager.ts +272 -0
  73. package/src/testing.ts +236 -0
  74. package/src/transport-protocol.ts +249 -0
  75. package/src/transport.ts +191 -0
  76. package/src/types.ts +392 -0
  77. package/src/websocket-server.ts +303 -0
  78. package/src/ws-transport.ts +205 -0
@@ -0,0 +1,623 @@
1
+ /**
2
+ * HTTP/SSE Transport
3
+ *
4
+ * Implements the Transport interface using HTTP requests and Server-Sent Events.
5
+ * This enables web browser clients to connect to the gateway without WebSocket support.
6
+ */
7
+
8
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http";
9
+ import { extractToken, validateAuth, setSSEHeaders, type AuthResult } from "@agentick/server";
10
+ import { isGuardError } from "@agentick/shared";
11
+ import type { GatewayMessage, RequestMessage } from "./transport-protocol.js";
12
+ import type { ClientState } from "./types.js";
13
+ import { BaseTransport, type TransportClient, type TransportConfig } from "./transport.js";
14
+
15
+ // ============================================================================
16
+ // HTTP Client (SSE connection)
17
+ // ============================================================================
18
+
19
+ class HTTPClientImpl implements TransportClient {
20
+ readonly id: string;
21
+ readonly state: ClientState;
22
+ private response: ServerResponse | null = null;
23
+ private _isConnected = false;
24
+
25
+ constructor(id: string) {
26
+ this.id = id;
27
+ this.state = {
28
+ id,
29
+ connectedAt: new Date(),
30
+ authenticated: false,
31
+ subscriptions: new Set(),
32
+ };
33
+ }
34
+
35
+ /** Set the SSE response object */
36
+ setResponse(res: ServerResponse): void {
37
+ this.response = res;
38
+ this._isConnected = true;
39
+
40
+ res.on("close", () => {
41
+ this._isConnected = false;
42
+ this.response = null;
43
+ });
44
+ }
45
+
46
+ send(message: GatewayMessage): void {
47
+ if (this.response && this._isConnected) {
48
+ const data = JSON.stringify(message);
49
+ this.response.write(`data: ${data}\n\n`);
50
+ }
51
+ }
52
+
53
+ close(_code?: number, _reason?: string): void {
54
+ if (this.response) {
55
+ this.response.end();
56
+ this.response = null;
57
+ }
58
+ this._isConnected = false;
59
+ }
60
+
61
+ get isConnected(): boolean {
62
+ return this._isConnected;
63
+ }
64
+ }
65
+
66
+ // ============================================================================
67
+ // HTTP/SSE Transport
68
+ // ============================================================================
69
+
70
+ export interface HTTPTransportConfig extends TransportConfig {
71
+ /** CORS origin (default: "*") */
72
+ corsOrigin?: string;
73
+
74
+ /** Path prefix for all endpoints (default: "") */
75
+ pathPrefix?: string;
76
+ }
77
+
78
+ export class HTTPTransport extends BaseTransport {
79
+ readonly type = "http" as const;
80
+ private server: Server | null = null;
81
+ private httpConfig: HTTPTransportConfig;
82
+
83
+ constructor(config: HTTPTransportConfig) {
84
+ super(config);
85
+ this.httpConfig = config;
86
+ }
87
+
88
+ override start(): Promise<void> {
89
+ return new Promise((resolve, reject) => {
90
+ try {
91
+ this.server = createServer((req, res) => {
92
+ this.handleRequest(req, res).catch((error) => {
93
+ console.error("Request error:", error);
94
+ if (!res.headersSent) {
95
+ res.writeHead(500, { "Content-Type": "application/json" });
96
+ res.end(JSON.stringify({ error: "Internal server error" }));
97
+ }
98
+ });
99
+ });
100
+
101
+ this.server.on("error", (error) => {
102
+ this.handlers.error?.(error);
103
+ reject(error);
104
+ });
105
+
106
+ this.server.listen(this.config.port, this.config.host, () => {
107
+ resolve();
108
+ });
109
+ } catch (error) {
110
+ reject(error);
111
+ }
112
+ });
113
+ }
114
+
115
+ override stop(): Promise<void> {
116
+ return new Promise((resolve) => {
117
+ if (!this.server) {
118
+ resolve();
119
+ return;
120
+ }
121
+
122
+ // Close all client connections
123
+ for (const client of this.clients.values()) {
124
+ client.close(1001, "Server shutting down");
125
+ }
126
+ this.clients.clear();
127
+
128
+ this.server.close(() => {
129
+ this.server = null;
130
+ resolve();
131
+ });
132
+ });
133
+ }
134
+
135
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
136
+ // Set CORS headers
137
+ const origin = this.httpConfig.corsOrigin ?? "*";
138
+ res.setHeader("Access-Control-Allow-Origin", origin);
139
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
140
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
141
+ res.setHeader("Access-Control-Allow-Credentials", "true");
142
+
143
+ // Handle preflight
144
+ if (req.method === "OPTIONS") {
145
+ res.writeHead(204);
146
+ res.end();
147
+ return;
148
+ }
149
+
150
+ const prefix = this.httpConfig.pathPrefix ?? "";
151
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
152
+ const path = url.pathname.replace(prefix, "");
153
+
154
+ // Route requests - exact matches first
155
+ switch (path) {
156
+ case "/events":
157
+ return this.handleSSE(req, res);
158
+ case "/send":
159
+ return this.handleSend(req, res);
160
+ case "/invoke":
161
+ return this.handleInvoke(req, res);
162
+ case "/subscribe":
163
+ return this.handleSubscribe(req, res);
164
+ case "/abort":
165
+ return this.handleAbort(req, res);
166
+ case "/close":
167
+ return this.handleClose(req, res);
168
+ case "/channel":
169
+ return this.handleChannel(req, res);
170
+ }
171
+
172
+ res.writeHead(404, { "Content-Type": "application/json" });
173
+ res.end(JSON.stringify({ error: "Not found" }));
174
+ }
175
+
176
+ /**
177
+ * SSE endpoint - establishes long-lived connection for events
178
+ */
179
+ private async handleSSE(req: IncomingMessage, res: ServerResponse): Promise<void> {
180
+ // Get auth token from header or query
181
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
182
+ const token = extractToken(req) ?? url.searchParams.get("token") ?? undefined;
183
+
184
+ // Validate auth
185
+ const authResult = await validateAuth(token, this.config.auth);
186
+ if (!authResult.valid) {
187
+ res.writeHead(401, { "Content-Type": "application/json" });
188
+ res.end(JSON.stringify({ error: "Authentication failed" }));
189
+ return;
190
+ }
191
+
192
+ // Create client
193
+ const clientId = url.searchParams.get("clientId") ?? this.generateClientId();
194
+ let client = this.clients.get(clientId) as HTTPClientImpl | undefined;
195
+
196
+ if (!client) {
197
+ client = new HTTPClientImpl(clientId);
198
+ client.state.authenticated = true;
199
+ client.state.user = authResult.user;
200
+ client.state.metadata = authResult.metadata;
201
+ this.clients.set(clientId, client);
202
+
203
+ // Notify connection handler
204
+ this.handlers.connection?.(client);
205
+ }
206
+
207
+ // Setup SSE response
208
+ setSSEHeaders(res);
209
+
210
+ client.setResponse(res);
211
+
212
+ // Send connection confirmation
213
+ // Client expects type: "connection" to resolve the connection promise
214
+ client.send({
215
+ type: "connection" as any,
216
+ connectionId: clientId,
217
+ subscriptions: Array.from(client.state.subscriptions),
218
+ } as any);
219
+
220
+ // Handle disconnect
221
+ res.on("close", () => {
222
+ this.clients.delete(clientId);
223
+ this.handlers.disconnect?.(clientId);
224
+ });
225
+
226
+ // Keep connection alive with periodic heartbeat
227
+ const heartbeat = setInterval(() => {
228
+ if (client?.isConnected) {
229
+ res.write(":heartbeat\n\n");
230
+ } else {
231
+ clearInterval(heartbeat);
232
+ }
233
+ }, 30000);
234
+
235
+ res.on("close", () => clearInterval(heartbeat));
236
+ }
237
+
238
+ /**
239
+ * Send endpoint - receives messages and streams response
240
+ */
241
+ private async handleSend(req: IncomingMessage, res: ServerResponse): Promise<void> {
242
+ if (req.method !== "POST") {
243
+ res.writeHead(405, { "Content-Type": "application/json" });
244
+ res.end(JSON.stringify({ error: "Method not allowed" }));
245
+ return;
246
+ }
247
+
248
+ // Get auth token
249
+ const token = extractToken(req);
250
+ const authResult = await validateAuth(token, this.config.auth);
251
+ if (!authResult.valid) {
252
+ res.writeHead(401, { "Content-Type": "application/json" });
253
+ res.end(JSON.stringify({ error: "Authentication failed" }));
254
+ return;
255
+ }
256
+
257
+ // Parse body
258
+ const body = await this.parseBody(req);
259
+ if (!body) {
260
+ res.writeHead(400, { "Content-Type": "application/json" });
261
+ res.end(JSON.stringify({ error: "Invalid request body" }));
262
+ return;
263
+ }
264
+
265
+ const sessionId = ((body as any).sessionId as string) ?? "main";
266
+ const rawMessage = (body as any).message;
267
+
268
+ // Validate and sanitize the message to ensure it's a proper Message object
269
+ // This prevents any unexpected properties from being passed through
270
+ if (
271
+ !rawMessage ||
272
+ typeof rawMessage !== "object" ||
273
+ !rawMessage.role ||
274
+ !Array.isArray(rawMessage.content)
275
+ ) {
276
+ res.writeHead(400, { "Content-Type": "application/json" });
277
+ res.end(
278
+ JSON.stringify({
279
+ error: "Invalid message format. Expected { role, content: ContentBlock[] }",
280
+ }),
281
+ );
282
+ return;
283
+ }
284
+
285
+ // Create a clean Message object with only expected properties
286
+ const message = {
287
+ role: rawMessage.role as "user" | "assistant" | "system" | "tool" | "event",
288
+ content: rawMessage.content,
289
+ ...(rawMessage.id && { id: rawMessage.id }),
290
+ ...(rawMessage.metadata && { metadata: rawMessage.metadata }),
291
+ };
292
+
293
+ // Check if we have a direct send handler
294
+ if (!this.config.onDirectSend) {
295
+ res.writeHead(501, { "Content-Type": "application/json" });
296
+ res.end(JSON.stringify({ error: "Send not supported without onDirectSend handler" }));
297
+ return;
298
+ }
299
+
300
+ // Setup streaming response
301
+ setSSEHeaders(res);
302
+
303
+ try {
304
+ // Use the direct send handler to stream events
305
+ const events = this.config.onDirectSend(sessionId, message);
306
+
307
+ for await (const event of events) {
308
+ const sseData = {
309
+ type: event.type,
310
+ sessionId,
311
+ ...(event.data && typeof event.data === "object" ? event.data : {}),
312
+ };
313
+ res.write(`data: ${JSON.stringify(sseData)}\n\n`);
314
+ }
315
+
316
+ // Send execution_end
317
+ res.write(`data: ${JSON.stringify({ type: "execution_end", sessionId })}\n\n`);
318
+ } catch (error) {
319
+ const errorMessage = error instanceof Error ? error.message : String(error);
320
+ res.write(`data: ${JSON.stringify({ type: "error", error: errorMessage, sessionId })}\n\n`);
321
+ } finally {
322
+ res.end();
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Invoke endpoint - execute custom gateway methods
328
+ * Returns JSON (not streaming) for simpler client handling
329
+ */
330
+ private async handleInvoke(req: IncomingMessage, res: ServerResponse): Promise<void> {
331
+ if (req.method !== "POST") {
332
+ res.writeHead(405, { "Content-Type": "application/json" });
333
+ res.end(JSON.stringify({ error: "Method not allowed" }));
334
+ return;
335
+ }
336
+
337
+ // Get auth token
338
+ const token = extractToken(req);
339
+ const authResult = await validateAuth(token, this.config.auth);
340
+ if (!authResult.valid) {
341
+ res.writeHead(401, { "Content-Type": "application/json" });
342
+ res.end(JSON.stringify({ error: "Authentication failed" }));
343
+ return;
344
+ }
345
+
346
+ // Parse body
347
+ const body = await this.parseBody(req);
348
+ if (!body) {
349
+ res.writeHead(400, { "Content-Type": "application/json" });
350
+ res.end(JSON.stringify({ error: "Invalid request body" }));
351
+ return;
352
+ }
353
+
354
+ const method = (body as { method?: string }).method;
355
+ const params = ((body as { params?: Record<string, unknown> }).params ?? {}) as Record<
356
+ string,
357
+ unknown
358
+ >;
359
+
360
+ if (!method || typeof method !== "string") {
361
+ res.writeHead(400, { "Content-Type": "application/json" });
362
+ res.end(JSON.stringify({ error: "method is required" }));
363
+ return;
364
+ }
365
+
366
+ // Check if we have an invoke handler
367
+ if (!this.config.onInvoke) {
368
+ res.writeHead(501, { "Content-Type": "application/json" });
369
+ res.end(JSON.stringify({ error: "Method invocation not supported" }));
370
+ return;
371
+ }
372
+
373
+ try {
374
+ const result = await this.config.onInvoke(method, params, authResult.user);
375
+ res.writeHead(200, { "Content-Type": "application/json" });
376
+ res.end(JSON.stringify(result));
377
+ } catch (error) {
378
+ const errorMessage = error instanceof Error ? error.message : String(error);
379
+ const statusCode = isGuardError(error) ? 403 : 400;
380
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
381
+ res.end(JSON.stringify({ error: errorMessage }));
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Subscribe endpoint - manage session subscriptions
387
+ */
388
+ private async handleSubscribe(req: IncomingMessage, res: ServerResponse): Promise<void> {
389
+ if (req.method !== "POST") {
390
+ res.writeHead(405, { "Content-Type": "application/json" });
391
+ res.end(JSON.stringify({ error: "Method not allowed" }));
392
+ return;
393
+ }
394
+
395
+ const token = extractToken(req);
396
+ const authResult = await validateAuth(token, this.config.auth);
397
+ if (!authResult.valid) {
398
+ res.writeHead(401, { "Content-Type": "application/json" });
399
+ res.end(JSON.stringify({ error: "Authentication failed" }));
400
+ return;
401
+ }
402
+
403
+ const body = await this.parseBody(req);
404
+ if (!body) {
405
+ res.writeHead(400, { "Content-Type": "application/json" });
406
+ res.end(JSON.stringify({ error: "Invalid request body" }));
407
+ return;
408
+ }
409
+
410
+ const { connectionId, add, remove } = body as {
411
+ connectionId: string;
412
+ add?: string[];
413
+ remove?: string[];
414
+ };
415
+
416
+ const client = this.clients.get(connectionId);
417
+ if (!client) {
418
+ res.writeHead(404, { "Content-Type": "application/json" });
419
+ res.end(JSON.stringify({ error: "Connection not found" }));
420
+ return;
421
+ }
422
+
423
+ // Update subscriptions
424
+ if (add) {
425
+ for (const sessionId of add) {
426
+ client.state.subscriptions.add(sessionId);
427
+ }
428
+ }
429
+ if (remove) {
430
+ for (const sessionId of remove) {
431
+ client.state.subscriptions.delete(sessionId);
432
+ }
433
+ }
434
+
435
+ res.writeHead(200, { "Content-Type": "application/json" });
436
+ res.end(
437
+ JSON.stringify({
438
+ subscriptions: Array.from(client.state.subscriptions),
439
+ }),
440
+ );
441
+ }
442
+
443
+ /**
444
+ * Abort endpoint
445
+ */
446
+ private async handleAbort(req: IncomingMessage, res: ServerResponse): Promise<void> {
447
+ if (req.method !== "POST") {
448
+ res.writeHead(405, { "Content-Type": "application/json" });
449
+ res.end(JSON.stringify({ error: "Method not allowed" }));
450
+ return;
451
+ }
452
+
453
+ const token = extractToken(req);
454
+ const authResult = await validateAuth(token, this.config.auth);
455
+ if (!authResult.valid) {
456
+ res.writeHead(401, { "Content-Type": "application/json" });
457
+ res.end(JSON.stringify({ error: "Authentication failed" }));
458
+ return;
459
+ }
460
+
461
+ const body = await this.parseBody(req);
462
+ if (!body) {
463
+ res.writeHead(400, { "Content-Type": "application/json" });
464
+ res.end(JSON.stringify({ error: "Invalid request body" }));
465
+ return;
466
+ }
467
+
468
+ // Forward as request message
469
+ const requestId = `req-${Date.now().toString(36)}`;
470
+ const requestMessage: RequestMessage = {
471
+ type: "req",
472
+ id: requestId,
473
+ method: "abort",
474
+ params: body as Record<string, unknown>,
475
+ };
476
+
477
+ // Find any authenticated client to use
478
+ const client = this.getAuthenticatedClients()[0];
479
+ if (client) {
480
+ this.handlers.message?.(client.id, requestMessage);
481
+ }
482
+
483
+ res.writeHead(200, { "Content-Type": "application/json" });
484
+ res.end(JSON.stringify({ ok: true }));
485
+ }
486
+
487
+ /**
488
+ * Close endpoint
489
+ */
490
+ private async handleClose(req: IncomingMessage, res: ServerResponse): Promise<void> {
491
+ if (req.method !== "POST") {
492
+ res.writeHead(405, { "Content-Type": "application/json" });
493
+ res.end(JSON.stringify({ error: "Method not allowed" }));
494
+ return;
495
+ }
496
+
497
+ const token = extractToken(req);
498
+ const authResult = await validateAuth(token, this.config.auth);
499
+ if (!authResult.valid) {
500
+ res.writeHead(401, { "Content-Type": "application/json" });
501
+ res.end(JSON.stringify({ error: "Authentication failed" }));
502
+ return;
503
+ }
504
+
505
+ const body = await this.parseBody(req);
506
+ if (!body) {
507
+ res.writeHead(400, { "Content-Type": "application/json" });
508
+ res.end(JSON.stringify({ error: "Invalid request body" }));
509
+ return;
510
+ }
511
+
512
+ // Forward as request message
513
+ const requestId = `req-${Date.now().toString(36)}`;
514
+ const requestMessage: RequestMessage = {
515
+ type: "req",
516
+ id: requestId,
517
+ method: "close",
518
+ params: body as Record<string, unknown>,
519
+ };
520
+
521
+ const client = this.getAuthenticatedClients()[0];
522
+ if (client) {
523
+ this.handlers.message?.(client.id, requestMessage);
524
+ }
525
+
526
+ res.writeHead(200, { "Content-Type": "application/json" });
527
+ res.end(JSON.stringify({ ok: true }));
528
+ }
529
+
530
+ /**
531
+ * Channel endpoint
532
+ */
533
+ private async handleChannel(req: IncomingMessage, res: ServerResponse): Promise<void> {
534
+ if (req.method !== "POST") {
535
+ res.writeHead(405, { "Content-Type": "application/json" });
536
+ res.end(JSON.stringify({ error: "Method not allowed" }));
537
+ return;
538
+ }
539
+
540
+ const token = extractToken(req);
541
+ const authResult = await validateAuth(token, this.config.auth);
542
+ if (!authResult.valid) {
543
+ res.writeHead(401, { "Content-Type": "application/json" });
544
+ res.end(JSON.stringify({ error: "Authentication failed" }));
545
+ return;
546
+ }
547
+
548
+ const body = await this.parseBody(req);
549
+ if (!body) {
550
+ res.writeHead(400, { "Content-Type": "application/json" });
551
+ res.end(JSON.stringify({ error: "Invalid request body" }));
552
+ return;
553
+ }
554
+
555
+ res.writeHead(200, { "Content-Type": "application/json" });
556
+ res.end(JSON.stringify({ ok: true }));
557
+ }
558
+
559
+ // ══════════════════════════════════════════════════════════════════════════
560
+ // Express Integration
561
+ // ══════════════════════════════════════════════════════════════════════════
562
+
563
+ /**
564
+ * Handle a request from Express middleware.
565
+ * This allows the gateway to be mounted in an existing Express app.
566
+ */
567
+ handleExpressRequest(
568
+ req: IncomingMessage & { path?: string },
569
+ res: ServerResponse & { status?: (code: number) => any; json?: (body: unknown) => void },
570
+ next: (err?: unknown) => void,
571
+ ): void {
572
+ this.handleRequest(req, res).catch((error) => {
573
+ console.error("Request error:", error);
574
+ if (!res.headersSent) {
575
+ if (res.status && res.json) {
576
+ res.status(500).json({ error: "Internal server error" });
577
+ } else {
578
+ res.writeHead(500, { "Content-Type": "application/json" });
579
+ res.end(JSON.stringify({ error: "Internal server error" }));
580
+ }
581
+ }
582
+ });
583
+ }
584
+
585
+ // ══════════════════════════════════════════════════════════════════════════
586
+ // Utilities
587
+ // ══════════════════════════════════════════════════════════════════════════
588
+
589
+ private parseBody(
590
+ req: IncomingMessage & { body?: unknown },
591
+ ): Promise<Record<string, unknown> | null> {
592
+ // If body already parsed by Express middleware, use it
593
+ if (req.body && typeof req.body === "object") {
594
+ return Promise.resolve(req.body as Record<string, unknown>);
595
+ }
596
+
597
+ // Otherwise, read from stream
598
+ return new Promise((resolve) => {
599
+ let body = "";
600
+ req.on("data", (chunk) => {
601
+ body += chunk.toString();
602
+ });
603
+ req.on("end", () => {
604
+ try {
605
+ resolve(JSON.parse(body));
606
+ } catch {
607
+ resolve(null);
608
+ }
609
+ });
610
+ req.on("error", () => {
611
+ resolve(null);
612
+ });
613
+ });
614
+ }
615
+ }
616
+
617
+ // ============================================================================
618
+ // Factory Function
619
+ // ============================================================================
620
+
621
+ export function createHTTPTransport(config: HTTPTransportConfig): HTTPTransport {
622
+ return new HTTPTransport(config);
623
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @agentick/gateway
3
+ *
4
+ * Standalone daemon for multi-client, multi-agent access.
5
+ * Supports both WebSocket and HTTP/SSE transports.
6
+ */
7
+
8
+ // Main exports
9
+ export { Gateway, createGateway } from "./gateway.js";
10
+ export { AppRegistry } from "./app-registry.js";
11
+ export { SessionManager } from "./session-manager.js";
12
+
13
+ // Transport layer
14
+ export {
15
+ type Transport,
16
+ type TransportClient,
17
+ type TransportConfig,
18
+ type TransportEvents,
19
+ BaseTransport,
20
+ } from "./transport.js";
21
+ export { WSTransport, createWSTransport } from "./ws-transport.js";
22
+ export { HTTPTransport, createHTTPTransport, type HTTPTransportConfig } from "./http-transport.js";
23
+
24
+ // Testing utilities
25
+ export {
26
+ createTestGateway,
27
+ createMockApp,
28
+ createMockSession,
29
+ createMockExecutionHandle,
30
+ waitForGatewayEvent,
31
+ type TestGatewayOptions,
32
+ type TestGatewayClient,
33
+ type TestGatewayResult,
34
+ type MockAppOptions,
35
+ type MockSessionOptions,
36
+ type MockSession,
37
+ type MockApp,
38
+ type MockSessionExecutionHandle,
39
+ type MockExecutionHandleOptions,
40
+ } from "./testing.js";
41
+
42
+ // Protocol types
43
+ export {
44
+ parseSessionKey,
45
+ formatSessionKey,
46
+ type SessionKey,
47
+ type ClientMessage,
48
+ type GatewayMessage,
49
+ type ConnectMessage,
50
+ type RequestMessage,
51
+ type ResponseMessage,
52
+ type EventMessage,
53
+ type GatewayMethod,
54
+ type GatewayEventType,
55
+ type SendParams,
56
+ type StatusParams,
57
+ type HistoryParams,
58
+ type StatusPayload,
59
+ type AppsPayload,
60
+ type SessionsPayload,
61
+ } from "./transport-protocol.js";
62
+
63
+ // Configuration types
64
+ export {
65
+ type GatewayConfig,
66
+ type AuthConfig,
67
+ type AuthResult,
68
+ type StorageConfig,
69
+ type ChannelAdapter,
70
+ type GatewayContext,
71
+ type SessionContext,
72
+ type SessionEvent,
73
+ type RoutingConfig,
74
+ type IncomingMessage,
75
+ type RoutingContext,
76
+ type ClientState,
77
+ type SessionState,
78
+ type GatewayEvents,
79
+ type UserContext,
80
+ // Method types
81
+ type MethodDefinition,
82
+ type MethodDefinitionInput,
83
+ type MethodNamespace,
84
+ type MethodsConfig,
85
+ type Method,
86
+ type SimpleMethodHandler,
87
+ type StreamingMethodHandler,
88
+ // Method factory
89
+ method,
90
+ isMethodDefinition,
91
+ METHOD_DEFINITION,
92
+ // Schema type for Zod 3/4 compatibility
93
+ type ZodLikeSchema,
94
+ } from "./types.js";