@axiom-lattice/gateway 2.1.21 → 2.1.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/gateway",
3
- "version": "2.1.21",
3
+ "version": "2.1.23",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -17,9 +17,13 @@
17
17
  "description": "API Gateway for LangGraph agent-based applications",
18
18
  "dependencies": {
19
19
  "@fastify/cors": "^11.0.0",
20
+ "@fastify/http-proxy": "^9.5.0",
21
+ "@fastify/multipart": "^9.4.0",
22
+ "@fastify/reply-from": "^12.5.0",
20
23
  "@fastify/sensible": "^6.0.3",
21
24
  "@fastify/swagger": "^9.5.1",
22
25
  "@fastify/swagger-ui": "^5.2.3",
26
+ "@fastify/websocket": "^11.0.1",
23
27
  "@langchain/core": "1.1.4",
24
28
  "@langchain/langgraph": "1.0.4",
25
29
  "@supabase/supabase-js": "^2.49.1",
@@ -32,15 +36,16 @@
32
36
  "pino-roll": "^3.1.0",
33
37
  "redis": "^5.0.1",
34
38
  "uuid": "^9.0.1",
35
- "@axiom-lattice/core": "2.1.16",
36
- "@axiom-lattice/protocols": "2.1.10",
37
- "@axiom-lattice/queue-redis": "1.0.9"
39
+ "@axiom-lattice/core": "2.1.18",
40
+ "@axiom-lattice/protocols": "2.1.11",
41
+ "@axiom-lattice/queue-redis": "1.0.10"
38
42
  },
39
43
  "devDependencies": {
40
44
  "@types/jest": "^29.5.14",
41
45
  "@types/lodash": "^4.17.16",
42
46
  "@types/node": "^20.17.23",
43
47
  "@types/uuid": "^9.0.8",
48
+ "@types/ws": "^8.18.1",
44
49
  "@typescript-eslint/eslint-plugin": "^7.2.0",
45
50
  "@typescript-eslint/parser": "^7.2.0",
46
51
  "eslint": "^8.57.0",
@@ -0,0 +1,334 @@
1
+ import { Readable } from "stream";
2
+ import { FastifyInstance } from "fastify";
3
+ import { sandboxService } from "../services/sandbox_service";
4
+ import { getSandBoxManager } from "@axiom-lattice/core";
5
+
6
+ /** Get filename from path (e.g. /home/gem/uploads/foo.pdf -> foo.pdf) */
7
+ function getFilenameFromPath(path: string): string {
8
+ const segments = path.replace(/\/+$/, "").split("/");
9
+ return segments[segments.length - 1] || "download";
10
+ }
11
+
12
+ /** Common extension -> MIME type for download response */
13
+ const EXT_TO_MIME: Record<string, string> = {
14
+ ".txt": "text/plain",
15
+ ".html": "text/html",
16
+ ".css": "text/css",
17
+ ".js": "application/javascript",
18
+ ".json": "application/json",
19
+ ".pdf": "application/pdf",
20
+ ".png": "image/png",
21
+ ".jpg": "image/jpeg",
22
+ ".jpeg": "image/jpeg",
23
+ ".gif": "image/gif",
24
+ ".webp": "image/webp",
25
+ ".svg": "image/svg+xml",
26
+ ".zip": "application/zip",
27
+ ".csv": "text/csv",
28
+ ".xml": "application/xml",
29
+ };
30
+
31
+ function getContentTypeFromFilename(filename: string): string {
32
+ const ext = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
33
+ return EXT_TO_MIME[ext] ?? "application/octet-stream";
34
+ }
35
+
36
+ interface SandboxParams {
37
+ assistantId: string;
38
+ threadId: string;
39
+ }
40
+
41
+ interface ProxyParams extends SandboxParams {
42
+ "*": string;
43
+ }
44
+
45
+ interface ResourceParams extends SandboxParams {
46
+ resourcePath: string;
47
+ }
48
+
49
+ export function registerSandboxProxyRoutes(app: FastifyInstance): void {
50
+ // Register uploadfile route FIRST before wildcard routes to ensure it matches
51
+ app.post<{ Params: SandboxParams }>(
52
+ "/api/assistants/:assistantId/threads/:threadId/sandbox/uploadfile",
53
+ async (request, reply) => {
54
+ console.log("[Sandbox Upload] Route matched:", request.url);
55
+ const { assistantId, threadId } = request.params;
56
+
57
+ const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
58
+ if (!sandboxConfig) {
59
+ return reply.status(500).send({ error: "Assistant sandbox config not found" });
60
+ }
61
+
62
+ const { isolatedLevel } = sandboxConfig;
63
+ const sandboxName = sandboxService.computeSandboxName(
64
+ assistantId,
65
+ threadId,
66
+ isolatedLevel
67
+ );
68
+
69
+ const sandboxManager = getSandBoxManager("default")
70
+ const sandbox = await sandboxManager.createSandbox(sandboxName)
71
+
72
+ try {
73
+ const data = await request.file();
74
+ if (!data) {
75
+ return reply.status(400).send({ error: "No file in request" });
76
+ }
77
+
78
+ const buffer = await data.toBuffer();
79
+ const pathEntry = data.fields?.path;
80
+ const pathValue =
81
+ pathEntry && typeof pathEntry === "object" && "value" in pathEntry
82
+ ? String((pathEntry as { value: unknown }).value)
83
+ : typeof pathEntry === "string"
84
+ ? pathEntry
85
+ : undefined;
86
+
87
+ const formData = new FormData();
88
+ formData.append("file", new Blob([buffer]), data.filename ?? "file");
89
+
90
+ const path = `/home/gem/uploads/${pathValue ? pathValue : ""}${data.filename}`;
91
+ const uploadResult = await sandbox.file.uploadFile({
92
+ file: buffer,
93
+ path: path,
94
+ })
95
+
96
+ if (!uploadResult.ok) {
97
+ return reply.status(502).send({ error: `Upload error: ${uploadResult.error}` });
98
+ }
99
+
100
+ const relativePath = uploadResult.body?.data?.file_path.replace(`/home/gem`, "");
101
+ const result = { id: relativePath, name: data.filename, size: buffer.length };
102
+
103
+ return reply.status(200).send({ message: "File uploaded successfully", ...result });
104
+ } catch (error: unknown) {
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ return reply.status(502).send({ error: `Upload proxy error: ${message}` });
107
+ }
108
+ }
109
+ );
110
+
111
+ // Download file from sandbox: GET with query param path (file path in sandbox, e.g. /home/gem/uploads/foo.txt)
112
+ app.get<{
113
+ Params: SandboxParams;
114
+ Querystring: { path: string };
115
+ }>(
116
+ "/api/assistants/:assistantId/threads/:threadId/sandbox/downloadfile",
117
+ async (request, reply) => {
118
+ const { assistantId, threadId } = request.params;
119
+ const { path: filePath } = request.query;
120
+
121
+ if (!filePath || typeof filePath !== "string") {
122
+ return reply.status(400).send({ error: "Query parameter 'path' is required" });
123
+ }
124
+
125
+ const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
126
+ if (!sandboxConfig) {
127
+ return reply.status(404).send({ error: "Assistant sandbox config not found" });
128
+ }
129
+
130
+ const { isolatedLevel } = sandboxConfig;
131
+ const sandboxName = sandboxService.computeSandboxName(
132
+ assistantId,
133
+ threadId,
134
+ isolatedLevel
135
+ );
136
+
137
+ const sandboxManager = getSandBoxManager("default");
138
+ const sandbox = await sandboxManager.createSandbox(sandboxName);
139
+
140
+ try {
141
+ // Resolve path: if relative (no leading /), prefix with /home/gem
142
+ const resolvedPath = filePath.startsWith("/home/gem") ? filePath : `/home/gem/${filePath.replace(/^\//, "")}`;
143
+ const filename = getFilenameFromPath(resolvedPath);
144
+ const inferredContentType = getContentTypeFromFilename(filename);
145
+
146
+ const downloadResult = await sandbox.file.downloadFile({
147
+ path: resolvedPath,
148
+ });
149
+
150
+ if (!downloadResult.ok) {
151
+ return reply.status(502).send({
152
+ error: `Download error: ${JSON.stringify(downloadResult.error)}`,
153
+ });
154
+ }
155
+
156
+ // Use body.stream() to get ReadableStream and pipe directly (no buffering)
157
+ const body = downloadResult.body as unknown as {
158
+ stream?: () => ReadableStream<Uint8Array>;
159
+ contentType?: string;
160
+ contentDisposition?: string;
161
+ };
162
+ if (typeof body?.stream === "function") {
163
+ const webStream = body.stream();
164
+ const nodeStream = Readable.fromWeb(webStream);
165
+ const contentType = body.contentType ?? inferredContentType;
166
+ const contentDisposition =
167
+ body.contentDisposition ?? `inline; filename="${filename.replace(/"/g, '\\"')}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
168
+ reply = reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(nodeStream);
169
+ return reply;
170
+ }
171
+
172
+ // Fallback: buffer body when stream is not available
173
+ const bodyUnknown = downloadResult.body as unknown;
174
+ let buf: Buffer;
175
+ let contentType = inferredContentType;
176
+ let contentDisposition = `inline; filename="${filename.replace(/"/g, '\\"')}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
177
+
178
+ if (bodyUnknown instanceof ArrayBuffer) {
179
+ buf = Buffer.from(bodyUnknown);
180
+ } else if (bodyUnknown instanceof Buffer) {
181
+ buf = bodyUnknown;
182
+ } else if (
183
+ bodyUnknown &&
184
+ typeof (bodyUnknown as { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer === "function"
185
+ ) {
186
+ const res = bodyUnknown as { arrayBuffer: () => Promise<ArrayBuffer>; headers?: Headers };
187
+ buf = Buffer.from(await res.arrayBuffer());
188
+ if (res.headers?.get("content-type")) contentType = res.headers.get("content-type")!;
189
+ if (res.headers?.get("content-disposition")) contentDisposition = res.headers.get("content-disposition")!;
190
+ } else if (bodyUnknown && typeof (bodyUnknown as { blob?: () => Promise<Blob> }).blob === "function") {
191
+ const blob = await (bodyUnknown as { blob: () => Promise<Blob> }).blob();
192
+ buf = Buffer.from(await blob.arrayBuffer());
193
+ } else {
194
+ return reply.status(502).send({ error: "Unexpected download response format" });
195
+ }
196
+
197
+ reply = reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(buf);
198
+ return reply;
199
+ } catch (error: unknown) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ return reply.status(502).send({ error: `Download proxy error: ${message}` });
202
+ }
203
+ }
204
+ );
205
+
206
+ // app.get<{ Params: SandboxParams }>(
207
+ // "/api/assistants/:assistantId/threads/:threadId/sandbox/browser",
208
+ // async (request, reply) => {
209
+ // const { assistantId, threadId } = request.params;
210
+
211
+ // const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
212
+ // if (!sandboxConfig) {
213
+ // const errorHtml = sandboxService.generateErrorHtml(
214
+ // assistantId,
215
+ // threadId,
216
+ // "unknown",
217
+ // `Assistant ${assistantId} not found`
218
+ // );
219
+ // return reply.status(404).type("text/html").send(errorHtml);
220
+ // }
221
+
222
+ // const { isolatedLevel } = sandboxConfig;
223
+ // const sandboxName = sandboxService.computeSandboxName(
224
+ // assistantId,
225
+ // threadId,
226
+ // isolatedLevel
227
+ // );
228
+
229
+ // try {
230
+ // const html = await sandboxService.getVncHtml(sandboxName);
231
+ // const rewrittenHtml = sandboxService.rewriteHtml(html, assistantId, threadId);
232
+ // return reply.type("text/html").send(rewrittenHtml);
233
+ // } catch (error: any) {
234
+ // const errorHtml = sandboxService.generateErrorHtml(
235
+ // assistantId,
236
+ // threadId,
237
+ // isolatedLevel,
238
+ // error.message || "Failed to connect to sandbox"
239
+ // );
240
+ // return reply.status(502).type("text/html").send(errorHtml);
241
+ // }
242
+ // }
243
+ // );
244
+
245
+
246
+ // app.get<{ Params: SandboxParams }>(
247
+ // "/api/assistants/:assistantId/threads/:threadId/sandbox/websockify",
248
+ // { websocket: true },
249
+
250
+ // (connection, request) => {
251
+
252
+ // const url = (connection?.url) as string;
253
+ // console.log(`[WebSocket] Received connection from URL: ${url}`);
254
+
255
+ // const urlMatch = url.match(/\/api\/assistants\/([^/]+)\/threads\/([^/]+)\/sandbox\/websockify/);
256
+ // if (!urlMatch) {
257
+ // console.error(`[WebSocket] Failed to parse params from URL: ${url}`);
258
+ // connection.close(1008, "Invalid URL format");
259
+ // return;
260
+ // }
261
+
262
+ // const assistantId = urlMatch[1];
263
+ // const threadId = urlMatch[2];
264
+ // console.log(`[WebSocket] Parsed params - assistantId: ${assistantId}, threadId: ${threadId}`);
265
+
266
+ // const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
267
+ // if (!sandboxConfig) {
268
+ // console.error(`[WebSocket] Assistant ${assistantId} not found`);
269
+ // connection.close(1008, "Assistant not found");
270
+ // return;
271
+ // }
272
+
273
+ // const { isolatedLevel } = sandboxConfig;
274
+ // const sandboxName = sandboxService.computeSandboxName(
275
+ // assistantId,
276
+ // threadId,
277
+ // isolatedLevel
278
+ // );
279
+
280
+ // const targetUrl = sandboxService.getTargetUrl(sandboxName);
281
+ // const targetWsUrl = targetUrl.replace(/^http/, "ws").replace(/^https/, "wss") + "/websockify";
282
+
283
+ // console.log(`[WebSocket] Connecting to target: ${targetWsUrl}`);
284
+
285
+ // const targetSocket = new WebSocket(targetWsUrl);
286
+ // const clientStream = createWebSocketStream(connection, { encoding: "utf8" });
287
+
288
+ // const targetStream = createWebSocketStream(targetSocket, { encoding: "utf8" });
289
+
290
+ // const forward = pipeline(clientStream, targetStream);
291
+ // const backward = pipeline(targetStream, clientStream);
292
+
293
+ // Promise.all([forward, backward]).catch((err) => {
294
+ // console.error(`[WebSocket] Proxy pipeline failed:`, err.message);
295
+ // targetSocket.terminate();
296
+ // connection.terminate();
297
+ // });
298
+ // }
299
+ // );
300
+
301
+ // app.get<{ Params: ProxyParams }>(
302
+ // "/api/assistants/:assistantId/threads/:threadId/sandbox/browser/vnc/*",
303
+ // async (request, reply) => {
304
+ // const { assistantId, threadId, "*": restPath } = request.params;
305
+
306
+ // const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
307
+ // if (!sandboxConfig) {
308
+ // return reply.status(404).send("Assistant not found");
309
+ // }
310
+
311
+ // const { isolatedLevel } = sandboxConfig;
312
+ // const sandboxName = sandboxService.computeSandboxName(
313
+ // assistantId,
314
+ // threadId,
315
+ // isolatedLevel
316
+ // );
317
+
318
+ // const targetPath = restPath ? `/vnc/${restPath}` : "/vnc/";
319
+ // const targetUrl = `${sandboxService.getTargetUrl(sandboxName)}${targetPath}`;
320
+
321
+ // try {
322
+ // const response = await fetch(targetUrl);
323
+ // const contentType = response.headers.get("content-type") || "application/octet-stream";
324
+
325
+ // const body = await response.arrayBuffer();
326
+ // reply.status(response.status).type(contentType).send(Buffer.from(body));
327
+ // } catch (error: any) {
328
+ // reply.status(502).send(`Proxy error: ${error.message}`);
329
+ // }
330
+ // }
331
+ // );
332
+
333
+
334
+ }