@axiom-lattice/gateway 3.0.2 → 3.0.4

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 (155) hide show
  1. package/dist/index.js +37 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +37 -9
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/.eslintrc.json +0 -22
  7. package/.turbo/turbo-build.log +0 -46
  8. package/AGENTS.md +0 -112
  9. package/CHANGELOG.md +0 -1190
  10. package/LOGGER_CONFIG.md +0 -135
  11. package/RESUME_STREAM_CONTENT_BASED.md +0 -326
  12. package/RESUME_STREAM_README.md +0 -388
  13. package/jest.config.js +0 -72
  14. package/public/sdk/README.md +0 -695
  15. package/public/sdk/dashboard-engine-skill.md +0 -1122
  16. package/public/sdk/dashboard-single-file-spec.md +0 -357
  17. package/public/sdk/data-query-sdk-skill.md +0 -307
  18. package/public/sdk/data-query-sdk.d.ts +0 -252
  19. package/public/sdk/data-query-sdk.js +0 -970
  20. package/public/sdk/occupancy-dashboard.html +0 -363
  21. package/public/sdk/test-dashboard.html +0 -690
  22. package/scripts/seed-lark-installation.ts +0 -54
  23. package/src/__tests__/__mocks__/e2b.ts +0 -1
  24. package/src/__tests__/a2a.test.ts +0 -428
  25. package/src/__tests__/channel-installations.test.ts +0 -199
  26. package/src/__tests__/data-query.test.ts +0 -77
  27. package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
  28. package/src/__tests__/setup.ts +0 -5
  29. package/src/__tests__/workspace.test.ts +0 -400
  30. package/src/channels/__tests__/routes.test.ts +0 -50
  31. package/src/channels/lark/LarkChannelAdapter.ts +0 -217
  32. package/src/channels/lark/README.md +0 -187
  33. package/src/channels/lark/__tests__/controller.test.ts +0 -136
  34. package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
  35. package/src/channels/lark/__tests__/parser.test.ts +0 -72
  36. package/src/channels/lark/__tests__/sender.test.ts +0 -37
  37. package/src/channels/lark/__tests__/verification.test.ts +0 -157
  38. package/src/channels/lark/controller.ts +0 -48
  39. package/src/channels/lark/mapping-service.ts +0 -138
  40. package/src/channels/lark/parser.ts +0 -68
  41. package/src/channels/lark/routes.ts +0 -22
  42. package/src/channels/lark/sender.ts +0 -58
  43. package/src/channels/lark/types.ts +0 -20
  44. package/src/channels/lark/verification.ts +0 -32
  45. package/src/channels/registry.ts +0 -20
  46. package/src/channels/routes.ts +0 -30
  47. package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
  48. package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
  49. package/src/channels/wechat/context-store.ts +0 -54
  50. package/src/channels/wechat/controller.ts +0 -59
  51. package/src/channels/wechat/routes.ts +0 -7
  52. package/src/channels/wechat/types.ts +0 -62
  53. package/src/channels/wechat/wechat-client.ts +0 -162
  54. package/src/config/local-a2a-templates.ts +0 -59
  55. package/src/config.ts +0 -124
  56. package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
  57. package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
  58. package/src/controllers/__tests__/mime.test.ts +0 -74
  59. package/src/controllers/__tests__/resources.test.ts +0 -122
  60. package/src/controllers/__tests__/run.test.ts +0 -115
  61. package/src/controllers/__tests__/tasks.test.ts +0 -215
  62. package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
  63. package/src/controllers/agent_task.ts +0 -72
  64. package/src/controllers/assistant.ts +0 -490
  65. package/src/controllers/auth.ts +0 -524
  66. package/src/controllers/channel-bindings.ts +0 -135
  67. package/src/controllers/channel-installations.ts +0 -409
  68. package/src/controllers/collections.ts +0 -176
  69. package/src/controllers/config.ts +0 -126
  70. package/src/controllers/connections.ts +0 -181
  71. package/src/controllers/data-query.ts +0 -236
  72. package/src/controllers/database-configs.ts +0 -440
  73. package/src/controllers/embeddings.ts +0 -22
  74. package/src/controllers/entries.ts +0 -167
  75. package/src/controllers/eval.ts +0 -513
  76. package/src/controllers/export-import.ts +0 -166
  77. package/src/controllers/health.ts +0 -36
  78. package/src/controllers/local-a2a-templates.ts +0 -18
  79. package/src/controllers/local-a2a.ts +0 -149
  80. package/src/controllers/mcp-configs.ts +0 -673
  81. package/src/controllers/memory.ts +0 -257
  82. package/src/controllers/menu-items.ts +0 -114
  83. package/src/controllers/metrics-configs.ts +0 -1061
  84. package/src/controllers/middleware-types.ts +0 -15
  85. package/src/controllers/models.ts +0 -199
  86. package/src/controllers/personal-assistant.ts +0 -191
  87. package/src/controllers/resources.ts +0 -477
  88. package/src/controllers/run.ts +0 -338
  89. package/src/controllers/sandbox.ts +0 -255
  90. package/src/controllers/schedules.ts +0 -336
  91. package/src/controllers/skills.ts +0 -256
  92. package/src/controllers/tasks.ts +0 -213
  93. package/src/controllers/tenants.ts +0 -121
  94. package/src/controllers/thread_status.ts +0 -66
  95. package/src/controllers/threads.ts +0 -260
  96. package/src/controllers/tools.ts +0 -139
  97. package/src/controllers/users.ts +0 -135
  98. package/src/controllers/workflow-tracking.ts +0 -693
  99. package/src/controllers/workspace.ts +0 -879
  100. package/src/export_registrations/a2a-api-key.registration.ts +0 -89
  101. package/src/export_registrations/agent.registration.ts +0 -80
  102. package/src/export_registrations/binding.registration.ts +0 -106
  103. package/src/export_registrations/channel-installation.registration.ts +0 -88
  104. package/src/export_registrations/collection.registration.ts +0 -85
  105. package/src/export_registrations/connection.registration.ts +0 -97
  106. package/src/export_registrations/database-config.registration.ts +0 -94
  107. package/src/export_registrations/eval.registration.ts +0 -334
  108. package/src/export_registrations/index.ts +0 -31
  109. package/src/export_registrations/mcp-config.registration.ts +0 -97
  110. package/src/export_registrations/menu.registration.ts +0 -91
  111. package/src/export_registrations/metrics-config.registration.ts +0 -94
  112. package/src/export_registrations/skill.registration.ts +0 -88
  113. package/src/export_registrations/task.registration.ts +0 -99
  114. package/src/index.ts +0 -497
  115. package/src/logger/Logger.ts +0 -186
  116. package/src/router/MessageContext.ts +0 -14
  117. package/src/router/MessageRouter.ts +0 -490
  118. package/src/router/middlewares/auditLogger.ts +0 -34
  119. package/src/router/middlewares/deduplication.ts +0 -38
  120. package/src/router/middlewares/index.ts +0 -3
  121. package/src/router/middlewares/rateLimit.ts +0 -46
  122. package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
  123. package/src/routes/a2a-bridge.ts +0 -266
  124. package/src/routes/a2a-test-connection.ts +0 -126
  125. package/src/routes/a2a.ts +0 -851
  126. package/src/routes/channel-bindings.ts +0 -11
  127. package/src/routes/channel-installations.ts +0 -33
  128. package/src/routes/index.ts +0 -640
  129. package/src/routes/menu-items.ts +0 -10
  130. package/src/routes/resource-routes.ts +0 -35
  131. package/src/routes/stt.ts +0 -102
  132. package/src/schemas/data-query.ts +0 -69
  133. package/src/schemas/index.ts +0 -852
  134. package/src/services/ExportImportService.ts +0 -296
  135. package/src/services/__tests__/ExportImportService.test.ts +0 -130
  136. package/src/services/__tests__/eval-runner.test.ts +0 -131
  137. package/src/services/__tests__/local-a2a.test.ts +0 -124
  138. package/src/services/agent_task_consumer.ts +0 -348
  139. package/src/services/collection_service.ts +0 -258
  140. package/src/services/eval-runner.ts +0 -283
  141. package/src/services/local-a2a.ts +0 -354
  142. package/src/services/queue_service.ts +0 -104
  143. package/src/services/sandbox_service.ts +0 -223
  144. package/src/services/skill_service.ts +0 -97
  145. package/src/services/supabase.ts +0 -60
  146. package/src/swagger.ts +0 -77
  147. package/src/types/index.ts +0 -118
  148. package/src/types/koa.d.ts +0 -7
  149. package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
  150. package/src/utils/__tests__/path-containment.test.ts +0 -35
  151. package/src/utils/a2a-url.ts +0 -20
  152. package/src/utils/local-a2a-runtime.ts +0 -107
  153. package/src/utils/mime.ts +0 -44
  154. package/src/utils/path-containment.ts +0 -38
  155. package/tsconfig.json +0 -26
@@ -1,879 +0,0 @@
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 { constants as fsConstants } from "fs";
12
- import * as os from "os";
13
- import { getStoreLattice } from "@axiom-lattice/core";
14
- import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
15
- import { getSandBoxManager } from "@axiom-lattice/core";
16
- import { v4 as uuidv4 } from "uuid";
17
- import { getContentTypeFromFilename, getFilenameFromPath } from "../utils/mime";
18
- import {
19
- isPathWithinRoot,
20
- PathOutsideRootError,
21
- resolvePathWithinRoot,
22
- } from "../utils/path-containment";
23
- import type {
24
- CreateWorkspaceRequest,
25
- UpdateWorkspaceRequest,
26
- CreateProjectRequest,
27
- UpdateProjectRequest,
28
- Workspace,
29
- Project,
30
- } from "@axiom-lattice/protocols";
31
-
32
- interface WorkspaceParams {
33
- workspaceId: string;
34
- }
35
-
36
- interface ProjectParams {
37
- workspaceId: string;
38
- projectId: string;
39
- }
40
-
41
- interface ListPathQuery {
42
- path?: string;
43
- assistantId?: string;
44
- }
45
-
46
- interface ReadFileQuery {
47
- path: string;
48
- offset?: number;
49
- limit?: number;
50
- assistantId?: string;
51
- }
52
-
53
- export class WorkspaceController {
54
- private workspaceStore;
55
- private projectStore;
56
-
57
- constructor() {
58
- this.workspaceStore = getStoreLattice("default", "workspace").store;
59
- this.projectStore = getStoreLattice("default", "project").store;
60
- }
61
-
62
- private getTenantId(request: FastifyRequest): string {
63
- // First try to get from authenticated user context
64
- const userTenantId = (request as any).user?.tenantId;
65
- if (userTenantId) {
66
- return userTenantId;
67
- }
68
-
69
- // Try to get from query parameters (for direct URL access like file downloads)
70
- const queryTenantId = (request.query as any)?.tenantId;
71
- if (queryTenantId) {
72
- return String(queryTenantId);
73
- }
74
-
75
- // Fallback to request headers for backward compatibility
76
- return (request.headers["x-tenant-id"] as string) || "default";
77
- }
78
-
79
- // ==================== Workspace CRUD ====================
80
-
81
- async listWorkspaces(request: FastifyRequest, reply: FastifyReply) {
82
- const tenantId = this.getTenantId(request);
83
- const workspaces = await this.workspaceStore.getAllWorkspaces(tenantId);
84
- return { success: true, data: workspaces };
85
- }
86
-
87
- async createWorkspace(
88
- request: FastifyRequest<{ Body: CreateWorkspaceRequest }>,
89
- reply: FastifyReply
90
- ) {
91
- const tenantId = this.getTenantId(request);
92
- const data = request.body;
93
- const id = uuidv4();
94
-
95
- const workspace = await this.workspaceStore.createWorkspace(
96
- tenantId,
97
- id,
98
- data
99
- );
100
-
101
- return reply.status(201).send({ success: true, data: workspace });
102
- }
103
-
104
- async getWorkspace(
105
- request: FastifyRequest<{ Params: WorkspaceParams }>,
106
- reply: FastifyReply
107
- ) {
108
- const tenantId = this.getTenantId(request);
109
- const { workspaceId } = request.params;
110
-
111
- const workspace = await this.workspaceStore.getWorkspaceById(
112
- tenantId,
113
- workspaceId
114
- );
115
-
116
- if (!workspace) {
117
- return reply.status(404).send({ success: false, error: "Workspace not found" });
118
- }
119
-
120
- return { success: true, data: workspace };
121
- }
122
-
123
- async updateWorkspace(
124
- request: FastifyRequest<{
125
- Params: WorkspaceParams;
126
- Body: UpdateWorkspaceRequest;
127
- }>,
128
- reply: FastifyReply
129
- ) {
130
- const tenantId = this.getTenantId(request);
131
- const { workspaceId } = request.params;
132
- const updates = request.body;
133
-
134
- const workspace = await this.workspaceStore.updateWorkspace(
135
- tenantId,
136
- workspaceId,
137
- updates
138
- );
139
-
140
- if (!workspace) {
141
- return reply.status(404).send({ success: false, error: "Workspace not found" });
142
- }
143
-
144
- return { success: true, data: workspace };
145
- }
146
-
147
- async deleteWorkspace(
148
- request: FastifyRequest<{ Params: WorkspaceParams }>,
149
- reply: FastifyReply
150
- ) {
151
- const tenantId = this.getTenantId(request);
152
- const { workspaceId } = request.params;
153
-
154
- const deleted = await this.workspaceStore.deleteWorkspace(
155
- tenantId,
156
- workspaceId
157
- );
158
-
159
- if (!deleted) {
160
- return reply.status(404).send({ success: false, error: "Workspace not found" });
161
- }
162
-
163
- return { success: true };
164
- }
165
-
166
- // ==================== Project CRUD ====================
167
-
168
- async listProjects(
169
- request: FastifyRequest<{ Params: WorkspaceParams }>,
170
- reply: FastifyReply
171
- ) {
172
- const tenantId = this.getTenantId(request);
173
- const { workspaceId } = request.params;
174
-
175
- const projects = await this.projectStore.getProjectsByWorkspace(
176
- tenantId,
177
- workspaceId
178
- );
179
-
180
- return { success: true, data: projects };
181
- }
182
-
183
- async createProject(
184
- request: FastifyRequest<{
185
- Params: WorkspaceParams;
186
- Body: CreateProjectRequest;
187
- }>,
188
- reply: FastifyReply
189
- ) {
190
- const tenantId = this.getTenantId(request);
191
- const { workspaceId } = request.params;
192
- const data = request.body;
193
- const id = uuidv4();
194
-
195
- const project = await this.projectStore.createProject(
196
- tenantId,
197
- workspaceId,
198
- id,
199
- data
200
- );
201
-
202
- return reply.status(201).send({ success: true, data: project });
203
- }
204
-
205
- async getProject(
206
- request: FastifyRequest<{ Params: ProjectParams }>,
207
- reply: FastifyReply
208
- ) {
209
- const tenantId = this.getTenantId(request);
210
- const { projectId } = request.params;
211
-
212
- const project = await this.projectStore.getProjectById(tenantId, projectId);
213
-
214
- if (!project) {
215
- return reply.status(404).send({ success: false, error: "Project not found" });
216
- }
217
-
218
- return { success: true, data: project };
219
- }
220
-
221
- async updateProject(
222
- request: FastifyRequest<{
223
- Params: ProjectParams;
224
- Body: UpdateProjectRequest;
225
- }>,
226
- reply: FastifyReply
227
- ) {
228
- const tenantId = this.getTenantId(request);
229
- const { projectId } = request.params;
230
- const updates = request.body;
231
-
232
- const project = await this.projectStore.updateProject(
233
- tenantId,
234
- projectId,
235
- updates
236
- );
237
-
238
- if (!project) {
239
- return reply.status(404).send({ success: false, error: "Project not found" });
240
- }
241
-
242
- return { success: true, data: project };
243
- }
244
-
245
- async deleteProject(
246
- request: FastifyRequest<{ Params: ProjectParams }>,
247
- reply: FastifyReply
248
- ) {
249
- const tenantId = this.getTenantId(request);
250
- const { projectId } = request.params;
251
-
252
- const deleted = await this.projectStore.deleteProject(tenantId, projectId);
253
-
254
- if (!deleted) {
255
- return reply.status(404).send({ success: false, error: "Project not found" });
256
- }
257
-
258
- return { success: true };
259
- }
260
-
261
- // ==================== File Operations ====================
262
-
263
- private async getBackend(tenantId: string, workspaceId: string, projectId: string, assistantId?: string, filePath?: string) {
264
- const workspace = await this.workspaceStore.getWorkspaceById(
265
- tenantId,
266
- workspaceId
267
- );
268
-
269
- if (!workspace) {
270
- throw new Error("Workspace not found");
271
- }
272
-
273
- console.log(`[getBackend] storageType=${workspace.storageType} filePath=${filePath}`);
274
-
275
- if (workspace.storageType === "sandbox") {
276
- const sandboxManager = getSandBoxManager();
277
-
278
- const volumeConfig = {
279
- assistant_id: assistantId || "",
280
- thread_id: "",
281
- tenantId,
282
- workspaceId,
283
- projectId,
284
- };
285
-
286
- console.log(`[getBackend] trying volume backend for path=${filePath} assistant_id=${assistantId}`);
287
- const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, filePath || "/project");
288
- if (volumeBackend) {
289
- console.log(`[getBackend] using VolumeFilesystem`);
290
- return { backend: volumeBackend, workspace };
291
- }
292
-
293
- console.log(`[getBackend] volume not found, falling back to SandboxFilesystem`);
294
- const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
295
- console.log(`[getBackend] sandbox acquired, name=${(sandbox as any).name || "unknown"}`);
296
- return {
297
- backend: new SandboxFilesystem({
298
- sandboxInstance: sandbox,
299
- }), workspace
300
- };
301
- }
302
-
303
- // Filesystem storage: use default /lattice_store path
304
- const rootDir = await this.getFilesystemRootDirectory(
305
- tenantId,
306
- workspaceId,
307
- projectId,
308
- );
309
-
310
- console.log(`[getBackend] using FilesystemBackend rootDir=${rootDir}`);
311
- return {
312
- backend: new FilesystemBackend({
313
- rootDir,
314
- virtualMode: true,
315
- }), workspace
316
- };
317
- }
318
-
319
- private async getFilesystemRootDirectory(
320
- tenantId: string,
321
- workspaceId: string,
322
- projectId: string,
323
- ): Promise<string> {
324
- return `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`;
325
- }
326
-
327
- private isBinaryContentType(filename: string): boolean {
328
- const ext = filename.split(".").pop()?.toLowerCase() || "";
329
- const binaryExtensions = new Set([
330
- "pdf", "xlsx", "xls", "docx", "doc", "pptx", "ppt",
331
- "png", "jpg", "jpeg", "gif", "bmp", "ico", "webp",
332
- "mp3", "mp4", "webm", "ogg", "wav", "avi", "mov",
333
- "zip", "tar", "gz", "rar", "7z",
334
- ]);
335
- return binaryExtensions.has(ext);
336
- }
337
-
338
- async downloadFile(
339
- request: FastifyRequest<{
340
- Params: ProjectParams;
341
- Querystring: { path: string; assistantId?: string };
342
- }>,
343
- reply: FastifyReply
344
- ) {
345
- const tenantId = this.getTenantId(request);
346
- const { workspaceId, projectId } = request.params;
347
- const filePath = request.query.path;
348
- const assistantId = request.query.assistantId;
349
-
350
- if (!filePath) {
351
- return reply.status(400).send({ success: false, error: "Path is required" });
352
- }
353
-
354
- try {
355
- const { workspace } = await this.getBackend(tenantId, workspaceId, projectId, assistantId);
356
- const resolvedPath = filePath;
357
-
358
- if (workspace.storageType === "sandbox") {
359
- const sandboxManager = getSandBoxManager();
360
- const volumeConfig = {
361
- assistant_id: assistantId || "",
362
- thread_id: "",
363
- tenantId,
364
- workspaceId,
365
- projectId,
366
- };
367
-
368
- const filename = getFilenameFromPath(resolvedPath);
369
- const isBinary = this.isBinaryContentType(filename);
370
-
371
- // Use volume backend when available; fall back to sandbox
372
- const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, resolvedPath);
373
-
374
- let buf: Buffer;
375
- if (volumeBackend) {
376
- if (isBinary && volumeBackend.readBinary) {
377
- buf = await volumeBackend.readBinary(resolvedPath);
378
- } else {
379
- const fileData = await volumeBackend.readRaw(resolvedPath);
380
- buf = Buffer.from(fileData.content.join("\n"), "utf-8");
381
- }
382
- } else {
383
- const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
384
- buf = await sandbox.file.downloadFile({ file: resolvedPath });
385
- }
386
-
387
- const inferredContentType = getContentTypeFromFilename(filename);
388
-
389
- const contentDisposition = `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
390
- return reply.status(200).type(inferredContentType).header("Content-Disposition", contentDisposition).send(buf);
391
- }
392
-
393
- const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, resolvedPath);
394
- const content = await backend.read(resolvedPath, 0, Infinity);
395
- const filename = getFilenameFromPath(resolvedPath);
396
- const mimeType = getContentTypeFromFilename(filename);
397
- const buffer = Buffer.from(content, "utf-8");
398
-
399
- return reply
400
- .status(200)
401
- .type(mimeType)
402
- .header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`)
403
- .send(buffer);
404
- } catch (error: unknown) {
405
- const message = error instanceof Error ? error.message : String(error);
406
- return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
407
- }
408
- }
409
-
410
- /**
411
- * View a file inline in the browser (not download).
412
- * Returns file content with inline Content-Disposition for preview.
413
- */
414
- async viewFile(
415
- request: FastifyRequest<{
416
- Params: ProjectParams;
417
- Querystring: { path: string; assistantId?: string };
418
- }>,
419
- reply: FastifyReply
420
- ) {
421
- const tenantId = this.getTenantId(request);
422
- const { workspaceId, projectId } = request.params;
423
- const filePath = request.query.path;
424
- const assistantId = request.query.assistantId;
425
-
426
- if (!filePath) {
427
- return reply.status(400).send({ success: false, error: "Path is required" });
428
- }
429
-
430
- try {
431
- const { workspace } = await this.getBackend(tenantId, workspaceId, projectId, assistantId);
432
- const resolvedPath = filePath;
433
-
434
- if (workspace.storageType === "sandbox") {
435
- const sandboxManager = getSandBoxManager();
436
- const volumeConfig = {
437
- assistant_id: assistantId || "",
438
- thread_id: "",
439
- tenantId,
440
- workspaceId,
441
- projectId,
442
- };
443
-
444
- const filename = getFilenameFromPath(resolvedPath);
445
- const isBinary = this.isBinaryContentType(filename);
446
-
447
- // Use volume backend when available; fall back to sandbox
448
- const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, resolvedPath);
449
-
450
- let buf: Buffer;
451
- if (volumeBackend) {
452
- if (isBinary && volumeBackend.readBinary) {
453
- buf = await volumeBackend.readBinary(resolvedPath);
454
- } else {
455
- const fileData = await volumeBackend.readRaw(resolvedPath);
456
- buf = Buffer.from(fileData.content.join("\n"), "utf-8");
457
- }
458
- } else {
459
- const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
460
- buf = await sandbox.file.downloadFile({ file: resolvedPath });
461
- }
462
-
463
- const inferredContentType = getContentTypeFromFilename(filename);
464
-
465
- try {
466
- const contentType = inferredContentType;
467
-
468
- // Inject AI2APP context script for HTML files (sandbox storage)
469
- const isHtml = contentType?.toLowerCase().includes("text/html") ||
470
- filename.toLowerCase().endsWith(".html") ||
471
- filename.toLowerCase().endsWith(".htm");
472
- let outputBuf = buf;
473
- if (isHtml) {
474
- console.log(`[viewFile] Injecting AI2APP context for sandbox HTML file: ${filename}, tenantId: ${tenantId}, contentType: ${contentType}`);
475
- let content = buf.toString("utf-8");
476
- const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
477
- tenantId,
478
- workspaceId,
479
- projectId,
480
- timestamp: Date.now()
481
- })};</script>`;
482
-
483
- if (content.toLowerCase().includes("</head>")) {
484
- content = content.replace(/<\/head>/i, `${contextScript}</head>`);
485
- console.log(`[viewFile] Context script injected before </head>`);
486
- } else if (content.toLowerCase().includes("<html>")) {
487
- content = content.replace(/<html>/i, `<html>${contextScript}`);
488
- console.log(`[viewFile] Context script injected after <html>`);
489
- } else {
490
- content = contextScript + content;
491
- console.log(`[viewFile] Context script prepended to content`);
492
- }
493
- outputBuf = Buffer.from(content, "utf-8");
494
- }
495
-
496
- return reply
497
- .status(200)
498
- .type(contentType)
499
- .header("Content-Disposition", "inline")
500
- .send(outputBuf);
501
- } catch (err) {
502
- return reply.status(502).send({ success: false, error: String(err) });
503
- }
504
- }
505
-
506
- const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, resolvedPath);
507
- const content = await backend.read(resolvedPath, 0, Infinity);
508
- const filename = getFilenameFromPath(resolvedPath);
509
- const mimeType = getContentTypeFromFilename(filename);
510
-
511
- // Inject AI2APP context script for HTML files
512
- let finalContent = content;
513
- const isHtmlFs = mimeType?.toLowerCase().includes("text/html") ||
514
- filename.toLowerCase().endsWith(".html") ||
515
- filename.toLowerCase().endsWith(".htm");
516
- if (isHtmlFs) {
517
- console.log(`[viewFile] Injecting AI2APP context for filesystem HTML file: ${filename}, tenantId: ${tenantId}, mimeType: ${mimeType}`);
518
- const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
519
- tenantId,
520
- workspaceId,
521
- projectId,
522
- timestamp: Date.now()
523
- })};</script>`;
524
- // Insert before </head> or </html>
525
- if (content.toLowerCase().includes("</head>")) {
526
- finalContent = content.replace(/<\/head>/i, `${contextScript}</head>`);
527
- console.log(`[viewFile] Context script injected before </head>`);
528
- } else if (content.toLowerCase().includes("</html>")) {
529
- finalContent = content.replace(/<\/html>/i, `${contextScript}</html>`);
530
- console.log(`[viewFile] Context script injected before </html>`);
531
- } else {
532
- finalContent = content + contextScript;
533
- console.log(`[viewFile] Context script appended to content`);
534
- }
535
- }
536
-
537
- const buffer = Buffer.from(finalContent, "utf-8");
538
-
539
- return reply
540
- .status(200)
541
- .type(mimeType)
542
- .header("Content-Disposition", "inline")
543
- .send(buffer);
544
- } catch (error: unknown) {
545
- const message = error instanceof Error ? error.message : String(error);
546
- return reply.status(502).send({ success: false, error: `View proxy error: ${message}` });
547
- }
548
- }
549
-
550
- async listPath(
551
- request: FastifyRequest<{
552
- Params: ProjectParams;
553
- Querystring: ListPathQuery;
554
- }>
555
- ) {
556
- const tenantId = this.getTenantId(request);
557
- const { workspaceId, projectId } = request.params;
558
- const path = (request.query.path as string) || "/";
559
- const assistantId = request.query.assistantId;
560
-
561
- console.log(`[listPath] tenantId=${tenantId} workspaceId=${workspaceId} projectId=${projectId} path=${path} assistantId=${assistantId}`);
562
-
563
- const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path);
564
- console.log(`[listPath] backend type=${backend.constructor.name} calling lsInfo(${path})`);
565
-
566
- const files = await backend.lsInfo(path);
567
- console.log(`[listPath] result count=${files.length}`);
568
-
569
- return { success: true, data: files };
570
- }
571
-
572
- async readFile(
573
- request: FastifyRequest<{
574
- Params: ProjectParams;
575
- Querystring: ReadFileQuery;
576
- }>
577
- ) {
578
- const tenantId = this.getTenantId(request);
579
- const { workspaceId, projectId } = request.params;
580
- const { path, offset = 0, limit = 1000, assistantId } = request.query;
581
-
582
- const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path);
583
- const content = await backend.read(path, Number(offset), Number(limit));
584
-
585
- return { success: true, data: { content, offset, limit } };
586
- }
587
-
588
- /**
589
- * Upload a file to the workspace project storage.
590
- *
591
- * Route: POST /api/workspaces/:workspaceId/projects/:projectId/uploadfile
592
- * Accepts multipart/form-data with:
593
- * - `file`: the file to upload (required)
594
- * - `path`: optional directory path relative to project root (e.g., "docs", "src/utils")
595
- * - `assistantId`: optional query parameter for sandbox identity matching
596
- * For sandbox storage, delegates to the sandbox upload API.
597
- * For filesystem storage, writes under the same project root used by the file backend.
598
- */
599
- async uploadFile(
600
- request: FastifyRequest<{
601
- Params: ProjectParams;
602
- Querystring: { assistantId?: string };
603
- }>,
604
- reply: FastifyReply
605
- ) {
606
- const tenantId = this.getTenantId(request);
607
- const { workspaceId, projectId } = request.params;
608
- const assistantId = (request.query as any).assistantId || (request.headers["x-assistant-id"] as string);
609
-
610
- const workspace = await this.workspaceStore.getWorkspaceById(
611
- tenantId,
612
- workspaceId
613
- );
614
-
615
- if (!workspace) {
616
- return reply
617
- .status(404)
618
- .send({ success: false, error: "Workspace not found" });
619
- }
620
-
621
- try {
622
- const data = await (request as any).file();
623
- if (!data) {
624
- return reply
625
- .status(400)
626
- .send({ success: false, error: "No file in request" });
627
- }
628
-
629
- const buffer: Buffer = await data.toBuffer();
630
- const filename: string = data.filename || "file";
631
-
632
- const pathEntry = data.fields?.path;
633
- const pathValue =
634
- pathEntry && typeof pathEntry === "object" && "value" in pathEntry
635
- ? String((pathEntry as { value: unknown }).value)
636
- : typeof pathEntry === "string"
637
- ? pathEntry
638
- : undefined;
639
-
640
- if (pathValue && !/^[a-zA-Z0-9_./~-]+$/.test(pathValue)) {
641
- return reply
642
- .status(400)
643
- .send({ success: false, error: "Invalid path parameter" });
644
- }
645
-
646
- if (workspace.storageType === "sandbox") {
647
- const sandboxManager = getSandBoxManager();
648
- const volumeConfig = {
649
- assistant_id: assistantId || "",
650
- thread_id: "",
651
- tenantId,
652
- workspaceId,
653
- projectId,
654
- };
655
-
656
- const realPath = pathValue
657
- ? path.posix.join(pathValue, filename)
658
- : path.posix.join("/project/uploads", filename);
659
-
660
- const isBinary = this.isBinaryContentType(filename);
661
- const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, realPath);
662
- if (volumeBackend && !isBinary) {
663
- await volumeBackend.write(realPath, buffer.toString("utf-8"));
664
- } else if (volumeBackend && volumeBackend.writeBinary) {
665
- await volumeBackend.writeBinary(realPath, buffer);
666
- } else {
667
- const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
668
- await sandbox.file.uploadFile({ file: realPath, data: buffer });
669
- }
670
-
671
- const relativePath = pathValue
672
- ? path.posix.join(pathValue, filename)
673
- : path.posix.join("/project/uploads", filename);
674
-
675
- const result = {
676
- path: relativePath,
677
- name: filename,
678
- is_dir: false,
679
- size: buffer.length,
680
- modified_at: new Date().toISOString(),
681
- };
682
-
683
- return reply.status(200).send({ success: true, data: result });
684
- }
685
-
686
- const rootDir = await this.getFilesystemRootDirectory(
687
- tenantId,
688
- workspaceId,
689
- projectId,
690
- );
691
- await fs.mkdir(rootDir, { recursive: true });
692
- const relativeDirectory = pathValue?.replace(/^[/\\]+/, "") || ".";
693
- const targetDir = path.resolve(rootDir, relativeDirectory);
694
- if (!isPathWithinRoot(path.resolve(rootDir), targetDir)) {
695
- return reply.status(400).send({
696
- success: false,
697
- error: "Upload path must stay within the project directory",
698
- });
699
- }
700
- await fs.mkdir(targetDir, { recursive: true });
701
-
702
- const [resolvedRoot, resolvedTargetDir] = await Promise.all([
703
- fs.realpath(rootDir),
704
- fs.realpath(targetDir),
705
- ]);
706
- if (!isPathWithinRoot(resolvedRoot, resolvedTargetDir)) {
707
- return reply.status(400).send({
708
- success: false,
709
- error: "Upload path must stay within the project directory",
710
- });
711
- }
712
-
713
- const safeFilename = path.basename(filename);
714
- if (safeFilename !== filename) {
715
- return reply.status(400).send({
716
- success: false,
717
- error: "Invalid filename",
718
- });
719
- }
720
- const targetPath = path.join(resolvedTargetDir, safeFilename);
721
- await fs.writeFile(targetPath, buffer);
722
- const stat = await fs.stat(targetPath);
723
-
724
- const result = {
725
- path: path.posix.join("/", relativeDirectory, safeFilename),
726
- name: filename,
727
- is_dir: false,
728
- size: stat.size,
729
- modified_at: stat.mtime.toISOString(),
730
- };
731
-
732
- return reply.status(200).send({ success: true, data: result });
733
- } catch (error: unknown) {
734
- const message = error instanceof Error ? error.message : String(error);
735
- return reply.status(500).send({
736
- success: false,
737
- error: `Upload handler error: ${message}`,
738
- });
739
- }
740
- }
741
- /**
742
- * Browse local filesystem directories for the folder picker.
743
- *
744
- * Security constraints:
745
- * - Restricted to os.homedir()
746
- * - Blacklists system directories
747
- * - Returns only directory names, no file contents or hidden files
748
- */
749
- async browseFilesystem(
750
- request: FastifyRequest<{
751
- Querystring: { path?: string };
752
- }>,
753
- reply: FastifyReply
754
- ) {
755
- const homeDir = os.homedir();
756
- const requestedPath = request.query.path || homeDir;
757
-
758
- const SYSTEM_BLACKLIST = ["/etc", "/sys", "/proc", "/var", "/tmp", "/dev"];
759
-
760
- try {
761
- const resolved = await resolvePathWithinRoot(homeDir, requestedPath);
762
- const normalized = resolved.targetPath;
763
-
764
- for (const blacklisted of SYSTEM_BLACKLIST) {
765
- if (isPathWithinRoot(blacklisted, normalized)) {
766
- return reply.status(403).send({
767
- success: false,
768
- error: `Cannot browse system directory: ${blacklisted}`,
769
- });
770
- }
771
- }
772
-
773
- const entries = await fs.readdir(normalized, { withFileTypes: true });
774
- const dirEntries = entries
775
- .filter((entry) => !entry.name.startsWith(".") && entry.isDirectory())
776
- .map((entry) => ({
777
- name: entry.name,
778
- path: path.join(normalized, entry.name),
779
- isDirectory: true,
780
- }));
781
-
782
- const parentPath = path.dirname(normalized);
783
- const hasParent =
784
- parentPath !== normalized &&
785
- isPathWithinRoot(resolved.rootPath, parentPath);
786
-
787
- return {
788
- success: true,
789
- data: {
790
- path: normalized,
791
- parentPath: hasParent ? parentPath : undefined,
792
- entries: dirEntries,
793
- },
794
- };
795
- } catch (error: unknown) {
796
- if (error instanceof PathOutsideRootError) {
797
- return reply.status(403).send({
798
- success: false,
799
- error: "Browse restricted to home directory",
800
- });
801
- }
802
- const message = error instanceof Error ? error.message : String(error);
803
- return reply.status(400).send({
804
- success: false,
805
- error: `Cannot read directory: ${message}`,
806
- });
807
- }
808
- }
809
- }
810
-
811
- export function registerWorkspaceRoutes(app: FastifyInstance) {
812
- const controller = new WorkspaceController();
813
-
814
- // Workspace routes
815
- app.get("/api/workspaces", controller.listWorkspaces.bind(controller));
816
- app.post("/api/workspaces", controller.createWorkspace.bind(controller));
817
- app.get(
818
- "/api/workspaces/:workspaceId",
819
- controller.getWorkspace.bind(controller)
820
- );
821
- app.patch(
822
- "/api/workspaces/:workspaceId",
823
- controller.updateWorkspace.bind(controller)
824
- );
825
- app.delete(
826
- "/api/workspaces/:workspaceId",
827
- controller.deleteWorkspace.bind(controller)
828
- );
829
-
830
- // Project routes
831
- app.get(
832
- "/api/workspaces/:workspaceId/projects",
833
- controller.listProjects.bind(controller)
834
- );
835
- app.post(
836
- "/api/workspaces/:workspaceId/projects",
837
- controller.createProject.bind(controller)
838
- );
839
- app.get(
840
- "/api/workspaces/:workspaceId/projects/:projectId",
841
- controller.getProject.bind(controller)
842
- );
843
- app.patch(
844
- "/api/workspaces/:workspaceId/projects/:projectId",
845
- controller.updateProject.bind(controller)
846
- );
847
- app.delete(
848
- "/api/workspaces/:workspaceId/projects/:projectId",
849
- controller.deleteProject.bind(controller)
850
- );
851
-
852
- // File operations
853
- app.get(
854
- "/api/workspaces/:workspaceId/projects/:projectId/listpath",
855
- controller.listPath.bind(controller)
856
- );
857
- app.get(
858
- "/api/workspaces/:workspaceId/projects/:projectId/readfile",
859
- controller.readFile.bind(controller)
860
- );
861
- app.post(
862
- "/api/workspaces/:workspaceId/projects/:projectId/uploadfile",
863
- controller.uploadFile.bind(controller)
864
- );
865
- app.get(
866
- "/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
867
- controller.downloadFile.bind(controller)
868
- );
869
- app.get(
870
- "/api/workspaces/:workspaceId/projects/:projectId/viewfile",
871
- controller.viewFile.bind(controller)
872
- );
873
-
874
- // Filesystem browsing for folder picker
875
- app.get(
876
- "/api/filesystem/browse",
877
- controller.browseFilesystem.bind(controller)
878
- );
879
- }