@axiom-lattice/gateway 2.1.108 → 2.1.110
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/.turbo/turbo-build.log +13 -13
- package/CHANGELOG.md +21 -0
- package/dist/index.js +462 -239
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +182 -9
- package/dist/index.mjs.map +1 -1
- package/dist/{resources-P4XF764M.mjs → resources-5POCLGXV.mjs} +122 -72
- package/dist/resources-5POCLGXV.mjs.map +1 -0
- package/package.json +6 -6
- package/src/controllers/connections.ts +174 -0
- package/src/controllers/middleware-types.ts +15 -0
- package/src/controllers/resources.ts +158 -86
- package/src/index.ts +5 -2
- package/src/routes/index.ts +7 -0
- package/src/routes/resource-routes.ts +11 -0
- package/dist/resources-P4XF764M.mjs.map +0 -1
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
createSharePayload,
|
|
7
7
|
TokenCache,
|
|
8
8
|
SandboxLatticeManager,
|
|
9
|
+
buildNamedVolumeName,
|
|
9
10
|
} from "@axiom-lattice/core";
|
|
10
|
-
import { getContentTypeFromFilename
|
|
11
|
+
import { getContentTypeFromFilename } from "../utils/mime";
|
|
11
12
|
|
|
12
13
|
interface ResourceControllerDeps {
|
|
13
14
|
store: SharedResourceStore;
|
|
@@ -60,7 +61,7 @@ export class ResourceController {
|
|
|
60
61
|
: "unknown";
|
|
61
62
|
const tenantId = (request.headers["x-tenant-id"] as string) || "default";
|
|
62
63
|
const shares = await this.deps.store.listByUser(tenantId, userId as string);
|
|
63
|
-
return reply.send(shares);
|
|
64
|
+
return reply.send(shares.filter((r: any) => (r as any).visibility !== "internal"));
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
async updateShare(request: FastifyRequest, reply: FastifyReply) {
|
|
@@ -108,17 +109,29 @@ export class ResourceController {
|
|
|
108
109
|
|
|
109
110
|
this.deps.logger.info("[share] resolve start", { token, subPath });
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
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) {
|
|
113
121
|
const record = await this.deps.store.findByToken(token);
|
|
114
122
|
if (!record || record.revoked) return reply.status(404).send("Not found");
|
|
115
123
|
if (record.expiresAt && new Date(record.expiresAt) < new Date()) {
|
|
116
124
|
return reply.status(410).send("Expired");
|
|
117
125
|
}
|
|
118
126
|
// Auth check for API calls (same as file access)
|
|
119
|
-
if (!this._isInternalRequest(request)
|
|
120
|
-
if (
|
|
121
|
-
return reply.status(401).send({ error: "
|
|
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
|
+
}
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
const ext = subPath.endsWith(".py") ? "py" : "js";
|
|
@@ -156,6 +169,10 @@ export class ResourceController {
|
|
|
156
169
|
// 3. Auth
|
|
157
170
|
const isInternal = this._isInternalRequest(request);
|
|
158
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
|
+
}
|
|
159
176
|
if (record.visibility === "password") {
|
|
160
177
|
const unlocked = this._isUnlocked(request, token);
|
|
161
178
|
if (!unlocked) {
|
|
@@ -190,6 +207,39 @@ export class ResourceController {
|
|
|
190
207
|
return this._serveFile(record.address, token, subPath, sandboxConfig, reply);
|
|
191
208
|
}
|
|
192
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
|
+
|
|
193
243
|
async unlockShare(request: FastifyRequest, reply: FastifyReply) {
|
|
194
244
|
const { token } = request.params as unknown as { token: string };
|
|
195
245
|
const { password } = request.body as unknown as { password: string };
|
|
@@ -219,9 +269,17 @@ export class ResourceController {
|
|
|
219
269
|
}
|
|
220
270
|
|
|
221
271
|
private _isInternalRequest(request: FastifyRequest): boolean {
|
|
222
|
-
// Check for valid user session from auth middleware
|
|
223
272
|
const user = (request as any).user;
|
|
224
|
-
|
|
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;
|
|
225
283
|
}
|
|
226
284
|
|
|
227
285
|
private _isUnlocked(request: FastifyRequest, token: string): boolean {
|
|
@@ -234,37 +292,33 @@ export class ResourceController {
|
|
|
234
292
|
return /\.[a-zA-Z0-9]+$/.test(path);
|
|
235
293
|
}
|
|
236
294
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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,
|
|
242
301
|
reply: FastifyReply,
|
|
302
|
+
baseTagPrefix: string,
|
|
303
|
+
sandboxConfig?: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },
|
|
243
304
|
) {
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
: sandboxPath;
|
|
253
|
-
|
|
254
|
-
this.deps.logger.info("[share] serving file", {
|
|
255
|
-
token, volume: address.volume, sandboxPath: entryPath, subPath: subPath || "(none)",
|
|
256
|
-
tenantId: sandboxConfig.tenantId, workspaceId: sandboxConfig.workspaceId, projectId: sandboxConfig.projectId,
|
|
257
|
-
});
|
|
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: "" };
|
|
258
313
|
|
|
259
314
|
let buf: Buffer;
|
|
260
315
|
try {
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
} catch
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
});
|
|
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");
|
|
268
322
|
try {
|
|
269
323
|
const sandbox = await this.deps.sandboxManager.getSandboxFromConfig({
|
|
270
324
|
assistant_id: sandboxConfig.assistantId ?? "",
|
|
@@ -274,32 +328,56 @@ export class ResourceController {
|
|
|
274
328
|
projectId: sandboxConfig.projectId,
|
|
275
329
|
});
|
|
276
330
|
buf = await sandbox.file.downloadFile({ file: `/project/${entryPath}` });
|
|
277
|
-
|
|
278
|
-
} catch (err) {
|
|
279
|
-
this.deps.logger.warn("[share] all resolution attempts failed", { token, resourcePath: entryPath, error: (err as Error).message });
|
|
331
|
+
} catch {
|
|
280
332
|
return reply.status(404).send("File not found");
|
|
281
333
|
}
|
|
282
334
|
}
|
|
283
335
|
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const isHtml = !subPath && /\.(html|htm)$/i.test(filename);
|
|
336
|
+
const filename = entryPath.split("/").pop() || "download";
|
|
337
|
+
const contentType = getContentTypeFromFilename(filename);
|
|
287
338
|
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|
|
290
352
|
}
|
|
291
353
|
|
|
292
|
-
const contentType = getContentTypeFromFilename(filename);
|
|
293
|
-
const contentDisposition = `inline; filename="${filename.replace(/"/g, '\\"')}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
294
|
-
|
|
295
354
|
return reply
|
|
296
355
|
.status(200)
|
|
297
356
|
.type(contentType)
|
|
298
|
-
.header("Content-Disposition",
|
|
357
|
+
.header("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(filename)}`)
|
|
299
358
|
.header("Access-Control-Allow-Origin", "*")
|
|
300
359
|
.send(buf);
|
|
301
360
|
}
|
|
302
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
|
+
|
|
303
381
|
private async _execApi(
|
|
304
382
|
record: ShareRecord,
|
|
305
383
|
apiSubPath: string,
|
|
@@ -307,26 +385,41 @@ export class ResourceController {
|
|
|
307
385
|
request: FastifyRequest,
|
|
308
386
|
reply: FastifyReply,
|
|
309
387
|
) {
|
|
310
|
-
|
|
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
|
+
) {
|
|
311
407
|
let sandbox: any;
|
|
312
408
|
try {
|
|
313
409
|
sandbox = await this.deps.sandboxManager.getSandboxFromConfig({
|
|
314
|
-
assistant_id:
|
|
410
|
+
assistant_id: config.assistantId ?? "",
|
|
315
411
|
thread_id: "",
|
|
316
|
-
tenantId:
|
|
317
|
-
workspaceId:
|
|
318
|
-
projectId:
|
|
412
|
+
tenantId: config.tenantId,
|
|
413
|
+
workspaceId: config.workspaceId,
|
|
414
|
+
projectId: config.projectId,
|
|
319
415
|
});
|
|
320
416
|
} catch (err) {
|
|
321
417
|
this.deps.logger.error("[share] sandbox start failed for API", { error: (err as Error).message });
|
|
322
418
|
return reply.status(502).send({ error: "Sandbox unavailable" });
|
|
323
419
|
}
|
|
324
420
|
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const cmd = ext === "py"
|
|
328
|
-
? `python3 ${scriptPath}`
|
|
329
|
-
: `node ${scriptPath}`;
|
|
421
|
+
const scriptPath = `/project/${config.resourcePath}`;
|
|
422
|
+
const cmd = ext === "py" ? `python3 ${scriptPath}` : `node ${scriptPath}`;
|
|
330
423
|
|
|
331
424
|
// Pass query params and body via environment
|
|
332
425
|
const queryStr = request.url.includes("?") ? (request.url as string).split("?")[1] : "";
|
|
@@ -344,30 +437,27 @@ export class ResourceController {
|
|
|
344
437
|
`export API_BODY='${bodyStr.replace(/'/g, "'\\''")}'`,
|
|
345
438
|
`export API_METHOD='${request.method}'`,
|
|
346
439
|
`export API_GATEWAY_URL='${request.protocol}://${request.hostname}'`,
|
|
347
|
-
`export API_TENANT_ID='${
|
|
348
|
-
`export API_ASSISTANT_ID='${
|
|
440
|
+
`export API_TENANT_ID='${config.tenantId}'`,
|
|
441
|
+
`export API_ASSISTANT_ID='${config.assistantId || ""}'`,
|
|
349
442
|
].join(" && ");
|
|
350
443
|
|
|
351
444
|
const fullCmd = `${envVars} && ${cmd}`;
|
|
352
445
|
|
|
353
|
-
this.deps.logger.info("[
|
|
446
|
+
this.deps.logger.info("[api] exec", { scriptPath, cmd: cmd.substring(0, 80) });
|
|
354
447
|
|
|
355
448
|
try {
|
|
356
|
-
const result = await sandbox.shell.
|
|
449
|
+
const result = await sandbox.shell.execCommand({ command: fullCmd });
|
|
357
450
|
// Parse stdout as JSON
|
|
358
451
|
let data: unknown;
|
|
359
452
|
try {
|
|
360
|
-
data = JSON.parse(result.
|
|
453
|
+
data = JSON.parse(result.output?.trim() || "{}");
|
|
361
454
|
} catch {
|
|
362
|
-
|
|
363
|
-
data = { output: result.stdout.trim() };
|
|
455
|
+
data = { output: result.output?.trim() || "" };
|
|
364
456
|
}
|
|
365
457
|
return reply.status(200).type("application/json").send(data);
|
|
366
458
|
} catch (err) {
|
|
367
|
-
this.deps.logger.warn("[
|
|
368
|
-
|
|
369
|
-
const msg = (err as any)?.stderr || (err as Error).message;
|
|
370
|
-
return reply.status(500).send({ error: msg });
|
|
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 });
|
|
371
461
|
}
|
|
372
462
|
}
|
|
373
463
|
|
|
@@ -382,24 +472,6 @@ export class ResourceController {
|
|
|
382
472
|
return (entryDir + subPath).replace(/\/+/g, "/");
|
|
383
473
|
}
|
|
384
474
|
|
|
385
|
-
private _injectBaseTag(buf: Buffer, token: string): Buffer {
|
|
386
|
-
const html = buf.toString("utf-8");
|
|
387
|
-
if (/<base\b/i.test(html)) return buf;
|
|
388
|
-
const baseTag = `<base href="/s/${token}/">`;
|
|
389
|
-
if (html.includes("</head>")) {
|
|
390
|
-
return Buffer.from(html.replace("</head>", `${baseTag}</head>`), "utf-8");
|
|
391
|
-
}
|
|
392
|
-
// No </head> — insert after <head> or <html> or after <!doctype>
|
|
393
|
-
if (html.includes("<head>")) {
|
|
394
|
-
return Buffer.from(html.replace("<head>", `<head>${baseTag}`), "utf-8");
|
|
395
|
-
}
|
|
396
|
-
if (html.includes("<html>")) {
|
|
397
|
-
return Buffer.from(html.replace("<html>", `<html>${baseTag}`), "utf-8");
|
|
398
|
-
}
|
|
399
|
-
// Fallback: prepend (will be after <!doctype> if present since html starts with <!doctype)
|
|
400
|
-
return Buffer.from(baseTag + html, "utf-8");
|
|
401
|
-
}
|
|
402
|
-
|
|
403
475
|
private _passwordPage(token: string): string {
|
|
404
476
|
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>`;
|
|
405
477
|
}
|
package/src/index.ts
CHANGED
|
@@ -78,8 +78,8 @@ function initializeLogger(config: LoggerConfig) {
|
|
|
78
78
|
|
|
79
79
|
// 创建 Fastify 应用
|
|
80
80
|
const app = fastify({
|
|
81
|
-
logger: false,
|
|
82
|
-
bodyLimit: Number(process.env.BODY_LIMIT) || 50 * 1024 * 1024,
|
|
81
|
+
logger: false,
|
|
82
|
+
bodyLimit: Number(process.env.BODY_LIMIT) || 50 * 1024 * 1024,
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
// Custom content type parser to handle DELETE requests with Content-Type but no body
|
|
@@ -138,6 +138,9 @@ app.addHook("preHandler", async (request, reply) => {
|
|
|
138
138
|
const urlPath = request.url.split("?")[0];
|
|
139
139
|
if (urlPath.includes("/viewfile") || urlPath.includes("/downloadfile") || urlPath.includes("/uploadfile")) return;
|
|
140
140
|
|
|
141
|
+
// Internal file access — auth handled by controller (Referer check)
|
|
142
|
+
if (urlPath.startsWith("/f/") || urlPath === "/f") return;
|
|
143
|
+
|
|
141
144
|
return reply.status(401).send({
|
|
142
145
|
success: false,
|
|
143
146
|
error: "Unauthorized - Missing or invalid token",
|
package/src/routes/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import * as dataQueryController from "../controllers/data-query";
|
|
|
19
19
|
import * as workflowTrackingController from "../controllers/workflow-tracking";
|
|
20
20
|
import * as paController from "../controllers/personal-assistant";
|
|
21
21
|
import * as tasksController from "../controllers/tasks";
|
|
22
|
+
import { middlewareTypesRoutes } from "../controllers/middleware-types";
|
|
22
23
|
import {
|
|
23
24
|
createRunSchema,
|
|
24
25
|
getAllMemoryItemsSchema,
|
|
@@ -41,6 +42,7 @@ import { registerSandboxProxyRoutes } from "../controllers/sandbox";
|
|
|
41
42
|
import { registerWorkspaceRoutes } from "../controllers/workspace";
|
|
42
43
|
import { registerDatabaseConfigRoutes } from "../controllers/database-configs";
|
|
43
44
|
import { registerMetricsServerConfigRoutes } from "../controllers/metrics-configs";
|
|
45
|
+
import { connectionRoutes } from "../controllers/connections";
|
|
44
46
|
import { registerMcpServerConfigRoutes } from "../controllers/mcp-configs";
|
|
45
47
|
import { registerEvalRoutes } from "../controllers/eval";
|
|
46
48
|
import { registerUserRoutes } from "../controllers/users";
|
|
@@ -412,6 +414,11 @@ export const registerLatticeRoutes = (app: FastifyInstance, channelDeps?: { rout
|
|
|
412
414
|
|
|
413
415
|
registerMetricsServerConfigRoutes(app);
|
|
414
416
|
|
|
417
|
+
connectionRoutes(app);
|
|
418
|
+
|
|
419
|
+
// Plugin middleware types discovery
|
|
420
|
+
middlewareTypesRoutes(app);
|
|
421
|
+
|
|
415
422
|
// Data query route
|
|
416
423
|
app.post<{
|
|
417
424
|
Body: any;
|
|
@@ -8,6 +8,9 @@ export function registerResourceRoutes(
|
|
|
8
8
|
// Public share access — MUST be before /api/** wildcard routes
|
|
9
9
|
app.get("/s/:token", controller.resolveResource.bind(controller));
|
|
10
10
|
app.get("/s/:token/*", controller.resolveResource.bind(controller));
|
|
11
|
+
app.post("/s/:token/*", controller.resolveResource.bind(controller));
|
|
12
|
+
app.put("/s/:token/*", controller.resolveResource.bind(controller));
|
|
13
|
+
app.delete("/s/:token/*", controller.resolveResource.bind(controller));
|
|
11
14
|
app.post("/s/:token/unlock", controller.unlockShare.bind(controller));
|
|
12
15
|
app.options("/s/:token", async (_req, reply) => {
|
|
13
16
|
reply.header("Access-Control-Allow-Origin", "*");
|
|
@@ -16,6 +19,14 @@ export function registerResourceRoutes(
|
|
|
16
19
|
return reply.status(204).send();
|
|
17
20
|
});
|
|
18
21
|
|
|
22
|
+
// Internal file access: /f/{tenant}/{workspace}/{project}/*
|
|
23
|
+
const dirFile = controller.resolveDirectFile.bind(controller);
|
|
24
|
+
app.get("/f/:tid/:wid/:pid", dirFile);
|
|
25
|
+
app.get("/f/:tid/:wid/:pid/*", dirFile);
|
|
26
|
+
app.post("/f/:tid/:wid/:pid/*", dirFile);
|
|
27
|
+
app.put("/f/:tid/:wid/:pid/*", dirFile);
|
|
28
|
+
app.delete("/f/:tid/:wid/:pid/*", dirFile);
|
|
29
|
+
|
|
19
30
|
// Management API
|
|
20
31
|
app.post("/api/resources/share", controller.createShare.bind(controller));
|
|
21
32
|
app.get("/api/resources/shares", controller.listShares.bind(controller));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/controllers/resources.ts"],"sourcesContent":["import { FastifyRequest, FastifyReply } from \"fastify\";\nimport type { ResourceAddress, ShareRecord, SharedResourceStore } from \"@axiom-lattice/protocols\";\nimport bcrypt from \"bcryptjs\";\nimport {\n generateToken,\n createSharePayload,\n TokenCache,\n SandboxLatticeManager,\n} from \"@axiom-lattice/core\";\nimport { getContentTypeFromFilename, getFilenameFromPath } from \"../utils/mime\";\n\ninterface ResourceControllerDeps {\n store: SharedResourceStore;\n sandboxManager: SandboxLatticeManager;\n cache: TokenCache;\n logger: { info: (msg: string, obj?: object) => void; warn: (msg: string, obj?: object) => void; error: (msg: string, obj?: object) => void };\n}\n\nexport class ResourceController {\n constructor(private deps: ResourceControllerDeps) {}\n\n async createShare(request: FastifyRequest, reply: FastifyReply) {\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n const tenantId = (request.headers[\"x-tenant-id\"] as string) || \"default\";\n const workspaceId = request.headers[\"x-workspace-id\"] as string;\n const projectId = request.headers[\"x-project-id\"] as string;\n const body = request.body as any;\n\n if (!workspaceId || !projectId) {\n return reply.status(400).send({ error: \"x-workspace-id and x-project-id headers required\" });\n }\n\n const payload = createSharePayload(\n tenantId,\n workspaceId,\n projectId,\n userId as string,\n body,\n );\n const token = generateToken();\n\n try {\n await this.deps.store.create({ ...payload, token });\n this.deps.logger.info(\n \"[share] created\",\n { token, originalPath: body.resourcePath, normalizedPath: payload.address.resourcePath, volume: payload.address.volume, userId },\n );\n } catch {\n return reply.status(500).send({ error: \"Failed to create share\" });\n }\n\n return reply.status(201).send({ token, url: `/s/${token}` });\n }\n\n async listShares(request: FastifyRequest, reply: FastifyReply) {\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n const tenantId = (request.headers[\"x-tenant-id\"] as string) || \"default\";\n const shares = await this.deps.store.listByUser(tenantId, userId as string);\n return reply.send(shares);\n }\n\n async updateShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n const patch = request.body as any;\n\n const record = await this.deps.store.findByToken(token);\n if (!record || record.createdBy !== userId) {\n return reply.status(404).send({ error: \"Share not found\" });\n }\n\n await this.deps.store.update(token, patch);\n\n if (patch.revoked) {\n this.deps.cache.invalidate(token);\n }\n\n return reply.send({ ok: true });\n }\n\n async revokeShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n\n const record = await this.deps.store.findByToken(token);\n if (!record || record.createdBy !== userId) {\n return reply.status(404).send({ error: \"Share not found\" });\n }\n\n await this.deps.store.update(token, { revoked: true });\n this.deps.cache.invalidate(token);\n\n return reply.send({ ok: true });\n }\n\n async resolveResource(request: FastifyRequest, reply: FastifyReply) {\n const params = request.params as unknown as { token: string; \"*\"?: string };\n const { token } = params;\n const subPath = params[\"*\"] ?? \"\";\n\n this.deps.logger.info(\"[share] resolve start\", { token, subPath });\n\n // API runtime — proxy to sandbox\n if (subPath && (subPath.endsWith(\".js\") || subPath.endsWith(\".py\")) && subPath.startsWith(\"api/\")) {\n const record = await this.deps.store.findByToken(token);\n if (!record || record.revoked) return reply.status(404).send(\"Not found\");\n if (record.expiresAt && new Date(record.expiresAt) < new Date()) {\n return reply.status(410).send(\"Expired\");\n }\n // Auth check for API calls (same as file access)\n if (!this._isInternalRequest(request) && record.visibility === \"password\") {\n if (!this._isUnlocked(request, token)) {\n return reply.status(401).send({ error: \"Password required\" });\n }\n }\n const ext = subPath.endsWith(\".py\") ? \"py\" : \"js\";\n return this._execApi(record, subPath, ext, request, reply);\n }\n\n // 1. Check cache\n const cached = this.deps.cache.get(token);\n if (cached && !cached.requiresUnlock) {\n this.deps.logger.info(\"[share] cache hit\", { token, volume: cached.address.volume, resourcePath: cached.address.resourcePath });\n return this._serveFile(cached.address, token, subPath, cached.sandboxConfig ?? { tenantId: \"default\", workspaceId: \"\", projectId: \"\", assistantId: null }, reply);\n }\n this.deps.logger.info(\"[share] cache miss, querying DB\", { token });\n\n // 2. Single DB lookup\n const record = await this.deps.store.findByToken(token);\n if (!record || record.revoked) {\n this.deps.logger.warn(\"[share] token not found or revoked\", { token, found: !!record, revoked: record?.revoked });\n return reply.status(404).send(\"Not found\");\n }\n if (record.expiresAt && new Date(record.expiresAt) < new Date()) {\n this.deps.logger.info(\"[share] token expired\", { token, expiresAt: record.expiresAt });\n return reply.status(410).send(\"Expired\");\n }\n\n this.deps.logger.info(\"[share] record found\", {\n token,\n volume: record.address.volume,\n resourcePath: record.address.resourcePath,\n visibility: record.visibility,\n workspaceId: record.workspaceId,\n projectId: record.projectId,\n });\n\n // 3. Auth\n const isInternal = this._isInternalRequest(request);\n if (!isInternal) {\n if (record.visibility === \"password\") {\n const unlocked = this._isUnlocked(request, token);\n if (!unlocked) {\n this.deps.logger.info(\"[share] password protected, returning password page\", { token });\n this.deps.cache.set(token, {\n address: record.address,\n requiresUnlock: true,\n sandboxConfig: { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId },\n });\n return reply.type(\"text/html\").send(this._passwordPage(token));\n }\n this.deps.logger.info(\"[share] password unlocked\", { token });\n }\n }\n\n // 4. Access count\n if (record.maxAccess !== null) {\n const ok = await this.deps.store.atomicIncrementAccess(token);\n if (!ok) return reply.status(410).send(\"Access limit reached\");\n } else {\n this.deps.store.incrementAccess(token).catch(() => {});\n }\n\n // 5. Cache\n const sandboxConfig = { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId };\n this.deps.cache.set(token, {\n address: record.address,\n requiresUnlock: record.visibility === \"password\" && !isInternal,\n sandboxConfig,\n });\n\n return this._serveFile(record.address, token, subPath, sandboxConfig, reply);\n }\n\n async unlockShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const { password } = request.body as unknown as { password: string };\n\n const record = await this.deps.store.findByToken(token);\n if (!record || !record.passwordHash) {\n return reply.status(404).send({ error: \"Invalid request\" });\n }\n\n const valid = await bcrypt.compare(password, record.passwordHash!);\n if (!valid) {\n return reply.status(401).send({ error: \"Incorrect password\" });\n }\n\n const cookieOpts = [\n `share_unlock_${token}=1`,\n \"Max-Age=86400\",\n `Path=/s/${token}`,\n \"SameSite=Lax\",\n \"HttpOnly\",\n ];\n if (request.protocol === \"https\") {\n cookieOpts.push(\"Secure\");\n }\n reply.header(\"Set-Cookie\", cookieOpts.join(\"; \"));\n return reply.send({ ok: true });\n }\n\n private _isInternalRequest(request: FastifyRequest): boolean {\n // Check for valid user session from auth middleware\n const user = (request as any).user;\n return !!user && !!user.id;\n }\n\n private _isUnlocked(request: FastifyRequest, token: string): boolean {\n const cookie = request.headers.cookie ?? \"\";\n return cookie.includes(`share_unlock_${token}=1`);\n }\n\n /** Returns true if the path has a file extension indicating it's a file, not a directory */\n private _isFilePath(path: string): boolean {\n return /\\.[a-zA-Z0-9]+$/.test(path);\n }\n\n private async _serveFile(\n address: ResourceAddress,\n token: string,\n subPath: string,\n sandboxConfig: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },\n reply: FastifyReply,\n ) {\n const provider = this.deps.sandboxManager.getDefaultProvider();\n const resolver = provider.getResourceResolver();\n const sandboxPath = subPath\n ? this._resolveSafeSubPath(address.resourcePath, subPath)\n : address.resourcePath;\n // If entry has no file extension, treat as directory → try index.html\n const entryPath = (!subPath && !this._isFilePath(sandboxPath))\n ? `${sandboxPath}/index.html`\n : sandboxPath;\n\n this.deps.logger.info(\"[share] serving file\", {\n token, volume: address.volume, sandboxPath: entryPath, subPath: subPath || \"(none)\",\n tenantId: sandboxConfig.tenantId, workspaceId: sandboxConfig.workspaceId, projectId: sandboxConfig.projectId,\n });\n\n let buf: Buffer;\n try {\n // Try volume FS first\n buf = await resolver.resolve({ ...address, resourcePath: entryPath });\n this.deps.logger.info(\"[share] resolved via volume FS\", { token, size: buf.length });\n } catch (err) {\n this.deps.logger.warn(\"[share] volume FS failed, trying sandbox fallback\", {\n token, path: entryPath, error: (err as Error).message,\n });\n try {\n const sandbox = await this.deps.sandboxManager.getSandboxFromConfig({\n assistant_id: sandboxConfig.assistantId ?? \"\",\n thread_id: \"\",\n tenantId: sandboxConfig.tenantId,\n workspaceId: sandboxConfig.workspaceId,\n projectId: sandboxConfig.projectId,\n });\n buf = await sandbox.file.downloadFile({ file: `/project/${entryPath}` });\n this.deps.logger.info(\"[share] resolved via sandbox fallback\", { token, size: buf.length });\n } catch (err) {\n this.deps.logger.warn(\"[share] all resolution attempts failed\", { token, resourcePath: entryPath, error: (err as Error).message });\n return reply.status(404).send(\"File not found\");\n }\n }\n\n const fullPath = entryPath;\n const filename = getFilenameFromPath(fullPath);\n const isHtml = !subPath && /\\.(html|htm)$/i.test(filename);\n\n if (isHtml) {\n buf = this._injectBaseTag(buf, token);\n }\n\n const contentType = getContentTypeFromFilename(filename);\n const contentDisposition = `inline; filename=\"${filename.replace(/\"/g, '\\\\\"')}\"; filename*=UTF-8''${encodeURIComponent(filename)}`;\n\n return reply\n .status(200)\n .type(contentType)\n .header(\"Content-Disposition\", contentDisposition)\n .header(\"Access-Control-Allow-Origin\", \"*\")\n .send(buf);\n }\n\n private async _execApi(\n record: ShareRecord,\n apiSubPath: string,\n ext: \"js\" | \"py\",\n request: FastifyRequest,\n reply: FastifyReply,\n ) {\n // Get or start sandbox\n let sandbox: any;\n try {\n sandbox = await this.deps.sandboxManager.getSandboxFromConfig({\n assistant_id: record.assistantId ?? \"\",\n thread_id: \"\",\n tenantId: record.tenantId,\n workspaceId: record.workspaceId,\n projectId: record.projectId,\n });\n } catch (err) {\n this.deps.logger.error(\"[share] sandbox start failed for API\", { error: (err as Error).message });\n return reply.status(502).send({ error: \"Sandbox unavailable\" });\n }\n\n // Build the command: node /project/api/hello.js or python3 /project/api/hello.py\n const scriptPath = `/project/${apiSubPath}`; // e.g., /project/api/hello.js\n const cmd = ext === \"py\"\n ? `python3 ${scriptPath}`\n : `node ${scriptPath}`;\n\n // Pass query params and body via environment\n const queryStr = request.url.includes(\"?\") ? (request.url as string).split(\"?\")[1] : \"\";\n const queryObj: Record<string, string> = {};\n if (queryStr) {\n for (const [k, v] of new URLSearchParams(queryStr)) {\n queryObj[k] = v;\n }\n }\n\n const bodyStr = (request.body as string) ?? \"\";\n\n const envVars = [\n `export API_QUERY='${JSON.stringify(queryObj).replace(/'/g, \"'\\\\''\")}'`,\n `export API_BODY='${bodyStr.replace(/'/g, \"'\\\\''\")}'`,\n `export API_METHOD='${request.method}'`,\n `export API_GATEWAY_URL='${request.protocol}://${request.hostname}'`,\n `export API_TENANT_ID='${record.tenantId}'`,\n `export API_ASSISTANT_ID='${record.assistantId || \"\"}'`,\n ].join(\" && \");\n\n const fullCmd = `${envVars} && ${cmd}`;\n\n this.deps.logger.info(\"[share] API exec\", { token: record.token, cmd: cmd, scriptPath });\n\n try {\n const result = await sandbox.shell.exec(fullCmd);\n // Parse stdout as JSON\n let data: unknown;\n try {\n data = JSON.parse(result.stdout.trim() || \"{}\");\n } catch {\n // If stdout is not JSON, wrap it\n data = { output: result.stdout.trim() };\n }\n return reply.status(200).type(\"application/json\").send(data);\n } catch (err) {\n this.deps.logger.warn(\"[share] API exec failed\", { token: record.token, cmd, error: (err as Error).message });\n // Try to extract error from stderr\n const msg = (err as any)?.stderr || (err as Error).message;\n return reply.status(500).send({ error: msg });\n }\n }\n\n private _resolveSafeSubPath(entryFile: string, subPath: string): string {\n if (!subPath) return entryFile;\n if (subPath.includes(\"..\")) throw new Error(\"Path traversal denied\");\n // If entry is a file (has extension), strip filename to get directory.\n // If entry is a directory (no extension), keep it as base.\n const entryDir = this._isFilePath(entryFile)\n ? entryFile.replace(/[^/]+$/, \"\")\n : entryFile + \"/\";\n return (entryDir + subPath).replace(/\\/+/g, \"/\");\n }\n\n private _injectBaseTag(buf: Buffer, token: string): Buffer {\n const html = buf.toString(\"utf-8\");\n if (/<base\\b/i.test(html)) return buf;\n const baseTag = `<base href=\"/s/${token}/\">`;\n if (html.includes(\"</head>\")) {\n return Buffer.from(html.replace(\"</head>\", `${baseTag}</head>`), \"utf-8\");\n }\n // No </head> — insert after <head> or <html> or after <!doctype>\n if (html.includes(\"<head>\")) {\n return Buffer.from(html.replace(\"<head>\", `<head>${baseTag}`), \"utf-8\");\n }\n if (html.includes(\"<html>\")) {\n return Buffer.from(html.replace(\"<html>\", `<html>${baseTag}`), \"utf-8\");\n }\n // Fallback: prepend (will be after <!doctype> if present since html starts with <!doctype)\n return Buffer.from(baseTag + html, \"utf-8\");\n }\n\n private _passwordPage(token: string): string {\n 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>`;\n }\n}\n"],"mappings":"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","names":["record","err"]}
|