@axiom-lattice/gateway 2.1.28 → 2.1.30

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.
@@ -0,0 +1,598 @@
1
+ /**
2
+ * WorkspaceController
3
+ *
4
+ * Controller for workspace and project management
5
+ * Handles CRUD operations, file browsing, and file uploads
6
+ */
7
+
8
+ import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
9
+ import * as fs from "fs/promises";
10
+ import * as path from "path";
11
+ import { Readable } from "stream";
12
+ import { getStoreLattice } from "@axiom-lattice/core";
13
+ import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
14
+ import { getSandBoxManager } from "@axiom-lattice/core";
15
+ import { sandboxService } from "../services/sandbox_service";
16
+ import { v4 as uuidv4 } from "uuid";
17
+ import type {
18
+ CreateWorkspaceRequest,
19
+ UpdateWorkspaceRequest,
20
+ CreateProjectRequest,
21
+ UpdateProjectRequest,
22
+ Workspace,
23
+ Project,
24
+ } from "@axiom-lattice/protocols";
25
+
26
+ interface WorkspaceParams {
27
+ workspaceId: string;
28
+ }
29
+
30
+ interface ProjectParams {
31
+ workspaceId: string;
32
+ projectId: string;
33
+ }
34
+
35
+ interface ListPathQuery {
36
+ path?: string;
37
+ }
38
+
39
+ interface ReadFileQuery {
40
+ path: string;
41
+ offset?: number;
42
+ limit?: number;
43
+ }
44
+
45
+ export class WorkspaceController {
46
+ private workspaceStore;
47
+ private projectStore;
48
+
49
+ constructor() {
50
+ this.workspaceStore = getStoreLattice("default", "workspace").store;
51
+ this.projectStore = getStoreLattice("default", "project").store;
52
+ }
53
+
54
+ private getTenantId(request: FastifyRequest): string {
55
+ return (request.headers["x-tenant-id"] as string) || "default";
56
+ }
57
+
58
+ // ==================== Workspace CRUD ====================
59
+
60
+ async listWorkspaces(request: FastifyRequest, reply: FastifyReply) {
61
+ const tenantId = this.getTenantId(request);
62
+ const workspaces = await this.workspaceStore.getAllWorkspaces(tenantId);
63
+ return { success: true, data: workspaces };
64
+ }
65
+
66
+ async createWorkspace(
67
+ request: FastifyRequest<{ Body: CreateWorkspaceRequest }>,
68
+ reply: FastifyReply
69
+ ) {
70
+ const tenantId = this.getTenantId(request);
71
+ const data = request.body;
72
+ const id = uuidv4();
73
+
74
+ const workspace = await this.workspaceStore.createWorkspace(
75
+ tenantId,
76
+ id,
77
+ data
78
+ );
79
+
80
+ return reply.status(201).send({ success: true, data: workspace });
81
+ }
82
+
83
+ async getWorkspace(
84
+ request: FastifyRequest<{ Params: WorkspaceParams }>,
85
+ reply: FastifyReply
86
+ ) {
87
+ const tenantId = this.getTenantId(request);
88
+ const { workspaceId } = request.params;
89
+
90
+ const workspace = await this.workspaceStore.getWorkspaceById(
91
+ tenantId,
92
+ workspaceId
93
+ );
94
+
95
+ if (!workspace) {
96
+ return reply.status(404).send({ success: false, error: "Workspace not found" });
97
+ }
98
+
99
+ return { success: true, data: workspace };
100
+ }
101
+
102
+ async updateWorkspace(
103
+ request: FastifyRequest<{
104
+ Params: WorkspaceParams;
105
+ Body: UpdateWorkspaceRequest;
106
+ }>,
107
+ reply: FastifyReply
108
+ ) {
109
+ const tenantId = this.getTenantId(request);
110
+ const { workspaceId } = request.params;
111
+ const updates = request.body;
112
+
113
+ const workspace = await this.workspaceStore.updateWorkspace(
114
+ tenantId,
115
+ workspaceId,
116
+ updates
117
+ );
118
+
119
+ if (!workspace) {
120
+ return reply.status(404).send({ success: false, error: "Workspace not found" });
121
+ }
122
+
123
+ return { success: true, data: workspace };
124
+ }
125
+
126
+ async deleteWorkspace(
127
+ request: FastifyRequest<{ Params: WorkspaceParams }>,
128
+ reply: FastifyReply
129
+ ) {
130
+ const tenantId = this.getTenantId(request);
131
+ const { workspaceId } = request.params;
132
+
133
+ const deleted = await this.workspaceStore.deleteWorkspace(
134
+ tenantId,
135
+ workspaceId
136
+ );
137
+
138
+ if (!deleted) {
139
+ return reply.status(404).send({ success: false, error: "Workspace not found" });
140
+ }
141
+
142
+ return { success: true };
143
+ }
144
+
145
+ // ==================== Project CRUD ====================
146
+
147
+ async listProjects(
148
+ request: FastifyRequest<{ Params: WorkspaceParams }>,
149
+ reply: FastifyReply
150
+ ) {
151
+ const tenantId = this.getTenantId(request);
152
+ const { workspaceId } = request.params;
153
+
154
+ const projects = await this.projectStore.getProjectsByWorkspace(
155
+ tenantId,
156
+ workspaceId
157
+ );
158
+
159
+ return { success: true, data: projects };
160
+ }
161
+
162
+ async createProject(
163
+ request: FastifyRequest<{
164
+ Params: WorkspaceParams;
165
+ Body: CreateProjectRequest;
166
+ }>,
167
+ reply: FastifyReply
168
+ ) {
169
+ const tenantId = this.getTenantId(request);
170
+ const { workspaceId } = request.params;
171
+ const data = request.body;
172
+ const id = uuidv4();
173
+
174
+ const project = await this.projectStore.createProject(
175
+ tenantId,
176
+ workspaceId,
177
+ id,
178
+ data
179
+ );
180
+
181
+ return reply.status(201).send({ success: true, data: project });
182
+ }
183
+
184
+ async getProject(
185
+ request: FastifyRequest<{ Params: ProjectParams }>,
186
+ reply: FastifyReply
187
+ ) {
188
+ const tenantId = this.getTenantId(request);
189
+ const { projectId } = request.params;
190
+
191
+ const project = await this.projectStore.getProjectById(tenantId, projectId);
192
+
193
+ if (!project) {
194
+ return reply.status(404).send({ success: false, error: "Project not found" });
195
+ }
196
+
197
+ return { success: true, data: project };
198
+ }
199
+
200
+ async updateProject(
201
+ request: FastifyRequest<{
202
+ Params: ProjectParams;
203
+ Body: UpdateProjectRequest;
204
+ }>,
205
+ reply: FastifyReply
206
+ ) {
207
+ const tenantId = this.getTenantId(request);
208
+ const { projectId } = request.params;
209
+ const updates = request.body;
210
+
211
+ const project = await this.projectStore.updateProject(
212
+ tenantId,
213
+ projectId,
214
+ updates
215
+ );
216
+
217
+ if (!project) {
218
+ return reply.status(404).send({ success: false, error: "Project not found" });
219
+ }
220
+
221
+ return { success: true, data: project };
222
+ }
223
+
224
+ async deleteProject(
225
+ request: FastifyRequest<{ Params: ProjectParams }>,
226
+ reply: FastifyReply
227
+ ) {
228
+ const tenantId = this.getTenantId(request);
229
+ const { projectId } = request.params;
230
+
231
+ const deleted = await this.projectStore.deleteProject(tenantId, projectId);
232
+
233
+ if (!deleted) {
234
+ return reply.status(404).send({ success: false, error: "Project not found" });
235
+ }
236
+
237
+ return { success: true };
238
+ }
239
+
240
+ // ==================== File Operations ====================
241
+
242
+ private async getBackend(workspaceId: string, projectId: string) {
243
+ const tenantId = "default";
244
+ const workspace = await this.workspaceStore.getWorkspaceById(
245
+ tenantId,
246
+ workspaceId
247
+ );
248
+
249
+ if (!workspace) {
250
+ throw new Error("Workspace not found");
251
+ }
252
+
253
+ if (workspace.storageType === "sandbox") {
254
+ const sandboxManager = getSandBoxManager("default");
255
+ const sandboxName = "global";
256
+ const sandbox = await sandboxManager.createSandbox(sandboxName);
257
+ return { backend: new SandboxFilesystem({
258
+ sandboxInstance: sandbox,
259
+ workingDirectory: `/workspaces/${workspaceId}/${projectId}`,
260
+ }), workspace };
261
+ } else {
262
+ return { backend: new FilesystemBackend({
263
+ rootDir: `/lattice_store/workspaces/${workspaceId}/${projectId}`,
264
+ virtualMode: true,
265
+ }), workspace };
266
+ }
267
+ }
268
+
269
+ private getFilenameFromPath(filePath: string): string {
270
+ const segments = filePath.split("/");
271
+ return segments[segments.length - 1] || "download";
272
+ }
273
+
274
+ private getMimeType(filename: string): string {
275
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
276
+ const mimeTypes: Record<string, string> = {
277
+ pdf: "application/pdf",
278
+ csv: "text/csv",
279
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
280
+ xls: "application/vnd.ms-excel",
281
+ txt: "text/plain",
282
+ json: "application/json",
283
+ png: "image/png",
284
+ jpg: "image/jpeg",
285
+ jpeg: "image/jpeg",
286
+ gif: "image/gif",
287
+ svg: "image/svg+xml",
288
+ html: "text/html",
289
+ htm: "text/html",
290
+ mp3: "audio/mpeg",
291
+ mp4: "video/mp4",
292
+ webm: "video/webm",
293
+ };
294
+ return mimeTypes[ext] || "application/octet-stream";
295
+ }
296
+
297
+ async downloadFile(
298
+ request: FastifyRequest<{
299
+ Params: ProjectParams;
300
+ Querystring: { path: string };
301
+ }>,
302
+ reply: FastifyReply
303
+ ) {
304
+ const { workspaceId, projectId } = request.params;
305
+ const filePath = request.query.path;
306
+
307
+ if (!filePath) {
308
+ return reply.status(400).send({ success: false, error: "Path is required" });
309
+ }
310
+
311
+ try {
312
+ const { workspace } = await this.getBackend(workspaceId, projectId);
313
+ const resolvedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
314
+
315
+ if (workspace.storageType === "sandbox") {
316
+ const sandboxManager = getSandBoxManager("default");
317
+ const sandbox = await sandboxManager.createSandbox("global");
318
+ const realPath = path.join("/home/gem/workspaces", workspaceId, projectId, resolvedPath);
319
+ const filename = this.getFilenameFromPath(resolvedPath);
320
+ const inferredContentType = this.getMimeType(filename);
321
+
322
+ const downloadResult = await sandbox.file.downloadFile({
323
+ path: realPath,
324
+ });
325
+
326
+ if (!downloadResult.ok) {
327
+ return reply.status(502).send({
328
+ success: false,
329
+ error: `Download error: ${JSON.stringify(downloadResult.error)}`,
330
+ });
331
+ }
332
+
333
+ const body = downloadResult.body as unknown as {
334
+ stream?: () => ReadableStream<Uint8Array>;
335
+ contentType?: string;
336
+ contentDisposition?: string;
337
+ };
338
+
339
+ if (typeof body?.stream === "function") {
340
+ const webStream = body.stream();
341
+ const nodeStream = Readable.fromWeb(webStream);
342
+ const contentType = body.contentType ?? inferredContentType;
343
+ const contentDisposition =
344
+ body.contentDisposition ?? `inline; filename*=UTF-8''${encodeURIComponent(filename)}`;
345
+ return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(nodeStream);
346
+ }
347
+
348
+ const bodyUnknown = downloadResult.body as unknown;
349
+ let buf: Buffer;
350
+ let contentType = inferredContentType;
351
+ let contentDisposition = `inline; filename*=UTF-8''${encodeURIComponent(filename)}`;
352
+
353
+ if (bodyUnknown instanceof ArrayBuffer) {
354
+ buf = Buffer.from(bodyUnknown);
355
+ } else if (bodyUnknown instanceof Buffer) {
356
+ buf = bodyUnknown;
357
+ } else if (
358
+ bodyUnknown &&
359
+ typeof (bodyUnknown as { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer === "function"
360
+ ) {
361
+ const res = bodyUnknown as { arrayBuffer: () => Promise<ArrayBuffer>; headers?: Headers };
362
+ buf = Buffer.from(await res.arrayBuffer());
363
+ if (res.headers?.get("content-type")) contentType = res.headers.get("content-type")!;
364
+ if (res.headers?.get("content-disposition")) contentDisposition = res.headers.get("content-disposition")!;
365
+ } else if (bodyUnknown && typeof (bodyUnknown as { blob?: () => Promise<Blob> }).blob === "function") {
366
+ const blob = await (bodyUnknown as { blob: () => Promise<Blob> }).blob();
367
+ buf = Buffer.from(await blob.arrayBuffer());
368
+ } else {
369
+ return reply.status(502).send({ success: false, error: "Unexpected download response format" });
370
+ }
371
+
372
+ return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(buf);
373
+ }
374
+
375
+ const { backend } = await this.getBackend(workspaceId, projectId);
376
+ const content = await backend.read(resolvedPath, 0, Infinity);
377
+ const filename = this.getFilenameFromPath(resolvedPath);
378
+ const mimeType = this.getMimeType(filename);
379
+ const buffer = Buffer.from(content, "utf-8");
380
+
381
+ return reply
382
+ .status(200)
383
+ .type(mimeType)
384
+ .header("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(filename)}`)
385
+ .send(buffer);
386
+ } catch (error: unknown) {
387
+ const message = error instanceof Error ? error.message : String(error);
388
+ return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
389
+ }
390
+ }
391
+
392
+ async listPath(
393
+ request: FastifyRequest<{
394
+ Params: ProjectParams;
395
+ Querystring: ListPathQuery;
396
+ }>
397
+ ) {
398
+ const { workspaceId, projectId } = request.params;
399
+ const path = (request.query.path as string) || "/";
400
+
401
+ const { backend } = await this.getBackend(workspaceId, projectId);
402
+ const files = await backend.lsInfo(path);
403
+
404
+ return { success: true, data: files };
405
+ }
406
+
407
+ async readFile(
408
+ request: FastifyRequest<{
409
+ Params: ProjectParams;
410
+ Querystring: ReadFileQuery;
411
+ }>
412
+ ) {
413
+ const { workspaceId, projectId } = request.params;
414
+ const { path, offset = 0, limit = 1000 } = request.query;
415
+
416
+ const { backend } = await this.getBackend(workspaceId, projectId);
417
+ const content = await backend.read(path, Number(offset), Number(limit));
418
+
419
+ return { success: true, data: { content, offset, limit } };
420
+ }
421
+
422
+ /**
423
+ * Upload a file to the workspace project storage.
424
+ *
425
+ * Route: POST /api/workspaces/:workspaceId/projects/:projectId/uploadfile
426
+ * Accepts multipart/form-data with:
427
+ * - `file`: the file to upload (required)
428
+ * - `path`: optional directory path relative to project root (e.g., "docs", "src/utils")
429
+ * For sandbox storage, delegates to the sandbox upload API.
430
+ * For filesystem storage, writes directly under /lattice_store/workspaces/{workspaceId}/{projectId}.
431
+ */
432
+ async uploadFile(
433
+ request: FastifyRequest<{
434
+ Params: ProjectParams;
435
+ }>,
436
+ reply: FastifyReply
437
+ ) {
438
+ const tenantId = this.getTenantId(request);
439
+ const { workspaceId, projectId } = request.params;
440
+
441
+ const workspace = await this.workspaceStore.getWorkspaceById(
442
+ tenantId,
443
+ workspaceId
444
+ );
445
+
446
+ if (!workspace) {
447
+ return reply
448
+ .status(404)
449
+ .send({ success: false, error: "Workspace not found" });
450
+ }
451
+
452
+ try {
453
+ const data = await (request as any).file();
454
+ if (!data) {
455
+ return reply
456
+ .status(400)
457
+ .send({ success: false, error: "No file in request" });
458
+ }
459
+
460
+ const buffer: Buffer = await data.toBuffer();
461
+ const filename: string = data.filename || "file";
462
+
463
+ const pathEntry = data.fields?.path;
464
+ const pathValue =
465
+ pathEntry && typeof pathEntry === "object" && "value" in pathEntry
466
+ ? String((pathEntry as { value: unknown }).value)
467
+ : typeof pathEntry === "string"
468
+ ? pathEntry
469
+ : undefined;
470
+
471
+ if (pathValue && !/^[a-zA-Z0-9_./-]+$/.test(pathValue)) {
472
+ return reply
473
+ .status(400)
474
+ .send({ success: false, error: "Invalid path parameter" });
475
+ }
476
+
477
+ if (workspace.storageType === "sandbox") {
478
+ const sandboxManager = getSandBoxManager("default");
479
+ const sandboxName = "global";
480
+ const sandbox = await sandboxManager.createSandbox(sandboxName);
481
+
482
+ const baseDir = path.join("/home/gem/workspaces", workspaceId, projectId);
483
+ const realPath = pathValue
484
+ ? path.join(baseDir, pathValue, filename)
485
+ : path.join(baseDir, filename);
486
+
487
+ const uploadResult = await sandbox.file.uploadFile({
488
+ file: buffer,
489
+ path: realPath,
490
+ }, { timeoutInSeconds: 300 });
491
+
492
+ if (!uploadResult.ok) {
493
+ return reply.status(502).send({
494
+ success: false,
495
+ error: `Upload error: ${JSON.stringify(uploadResult.error)}`,
496
+ });
497
+ }
498
+
499
+ const relativePath =
500
+ uploadResult.body?.data?.file_path?.replace(path.join("/home/gem/workspaces", workspaceId, projectId), "") ||
501
+ (pathValue ? `/${pathValue}/${filename}` : `/${filename}`);
502
+
503
+ const result = {
504
+ path: relativePath,
505
+ name: filename,
506
+ is_dir: false,
507
+ size: buffer.length,
508
+ modified_at: new Date().toISOString(),
509
+ };
510
+
511
+ return reply.status(200).send({ success: true, data: result });
512
+ }
513
+
514
+ // Filesystem storage: write under /lattice_store/workspaces/{workspaceId}/{projectId} and expose as virtual path "/<filename>"
515
+ const rootDir = `/lattice_store/workspaces/${workspaceId}/${projectId}`;
516
+ const targetDir = pathValue ? path.join(rootDir, pathValue) : rootDir;
517
+ const targetPath = path.join(targetDir, filename);
518
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
519
+ await fs.writeFile(targetPath, buffer);
520
+ const stat = await fs.stat(targetPath);
521
+
522
+ const result = {
523
+ path: pathValue ? `/${pathValue}/${filename}` : `/${filename}`,
524
+ name: filename,
525
+ is_dir: false,
526
+ size: stat.size,
527
+ modified_at: stat.mtime.toISOString(),
528
+ };
529
+
530
+ return reply.status(200).send({ success: true, data: result });
531
+ } catch (error: any) {
532
+ return reply.status(500).send({
533
+ success: false,
534
+ error: `Upload handler error: ${error?.message || String(error)}`,
535
+ });
536
+ }
537
+ }
538
+ }
539
+
540
+ export function registerWorkspaceRoutes(app: FastifyInstance) {
541
+ const controller = new WorkspaceController();
542
+
543
+ // Workspace routes
544
+ app.get("/api/workspaces", controller.listWorkspaces.bind(controller));
545
+ app.post("/api/workspaces", controller.createWorkspace.bind(controller));
546
+ app.get(
547
+ "/api/workspaces/:workspaceId",
548
+ controller.getWorkspace.bind(controller)
549
+ );
550
+ app.patch(
551
+ "/api/workspaces/:workspaceId",
552
+ controller.updateWorkspace.bind(controller)
553
+ );
554
+ app.delete(
555
+ "/api/workspaces/:workspaceId",
556
+ controller.deleteWorkspace.bind(controller)
557
+ );
558
+
559
+ // Project routes
560
+ app.get(
561
+ "/api/workspaces/:workspaceId/projects",
562
+ controller.listProjects.bind(controller)
563
+ );
564
+ app.post(
565
+ "/api/workspaces/:workspaceId/projects",
566
+ controller.createProject.bind(controller)
567
+ );
568
+ app.get(
569
+ "/api/workspaces/:workspaceId/projects/:projectId",
570
+ controller.getProject.bind(controller)
571
+ );
572
+ app.patch(
573
+ "/api/workspaces/:workspaceId/projects/:projectId",
574
+ controller.updateProject.bind(controller)
575
+ );
576
+ app.delete(
577
+ "/api/workspaces/:workspaceId/projects/:projectId",
578
+ controller.deleteProject.bind(controller)
579
+ );
580
+
581
+ // File operations
582
+ app.get(
583
+ "/api/workspaces/:workspaceId/projects/:projectId/listpath",
584
+ controller.listPath.bind(controller)
585
+ );
586
+ app.get(
587
+ "/api/workspaces/:workspaceId/projects/:projectId/readfile",
588
+ controller.readFile.bind(controller)
589
+ );
590
+ app.post(
591
+ "/api/workspaces/:workspaceId/projects/:projectId/uploadfile",
592
+ controller.uploadFile.bind(controller)
593
+ );
594
+ app.get(
595
+ "/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
596
+ controller.downloadFile.bind(controller)
597
+ );
598
+ }
package/src/index.ts CHANGED
@@ -108,16 +108,8 @@ app.addHook("onResponse", (request, reply, done) => {
108
108
  // cors
109
109
  app.register(cors, {
110
110
  origin: true,
111
- methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
112
- allowedHeaders: [
113
- "Content-Type",
114
- "Authorization",
115
- "X-Requested-With",
116
- "x-tenant-id",
117
- "x-request-id",
118
- "x-assistant-id",
119
- "x-thread-id",
120
- ],
111
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
112
+ allowedHeaders: "*",
121
113
  exposedHeaders: ["Content-Type"],
122
114
  credentials: true,
123
115
  });
@@ -29,6 +29,12 @@ import {
29
29
  getSandboxUrlSchema,
30
30
  } from "../schemas";
31
31
  import { registerSandboxProxyRoutes } from "../controllers/sandbox";
32
+ import { registerWorkspaceRoutes } from "../controllers/workspace";
33
+ import { registerDatabaseConfigRoutes } from "../controllers/database-configs";
34
+ import { registerMetricsServerConfigRoutes } from "../controllers/metrics-configs";
35
+ import { registerUserRoutes } from "../controllers/users";
36
+ import { registerTenantRoutes } from "../controllers/tenants";
37
+ import { registerAuthRoutes } from "../controllers/auth";
32
38
 
33
39
  export const registerLatticeRoutes = (app: FastifyInstance): void => {
34
40
  // 运行路由
@@ -293,4 +299,19 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
293
299
  );
294
300
 
295
301
  registerSandboxProxyRoutes(app);
302
+
303
+ registerWorkspaceRoutes(app);
304
+
305
+ registerDatabaseConfigRoutes(app);
306
+
307
+ registerMetricsServerConfigRoutes(app);
308
+
309
+ registerUserRoutes(app);
310
+
311
+ registerTenantRoutes(app);
312
+
313
+ registerAuthRoutes(app, {
314
+ autoApproveUsers: process.env.AUTO_APPROVE_USERS !== "false",
315
+ allowTenantRegistration: process.env.ALLOW_TENANT_REGISTRATION !== "false",
316
+ });
296
317
  };