@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,477 +0,0 @@
1
- import { FastifyRequest, FastifyReply } from "fastify";
2
- import type { ResourceAddress, ShareRecord, SharedResourceStore } from "@axiom-lattice/protocols";
3
- import bcrypt from "bcryptjs";
4
- import {
5
- generateToken,
6
- createSharePayload,
7
- TokenCache,
8
- SandboxLatticeManager,
9
- buildNamedVolumeName,
10
- } from "@axiom-lattice/core";
11
- import { getContentTypeFromFilename } from "../utils/mime";
12
-
13
- interface ResourceControllerDeps {
14
- store: SharedResourceStore;
15
- sandboxManager: SandboxLatticeManager;
16
- cache: TokenCache;
17
- logger: { info: (msg: string, obj?: object) => void; warn: (msg: string, obj?: object) => void; error: (msg: string, obj?: object) => void };
18
- }
19
-
20
- export class ResourceController {
21
- constructor(private deps: ResourceControllerDeps) {}
22
-
23
- async createShare(request: FastifyRequest, reply: FastifyReply) {
24
- const userId = (request as any).user
25
- ? (request as any).user.id ?? "unknown"
26
- : "unknown";
27
- const tenantId = (request.headers["x-tenant-id"] as string) || "default";
28
- const workspaceId = request.headers["x-workspace-id"] as string;
29
- const projectId = request.headers["x-project-id"] as string;
30
- const body = request.body as any;
31
-
32
- if (!workspaceId || !projectId) {
33
- return reply.status(400).send({ error: "x-workspace-id and x-project-id headers required" });
34
- }
35
-
36
- const payload = createSharePayload(
37
- tenantId,
38
- workspaceId,
39
- projectId,
40
- userId as string,
41
- body,
42
- );
43
- const token = generateToken();
44
-
45
- try {
46
- await this.deps.store.create({ ...payload, token });
47
- this.deps.logger.info(
48
- "[share] created",
49
- { token, originalPath: body.resourcePath, normalizedPath: payload.address.resourcePath, volume: payload.address.volume, userId },
50
- );
51
- } catch {
52
- return reply.status(500).send({ error: "Failed to create share" });
53
- }
54
-
55
- return reply.status(201).send({ token, url: `/s/${token}` });
56
- }
57
-
58
- async listShares(request: FastifyRequest, reply: FastifyReply) {
59
- const userId = (request as any).user
60
- ? (request as any).user.id ?? "unknown"
61
- : "unknown";
62
- const tenantId = (request.headers["x-tenant-id"] as string) || "default";
63
- const shares = await this.deps.store.listByUser(tenantId, userId as string);
64
- return reply.send(shares.filter((r: any) => (r as any).visibility !== "internal"));
65
- }
66
-
67
- async updateShare(request: FastifyRequest, reply: FastifyReply) {
68
- const { token } = request.params as unknown as { token: string };
69
- const userId = (request as any).user
70
- ? (request as any).user.id ?? "unknown"
71
- : "unknown";
72
- const patch = request.body as any;
73
-
74
- const record = await this.deps.store.findByToken(token);
75
- if (!record || record.createdBy !== userId) {
76
- return reply.status(404).send({ error: "Share not found" });
77
- }
78
-
79
- await this.deps.store.update(token, patch);
80
-
81
- if (patch.revoked) {
82
- this.deps.cache.invalidate(token);
83
- }
84
-
85
- return reply.send({ ok: true });
86
- }
87
-
88
- async revokeShare(request: FastifyRequest, reply: FastifyReply) {
89
- const { token } = request.params as unknown as { token: string };
90
- const userId = (request as any).user
91
- ? (request as any).user.id ?? "unknown"
92
- : "unknown";
93
-
94
- const record = await this.deps.store.findByToken(token);
95
- if (!record || record.createdBy !== userId) {
96
- return reply.status(404).send({ error: "Share not found" });
97
- }
98
-
99
- await this.deps.store.update(token, { revoked: true });
100
- this.deps.cache.invalidate(token);
101
-
102
- return reply.send({ ok: true });
103
- }
104
-
105
- async resolveResource(request: FastifyRequest, reply: FastifyReply) {
106
- const params = request.params as unknown as { token: string; "*"?: string };
107
- const { token } = params;
108
- const subPath = params["*"] ?? "";
109
-
110
- this.deps.logger.info("[share] resolve start", { token, subPath });
111
-
112
- const isApi = subPath && /(^|\/)api\//.test(subPath) && (subPath.endsWith(".js") || subPath.endsWith(".py"));
113
-
114
- // Only API paths accept non-GET methods
115
- if (request.method !== "GET" && !isApi) {
116
- return reply.status(405).send({ error: "Method not allowed" });
117
- }
118
-
119
- // API runtime — proxy to sandbox. Detected by /api/ anywhere in subPath.
120
- if (isApi) {
121
- const record = await this.deps.store.findByToken(token);
122
- if (!record || record.revoked) return reply.status(404).send("Not found");
123
- if (record.expiresAt && new Date(record.expiresAt) < new Date()) {
124
- return reply.status(410).send("Expired");
125
- }
126
- // Auth check for API calls (same as file access)
127
- if (!this._isInternalRequest(request)) {
128
- if (record.visibility === "internal") {
129
- return reply.status(401).send({ error: "Internal access only" });
130
- }
131
- if (record.visibility === "password") {
132
- if (!this._isUnlocked(request, token)) {
133
- return reply.status(401).send({ error: "Password required" });
134
- }
135
- }
136
- }
137
- const ext = subPath.endsWith(".py") ? "py" : "js";
138
- return this._execApi(record, subPath, ext, request, reply);
139
- }
140
-
141
- // 1. Check cache
142
- const cached = this.deps.cache.get(token);
143
- if (cached && !cached.requiresUnlock) {
144
- this.deps.logger.info("[share] cache hit", { token, volume: cached.address.volume, resourcePath: cached.address.resourcePath });
145
- return this._serveFile(cached.address, token, subPath, cached.sandboxConfig ?? { tenantId: "default", workspaceId: "", projectId: "", assistantId: null }, reply);
146
- }
147
- this.deps.logger.info("[share] cache miss, querying DB", { token });
148
-
149
- // 2. Single DB lookup
150
- const record = await this.deps.store.findByToken(token);
151
- if (!record || record.revoked) {
152
- this.deps.logger.warn("[share] token not found or revoked", { token, found: !!record, revoked: record?.revoked });
153
- return reply.status(404).send("Not found");
154
- }
155
- if (record.expiresAt && new Date(record.expiresAt) < new Date()) {
156
- this.deps.logger.info("[share] token expired", { token, expiresAt: record.expiresAt });
157
- return reply.status(410).send("Expired");
158
- }
159
-
160
- this.deps.logger.info("[share] record found", {
161
- token,
162
- volume: record.address.volume,
163
- resourcePath: record.address.resourcePath,
164
- visibility: record.visibility,
165
- workspaceId: record.workspaceId,
166
- projectId: record.projectId,
167
- });
168
-
169
- // 3. Auth
170
- const isInternal = this._isInternalRequest(request);
171
- if (!isInternal) {
172
- // Internal shares: must be logged in
173
- if (record.visibility === "internal") {
174
- return reply.status(401).send({ error: "Internal access only" });
175
- }
176
- if (record.visibility === "password") {
177
- const unlocked = this._isUnlocked(request, token);
178
- if (!unlocked) {
179
- this.deps.logger.info("[share] password protected, returning password page", { token });
180
- this.deps.cache.set(token, {
181
- address: record.address,
182
- requiresUnlock: true,
183
- sandboxConfig: { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId },
184
- });
185
- return reply.type("text/html").send(this._passwordPage(token));
186
- }
187
- this.deps.logger.info("[share] password unlocked", { token });
188
- }
189
- }
190
-
191
- // 4. Access count
192
- if (record.maxAccess !== null) {
193
- const ok = await this.deps.store.atomicIncrementAccess(token);
194
- if (!ok) return reply.status(410).send("Access limit reached");
195
- } else {
196
- this.deps.store.incrementAccess(token).catch(() => {});
197
- }
198
-
199
- // 5. Cache
200
- const sandboxConfig = { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId };
201
- this.deps.cache.set(token, {
202
- address: record.address,
203
- requiresUnlock: record.visibility === "password" && !isInternal,
204
- sandboxConfig,
205
- });
206
-
207
- return this._serveFile(record.address, token, subPath, sandboxConfig, reply);
208
- }
209
-
210
- /**
211
- * Thin redirect: /f/{uuidHex3}/{path} → /s/{internal-token}/{path}
212
- * Decodes 3 UUIDs (no dashes, 96 hex chars), ensures internal share, 302 redirects.
213
- */
214
- async resolveDirectFile(request: FastifyRequest, reply: FastifyReply) {
215
- const params = request.params as unknown as { tid: string; wid: string; pid: string; "*"?: string };
216
- const tenantId = params.tid;
217
- const workspaceId = params.wid;
218
- const projectId = params.pid;
219
- const subPath = (params["*"] ?? "");
220
-
221
- const isApi = subPath && /(^|\/)api\//.test(subPath) && (subPath.endsWith(".js") || subPath.endsWith(".py"));
222
-
223
- // Only API paths accept non-GET methods
224
- if (request.method !== "GET" && !isApi) {
225
- return reply.status(405).send({ error: "Method not allowed" });
226
- }
227
-
228
- // API runtime
229
- if (isApi) {
230
- const ext = subPath.endsWith(".py") ? "py" : "js";
231
- return this._execApiCore({
232
- tenantId, workspaceId, projectId, assistantId: "",
233
- resourcePath: subPath,
234
- }, subPath, ext, request, reply);
235
- }
236
-
237
- const volume = buildNamedVolumeName("p", "project", tenantId, workspaceId, projectId);
238
- return this._resolveAndServe(volume, subPath, reply, `/f/${tenantId}/${workspaceId}/${projectId}`,
239
- { tenantId, workspaceId, projectId, assistantId: null },
240
- );
241
- }
242
-
243
- async unlockShare(request: FastifyRequest, reply: FastifyReply) {
244
- const { token } = request.params as unknown as { token: string };
245
- const { password } = request.body as unknown as { password: string };
246
-
247
- const record = await this.deps.store.findByToken(token);
248
- if (!record || !record.passwordHash) {
249
- return reply.status(404).send({ error: "Invalid request" });
250
- }
251
-
252
- const valid = await bcrypt.compare(password, record.passwordHash!);
253
- if (!valid) {
254
- return reply.status(401).send({ error: "Incorrect password" });
255
- }
256
-
257
- const cookieOpts = [
258
- `share_unlock_${token}=1`,
259
- "Max-Age=86400",
260
- `Path=/s/${token}`,
261
- "SameSite=Lax",
262
- "HttpOnly",
263
- ];
264
- if (request.protocol === "https") {
265
- cookieOpts.push("Secure");
266
- }
267
- reply.header("Set-Cookie", cookieOpts.join("; "));
268
- return reply.send({ ok: true });
269
- }
270
-
271
- private _isInternalRequest(request: FastifyRequest): boolean {
272
- const user = (request as any).user;
273
- if (user && user.id) return true;
274
- // Fallback: same-origin browser request (iframe/img embed — no Auth header)
275
- const origin = request.headers.origin || request.headers.referer;
276
- if (origin) {
277
- try {
278
- const { hostname } = new URL(origin);
279
- if (hostname === request.hostname) return true;
280
- } catch { /* malformed */ }
281
- }
282
- return false;
283
- }
284
-
285
- private _isUnlocked(request: FastifyRequest, token: string): boolean {
286
- const cookie = request.headers.cookie ?? "";
287
- return cookie.includes(`share_unlock_${token}=1`);
288
- }
289
-
290
- /** Returns true if the path has a file extension indicating it's a file, not a directory */
291
- private _isFilePath(path: string): boolean {
292
- return /\.[a-zA-Z0-9]+$/.test(path);
293
- }
294
-
295
- /**
296
- * Shared file resolution: directory → index.html, volume FS → sandbox, base tag, response.
297
- */
298
- private async _resolveAndServe(
299
- volume: string,
300
- resourcePath: string,
301
- reply: FastifyReply,
302
- baseTagPrefix: string,
303
- sandboxConfig?: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },
304
- ) {
305
- const cleanPath = resourcePath.replace(/\/+$/, "");
306
- const entryPath = !cleanPath
307
- ? "index.html"
308
- : this._isFilePath(cleanPath)
309
- ? cleanPath
310
- : `${cleanPath}/index.html`;
311
-
312
- const address: ResourceAddress = { volume, resourcePath: "" };
313
-
314
- let buf: Buffer;
315
- try {
316
- // Volume FS first
317
- const provider = this.deps.sandboxManager.getDefaultProvider();
318
- buf = await provider.getResourceResolver().resolve({ ...address, resourcePath: entryPath });
319
- } catch {
320
- // Sandbox fallback
321
- if (!sandboxConfig) return reply.status(404).send("File not found");
322
- try {
323
- const sandbox = await this.deps.sandboxManager.getSandboxFromConfig({
324
- assistant_id: sandboxConfig.assistantId ?? "",
325
- thread_id: "",
326
- tenantId: sandboxConfig.tenantId,
327
- workspaceId: sandboxConfig.workspaceId,
328
- projectId: sandboxConfig.projectId,
329
- });
330
- buf = await sandbox.file.downloadFile({ file: `/project/${entryPath}` });
331
- } catch {
332
- return reply.status(404).send("File not found");
333
- }
334
- }
335
-
336
- const filename = entryPath.split("/").pop() || "download";
337
- const contentType = getContentTypeFromFilename(filename);
338
-
339
- // Inject base tag for HTML
340
- if (/\.(html|htm)$/i.test(filename)) {
341
- const baseDir = cleanPath
342
- ? (this._isFilePath(cleanPath) ? cleanPath.replace(/[^/]+$/, "") : cleanPath)
343
- : "";
344
- const html = buf.toString("utf-8");
345
- if (!/<base\b/i.test(html)) {
346
- const baseTag = `<base href="${baseTagPrefix}${baseDir ? "/" + baseDir : ""}/">`;
347
- buf = Buffer.from(
348
- html.includes("<head>") ? html.replace("<head>", `<head>${baseTag}`) : baseTag + html,
349
- "utf-8"
350
- );
351
- }
352
- }
353
-
354
- return reply
355
- .status(200)
356
- .type(contentType)
357
- .header("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(filename)}`)
358
- .header("Access-Control-Allow-Origin", "*")
359
- .send(buf);
360
- }
361
-
362
- private async _serveFile(
363
- address: ResourceAddress,
364
- token: string,
365
- subPath: string,
366
- sandboxConfig: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },
367
- reply: FastifyReply,
368
- ) {
369
- const resourcePath = subPath
370
- ? this._resolveSafeSubPath(address.resourcePath, subPath)
371
- : address.resourcePath;
372
- return this._resolveAndServe(
373
- address.volume,
374
- resourcePath,
375
- reply,
376
- `/s/${token}`,
377
- sandboxConfig,
378
- );
379
- }
380
-
381
- private async _execApi(
382
- record: ShareRecord,
383
- apiSubPath: string,
384
- ext: "js" | "py",
385
- request: FastifyRequest,
386
- reply: FastifyReply,
387
- ) {
388
- const basePath = this._isFilePath(record.address.resourcePath)
389
- ? ""
390
- : `${record.address.resourcePath}/`;
391
- return this._execApiCore({
392
- tenantId: record.tenantId,
393
- workspaceId: record.workspaceId,
394
- projectId: record.projectId,
395
- assistantId: record.assistantId,
396
- resourcePath: basePath + apiSubPath,
397
- }, apiSubPath, ext, request, reply);
398
- }
399
-
400
- private async _execApiCore(
401
- config: { tenantId: string; workspaceId: string; projectId: string; assistantId?: string | null; resourcePath: string },
402
- apiSubPath: string,
403
- ext: "js" | "py",
404
- request: FastifyRequest,
405
- reply: FastifyReply,
406
- ) {
407
- let sandbox: any;
408
- try {
409
- sandbox = await this.deps.sandboxManager.getSandboxFromConfig({
410
- assistant_id: config.assistantId ?? "",
411
- thread_id: "",
412
- tenantId: config.tenantId,
413
- workspaceId: config.workspaceId,
414
- projectId: config.projectId,
415
- });
416
- } catch (err) {
417
- this.deps.logger.error("[share] sandbox start failed for API", { error: (err as Error).message });
418
- return reply.status(502).send({ error: "Sandbox unavailable" });
419
- }
420
-
421
- const scriptPath = `/project/${config.resourcePath}`;
422
- const cmd = ext === "py" ? `python3 ${scriptPath}` : `node ${scriptPath}`;
423
-
424
- // Pass query params and body via environment
425
- const queryStr = request.url.includes("?") ? (request.url as string).split("?")[1] : "";
426
- const queryObj: Record<string, string> = {};
427
- if (queryStr) {
428
- for (const [k, v] of new URLSearchParams(queryStr)) {
429
- queryObj[k] = v;
430
- }
431
- }
432
-
433
- const bodyStr = (request.body as string) ?? "";
434
-
435
- const envVars = [
436
- `export API_QUERY='${JSON.stringify(queryObj).replace(/'/g, "'\\''")}'`,
437
- `export API_BODY='${bodyStr.replace(/'/g, "'\\''")}'`,
438
- `export API_METHOD='${request.method}'`,
439
- `export API_GATEWAY_URL='${request.protocol}://${request.hostname}'`,
440
- `export API_TENANT_ID='${config.tenantId}'`,
441
- `export API_ASSISTANT_ID='${config.assistantId || ""}'`,
442
- ].join(" && ");
443
-
444
- const fullCmd = `${envVars} && ${cmd}`;
445
-
446
- this.deps.logger.info("[api] exec", { scriptPath, cmd: cmd.substring(0, 80) });
447
-
448
- try {
449
- const result = await sandbox.shell.execCommand({ command: fullCmd });
450
- // Parse stdout as JSON
451
- let data: unknown;
452
- try {
453
- data = JSON.parse(result.output?.trim() || "{}");
454
- } catch {
455
- data = { output: result.output?.trim() || "" };
456
- }
457
- return reply.status(200).type("application/json").send(data);
458
- } catch (err) {
459
- this.deps.logger.warn("[api] exec failed", { scriptPath, error: (err as Error).message });
460
- return reply.status(500).send({ error: (err as Error).message });
461
- }
462
- }
463
-
464
- private _resolveSafeSubPath(entryFile: string, subPath: string): string {
465
- if (!subPath) return entryFile;
466
- if (subPath.includes("..")) throw new Error("Path traversal denied");
467
- if (subPath.startsWith(entryFile + "/") || subPath === entryFile) return subPath;
468
- const entryDir = this._isFilePath(entryFile)
469
- ? entryFile.replace(/[^/]+$/, "")
470
- : entryFile + "/";
471
- return (entryDir + subPath).replace(/\/+/g, "/");
472
- }
473
-
474
- private _passwordPage(token: string): string {
475
- return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Password Protected</title><style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f5f5f5}.card{background:#fff;padding:32px;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.1);width:100%;max-width:360px;text-align:center}h2{margin:0 0 8px;font-size:20px}p{margin:0 0 20px;color:#666;font-size:14px}input{width:100%;padding:10px 12px;border:1px solid #d9d9d9;border-radius:8px;font-size:14px;box-sizing:border-box;margin-bottom:12px}button{width:100%;padding:10px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer}.error{color:#ef4444;font-size:13px;margin-top:8px;display:none}</style></head><body><div class="card"><h2>Password Protected</h2><p>This share requires a password to access.</p><form id="f"><input type="password" id="p" placeholder="Enter password" required><button type="submit">Unlock</button><div class="error" id="e">Incorrect password</div></form></div><script>document.getElementById('f').onsubmit=async(e)=>{e.preventDefault();const r=await fetch('/s/${token}/unlock',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:document.getElementById('p').value})});if(r.ok)location.reload();else document.getElementById('e').style.display='block'}</script></body></html>`;
476
- }
477
- }