@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.
- package/LICENSE +21 -0
- package/README.md +477 -0
- package/dist/agent-registry.d.ts +51 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +78 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/app-registry.d.ts +51 -0
- package/dist/app-registry.d.ts.map +1 -0
- package/dist/app-registry.js +78 -0
- package/dist/app-registry.js.map +1 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +37 -0
- package/dist/bin.js.map +1 -0
- package/dist/gateway.d.ts +165 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +1339 -0
- package/dist/gateway.js.map +1 -0
- package/dist/http-transport.d.ts +65 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/http-transport.js +517 -0
- package/dist/http-transport.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +162 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +16 -0
- package/dist/protocol.js.map +1 -0
- package/dist/session-manager.d.ts +101 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +208 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +129 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport-protocol.d.ts +162 -0
- package/dist/transport-protocol.d.ts.map +1 -0
- package/dist/transport-protocol.js +16 -0
- package/dist/transport-protocol.js.map +1 -0
- package/dist/transport.d.ts +115 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +56 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket-server.d.ts +87 -0
- package/dist/websocket-server.d.ts.map +1 -0
- package/dist/websocket-server.js +245 -0
- package/dist/websocket-server.js.map +1 -0
- package/dist/ws-transport.d.ts +17 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +174 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +51 -0
- package/src/__tests__/custom-methods.spec.ts +220 -0
- package/src/__tests__/gateway-methods.spec.ts +262 -0
- package/src/__tests__/gateway.spec.ts +404 -0
- package/src/__tests__/guards.spec.ts +235 -0
- package/src/__tests__/protocol.spec.ts +58 -0
- package/src/__tests__/session-manager.spec.ts +220 -0
- package/src/__tests__/ws-transport.spec.ts +246 -0
- package/src/app-registry.ts +103 -0
- package/src/bin.ts +38 -0
- package/src/gateway.ts +1712 -0
- package/src/http-transport.ts +623 -0
- package/src/index.ts +94 -0
- package/src/session-manager.ts +272 -0
- package/src/testing.ts +236 -0
- package/src/transport-protocol.ts +249 -0
- package/src/transport.ts +191 -0
- package/src/types.ts +392 -0
- package/src/websocket-server.ts +303 -0
- 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";
|