@axiom-lattice/gateway 2.1.110 → 2.1.112
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 +182 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +188 -66
- package/dist/index.mjs.map +1 -1
- package/dist/{resources-5POCLGXV.mjs → resources-VA7LSDKN.mjs} +2 -1
- package/dist/{resources-5POCLGXV.mjs.map → resources-VA7LSDKN.mjs.map} +1 -1
- package/package.json +5 -6
- package/src/controllers/__tests__/resources.test.ts +23 -0
- package/src/controllers/__tests__/workflow-tracking.test.ts +113 -0
- package/src/controllers/connections.ts +7 -0
- package/src/controllers/eval.ts +44 -4
- package/src/controllers/resources.ts +1 -2
- package/src/controllers/workflow-tracking.ts +105 -11
- package/src/index.ts +14 -8
- package/src/routes/index.ts +8 -1
- package/src/services/eval-runner.ts +63 -55
|
@@ -347,6 +347,7 @@ var ResourceController = class {
|
|
|
347
347
|
_resolveSafeSubPath(entryFile, subPath) {
|
|
348
348
|
if (!subPath) return entryFile;
|
|
349
349
|
if (subPath.includes("..")) throw new Error("Path traversal denied");
|
|
350
|
+
if (subPath.startsWith(entryFile + "/") || subPath === entryFile) return subPath;
|
|
350
351
|
const entryDir = this._isFilePath(entryFile) ? entryFile.replace(/[^/]+$/, "") : entryFile + "/";
|
|
351
352
|
return (entryDir + subPath).replace(/\/+/g, "/");
|
|
352
353
|
}
|
|
@@ -357,4 +358,4 @@ var ResourceController = class {
|
|
|
357
358
|
export {
|
|
358
359
|
ResourceController
|
|
359
360
|
};
|
|
360
|
-
//# sourceMappingURL=resources-
|
|
361
|
+
//# sourceMappingURL=resources-VA7LSDKN.mjs.map
|
|
@@ -1 +1 @@
|
|
|
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 buildNamedVolumeName,\n} from \"@axiom-lattice/core\";\nimport { getContentTypeFromFilename } 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.filter((r: any) => (r as any).visibility !== \"internal\"));\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 const isApi = subPath && /(^|\\/)api\\//.test(subPath) && (subPath.endsWith(\".js\") || subPath.endsWith(\".py\"));\n\n // Only API paths accept non-GET methods\n if (request.method !== \"GET\" && !isApi) {\n return reply.status(405).send({ error: \"Method not allowed\" });\n }\n\n // API runtime — proxy to sandbox. Detected by /api/ anywhere in subPath.\n if (isApi) {\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)) {\n if (record.visibility === \"internal\") {\n return reply.status(401).send({ error: \"Internal access only\" });\n }\n if (record.visibility === \"password\") {\n if (!this._isUnlocked(request, token)) {\n return reply.status(401).send({ error: \"Password required\" });\n }\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 // Internal shares: must be logged in\n if (record.visibility === \"internal\") {\n return reply.status(401).send({ error: \"Internal access only\" });\n }\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 /**\n * Thin redirect: /f/{uuidHex3}/{path} → /s/{internal-token}/{path}\n * Decodes 3 UUIDs (no dashes, 96 hex chars), ensures internal share, 302 redirects.\n */\n async resolveDirectFile(request: FastifyRequest, reply: FastifyReply) {\n const params = request.params as unknown as { tid: string; wid: string; pid: string; \"*\"?: string };\n const tenantId = params.tid;\n const workspaceId = params.wid;\n const projectId = params.pid;\n const subPath = (params[\"*\"] ?? \"\");\n\n const isApi = subPath && /(^|\\/)api\\//.test(subPath) && (subPath.endsWith(\".js\") || subPath.endsWith(\".py\"));\n\n // Only API paths accept non-GET methods\n if (request.method !== \"GET\" && !isApi) {\n return reply.status(405).send({ error: \"Method not allowed\" });\n }\n\n // API runtime\n if (isApi) {\n const ext = subPath.endsWith(\".py\") ? \"py\" : \"js\";\n return this._execApiCore({\n tenantId, workspaceId, projectId, assistantId: \"\",\n resourcePath: subPath,\n }, subPath, ext, request, reply);\n }\n\n const volume = buildNamedVolumeName(\"p\", \"project\", tenantId, workspaceId, projectId);\n return this._resolveAndServe(volume, subPath, reply, `/f/${tenantId}/${workspaceId}/${projectId}`,\n { tenantId, workspaceId, projectId, assistantId: null },\n );\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 const user = (request as any).user;\n if (user && user.id) return true;\n // Fallback: same-origin browser request (iframe/img embed — no Auth header)\n const origin = request.headers.origin || request.headers.referer;\n if (origin) {\n try {\n const { hostname } = new URL(origin);\n if (hostname === request.hostname) return true;\n } catch { /* malformed */ }\n }\n return false;\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 /**\n * Shared file resolution: directory → index.html, volume FS → sandbox, base tag, response.\n */\n private async _resolveAndServe(\n volume: string,\n resourcePath: string,\n reply: FastifyReply,\n baseTagPrefix: string,\n sandboxConfig?: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },\n ) {\n const cleanPath = resourcePath.replace(/\\/+$/, \"\");\n const entryPath = !cleanPath\n ? \"index.html\"\n : this._isFilePath(cleanPath)\n ? cleanPath\n : `${cleanPath}/index.html`;\n\n const address: ResourceAddress = { volume, resourcePath: \"\" };\n\n let buf: Buffer;\n try {\n // Volume FS first\n const provider = this.deps.sandboxManager.getDefaultProvider();\n buf = await provider.getResourceResolver().resolve({ ...address, resourcePath: entryPath });\n } catch {\n // Sandbox fallback\n if (!sandboxConfig) return reply.status(404).send(\"File not found\");\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 } catch {\n return reply.status(404).send(\"File not found\");\n }\n }\n\n const filename = entryPath.split(\"/\").pop() || \"download\";\n const contentType = getContentTypeFromFilename(filename);\n\n // Inject base tag for HTML\n if (/\\.(html|htm)$/i.test(filename)) {\n const baseDir = cleanPath\n ? (this._isFilePath(cleanPath) ? cleanPath.replace(/[^/]+$/, \"\") : cleanPath)\n : \"\";\n const html = buf.toString(\"utf-8\");\n if (!/<base\\b/i.test(html)) {\n const baseTag = `<base href=\"${baseTagPrefix}${baseDir ? \"/\" + baseDir : \"\"}/\">`;\n buf = Buffer.from(\n html.includes(\"<head>\") ? html.replace(\"<head>\", `<head>${baseTag}`) : baseTag + html,\n \"utf-8\"\n );\n }\n }\n\n return reply\n .status(200)\n .type(contentType)\n .header(\"Content-Disposition\", `inline; filename*=UTF-8''${encodeURIComponent(filename)}`)\n .header(\"Access-Control-Allow-Origin\", \"*\")\n .send(buf);\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 resourcePath = subPath\n ? this._resolveSafeSubPath(address.resourcePath, subPath)\n : address.resourcePath;\n return this._resolveAndServe(\n address.volume,\n resourcePath,\n reply,\n `/s/${token}`,\n sandboxConfig,\n );\n }\n\n private async _execApi(\n record: ShareRecord,\n apiSubPath: string,\n ext: \"js\" | \"py\",\n request: FastifyRequest,\n reply: FastifyReply,\n ) {\n const basePath = this._isFilePath(record.address.resourcePath)\n ? \"\"\n : `${record.address.resourcePath}/`;\n return this._execApiCore({\n tenantId: record.tenantId,\n workspaceId: record.workspaceId,\n projectId: record.projectId,\n assistantId: record.assistantId,\n resourcePath: basePath + apiSubPath,\n }, apiSubPath, ext, request, reply);\n }\n\n private async _execApiCore(\n config: { tenantId: string; workspaceId: string; projectId: string; assistantId?: string | null; resourcePath: string },\n apiSubPath: string,\n ext: \"js\" | \"py\",\n request: FastifyRequest,\n reply: FastifyReply,\n ) {\n let sandbox: any;\n try {\n sandbox = await this.deps.sandboxManager.getSandboxFromConfig({\n assistant_id: config.assistantId ?? \"\",\n thread_id: \"\",\n tenantId: config.tenantId,\n workspaceId: config.workspaceId,\n projectId: config.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 const scriptPath = `/project/${config.resourcePath}`;\n const cmd = ext === \"py\" ? `python3 ${scriptPath}` : `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='${config.tenantId}'`,\n `export API_ASSISTANT_ID='${config.assistantId || \"\"}'`,\n ].join(\" && \");\n\n const fullCmd = `${envVars} && ${cmd}`;\n\n this.deps.logger.info(\"[api] exec\", { scriptPath, cmd: cmd.substring(0, 80) });\n\n try {\n const result = await sandbox.shell.execCommand({ command: fullCmd });\n // Parse stdout as JSON\n let data: unknown;\n try {\n data = JSON.parse(result.output?.trim() || \"{}\");\n } catch {\n data = { output: result.output?.trim() || \"\" };\n }\n return reply.status(200).type(\"application/json\").send(data);\n } catch (err) {\n this.deps.logger.warn(\"[api] exec failed\", { scriptPath, error: (err as Error).message });\n return reply.status(500).send({ error: (err as Error).message });\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 _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"]}
|
|
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 buildNamedVolumeName,\n} from \"@axiom-lattice/core\";\nimport { getContentTypeFromFilename } 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.filter((r: any) => (r as any).visibility !== \"internal\"));\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 const isApi = subPath && /(^|\\/)api\\//.test(subPath) && (subPath.endsWith(\".js\") || subPath.endsWith(\".py\"));\n\n // Only API paths accept non-GET methods\n if (request.method !== \"GET\" && !isApi) {\n return reply.status(405).send({ error: \"Method not allowed\" });\n }\n\n // API runtime — proxy to sandbox. Detected by /api/ anywhere in subPath.\n if (isApi) {\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)) {\n if (record.visibility === \"internal\") {\n return reply.status(401).send({ error: \"Internal access only\" });\n }\n if (record.visibility === \"password\") {\n if (!this._isUnlocked(request, token)) {\n return reply.status(401).send({ error: \"Password required\" });\n }\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 // Internal shares: must be logged in\n if (record.visibility === \"internal\") {\n return reply.status(401).send({ error: \"Internal access only\" });\n }\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 /**\n * Thin redirect: /f/{uuidHex3}/{path} → /s/{internal-token}/{path}\n * Decodes 3 UUIDs (no dashes, 96 hex chars), ensures internal share, 302 redirects.\n */\n async resolveDirectFile(request: FastifyRequest, reply: FastifyReply) {\n const params = request.params as unknown as { tid: string; wid: string; pid: string; \"*\"?: string };\n const tenantId = params.tid;\n const workspaceId = params.wid;\n const projectId = params.pid;\n const subPath = (params[\"*\"] ?? \"\");\n\n const isApi = subPath && /(^|\\/)api\\//.test(subPath) && (subPath.endsWith(\".js\") || subPath.endsWith(\".py\"));\n\n // Only API paths accept non-GET methods\n if (request.method !== \"GET\" && !isApi) {\n return reply.status(405).send({ error: \"Method not allowed\" });\n }\n\n // API runtime\n if (isApi) {\n const ext = subPath.endsWith(\".py\") ? \"py\" : \"js\";\n return this._execApiCore({\n tenantId, workspaceId, projectId, assistantId: \"\",\n resourcePath: subPath,\n }, subPath, ext, request, reply);\n }\n\n const volume = buildNamedVolumeName(\"p\", \"project\", tenantId, workspaceId, projectId);\n return this._resolveAndServe(volume, subPath, reply, `/f/${tenantId}/${workspaceId}/${projectId}`,\n { tenantId, workspaceId, projectId, assistantId: null },\n );\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 const user = (request as any).user;\n if (user && user.id) return true;\n // Fallback: same-origin browser request (iframe/img embed — no Auth header)\n const origin = request.headers.origin || request.headers.referer;\n if (origin) {\n try {\n const { hostname } = new URL(origin);\n if (hostname === request.hostname) return true;\n } catch { /* malformed */ }\n }\n return false;\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 /**\n * Shared file resolution: directory → index.html, volume FS → sandbox, base tag, response.\n */\n private async _resolveAndServe(\n volume: string,\n resourcePath: string,\n reply: FastifyReply,\n baseTagPrefix: string,\n sandboxConfig?: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },\n ) {\n const cleanPath = resourcePath.replace(/\\/+$/, \"\");\n const entryPath = !cleanPath\n ? \"index.html\"\n : this._isFilePath(cleanPath)\n ? cleanPath\n : `${cleanPath}/index.html`;\n\n const address: ResourceAddress = { volume, resourcePath: \"\" };\n\n let buf: Buffer;\n try {\n // Volume FS first\n const provider = this.deps.sandboxManager.getDefaultProvider();\n buf = await provider.getResourceResolver().resolve({ ...address, resourcePath: entryPath });\n } catch {\n // Sandbox fallback\n if (!sandboxConfig) return reply.status(404).send(\"File not found\");\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 } catch {\n return reply.status(404).send(\"File not found\");\n }\n }\n\n const filename = entryPath.split(\"/\").pop() || \"download\";\n const contentType = getContentTypeFromFilename(filename);\n\n // Inject base tag for HTML\n if (/\\.(html|htm)$/i.test(filename)) {\n const baseDir = cleanPath\n ? (this._isFilePath(cleanPath) ? cleanPath.replace(/[^/]+$/, \"\") : cleanPath)\n : \"\";\n const html = buf.toString(\"utf-8\");\n if (!/<base\\b/i.test(html)) {\n const baseTag = `<base href=\"${baseTagPrefix}${baseDir ? \"/\" + baseDir : \"\"}/\">`;\n buf = Buffer.from(\n html.includes(\"<head>\") ? html.replace(\"<head>\", `<head>${baseTag}`) : baseTag + html,\n \"utf-8\"\n );\n }\n }\n\n return reply\n .status(200)\n .type(contentType)\n .header(\"Content-Disposition\", `inline; filename*=UTF-8''${encodeURIComponent(filename)}`)\n .header(\"Access-Control-Allow-Origin\", \"*\")\n .send(buf);\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 resourcePath = subPath\n ? this._resolveSafeSubPath(address.resourcePath, subPath)\n : address.resourcePath;\n return this._resolveAndServe(\n address.volume,\n resourcePath,\n reply,\n `/s/${token}`,\n sandboxConfig,\n );\n }\n\n private async _execApi(\n record: ShareRecord,\n apiSubPath: string,\n ext: \"js\" | \"py\",\n request: FastifyRequest,\n reply: FastifyReply,\n ) {\n const basePath = this._isFilePath(record.address.resourcePath)\n ? \"\"\n : `${record.address.resourcePath}/`;\n return this._execApiCore({\n tenantId: record.tenantId,\n workspaceId: record.workspaceId,\n projectId: record.projectId,\n assistantId: record.assistantId,\n resourcePath: basePath + apiSubPath,\n }, apiSubPath, ext, request, reply);\n }\n\n private async _execApiCore(\n config: { tenantId: string; workspaceId: string; projectId: string; assistantId?: string | null; resourcePath: string },\n apiSubPath: string,\n ext: \"js\" | \"py\",\n request: FastifyRequest,\n reply: FastifyReply,\n ) {\n let sandbox: any;\n try {\n sandbox = await this.deps.sandboxManager.getSandboxFromConfig({\n assistant_id: config.assistantId ?? \"\",\n thread_id: \"\",\n tenantId: config.tenantId,\n workspaceId: config.workspaceId,\n projectId: config.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 const scriptPath = `/project/${config.resourcePath}`;\n const cmd = ext === \"py\" ? `python3 ${scriptPath}` : `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='${config.tenantId}'`,\n `export API_ASSISTANT_ID='${config.assistantId || \"\"}'`,\n ].join(\" && \");\n\n const fullCmd = `${envVars} && ${cmd}`;\n\n this.deps.logger.info(\"[api] exec\", { scriptPath, cmd: cmd.substring(0, 80) });\n\n try {\n const result = await sandbox.shell.execCommand({ command: fullCmd });\n // Parse stdout as JSON\n let data: unknown;\n try {\n data = JSON.parse(result.output?.trim() || \"{}\");\n } catch {\n data = { output: result.output?.trim() || \"\" };\n }\n return reply.status(200).type(\"application/json\").send(data);\n } catch (err) {\n this.deps.logger.warn(\"[api] exec failed\", { scriptPath, error: (err as Error).message });\n return reply.status(500).send({ error: (err as Error).message });\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 (subPath.startsWith(entryFile + \"/\") || subPath === entryFile) return subPath;\n const entryDir = this._isFilePath(entryFile)\n ? entryFile.replace(/[^/]+$/, \"\")\n : entryFile + \"/\";\n return (entryDir + subPath).replace(/\\/+/g, \"/\");\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.112",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -41,11 +41,10 @@
|
|
|
41
41
|
"redis": "^5.0.1",
|
|
42
42
|
"uuid": "^9.0.1",
|
|
43
43
|
"zod": "3.25.76",
|
|
44
|
-
"@axiom-lattice/
|
|
45
|
-
"@axiom-lattice/
|
|
46
|
-
"@axiom-lattice/
|
|
47
|
-
"@axiom-lattice/
|
|
48
|
-
"@axiom-lattice/queue-redis": "1.0.49"
|
|
44
|
+
"@axiom-lattice/core": "2.1.99",
|
|
45
|
+
"@axiom-lattice/pg-stores": "1.0.90",
|
|
46
|
+
"@axiom-lattice/protocols": "2.1.51",
|
|
47
|
+
"@axiom-lattice/queue-redis": "1.0.50"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
51
50
|
"@types/bcrypt": "^6.0.0",
|
|
@@ -63,6 +63,29 @@ describe("ResourceController._resolveSafeSubPath", () => {
|
|
|
63
63
|
it("deduplicates consecutive slashes", () => {
|
|
64
64
|
expect(ctrl._resolveSafeSubPath("app/index.html", "extra/slash")).toBe("app/extra/slash");
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
it("does not double prefix when subPath already starts with directory entry", () => {
|
|
68
|
+
// Directory share: base tag /s/TOKEN/dir/
|
|
69
|
+
// Browser resolves style.css → subPath already includes the entry directory
|
|
70
|
+
expect(ctrl._resolveSafeSubPath("knowledge-graph-visualization", "knowledge-graph-visualization/style.css"))
|
|
71
|
+
.toBe("knowledge-graph-visualization/style.css");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not double prefix when subPath exactly matches directory entry", () => {
|
|
75
|
+
// Accessing /s/TOKEN/knowledge-graph-visualization (no trailing slash)
|
|
76
|
+
expect(ctrl._resolveSafeSubPath("knowledge-graph-visualization", "knowledge-graph-visualization"))
|
|
77
|
+
.toBe("knowledge-graph-visualization");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not double prefix for deeply nested assets", () => {
|
|
81
|
+
expect(ctrl._resolveSafeSubPath("dir", "dir/sub/file.js"))
|
|
82
|
+
.toBe("dir/sub/file.js");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("still appends subPath when it does NOT start with entry", () => {
|
|
86
|
+
// File share: base tag /s/TOKEN/ → subPath is bare filename
|
|
87
|
+
expect(ctrl._resolveSafeSubPath("app/index.html", "style.css")).toBe("app/style.css");
|
|
88
|
+
});
|
|
66
89
|
});
|
|
67
90
|
|
|
68
91
|
describe("ResourceController._injectBaseTag", () => {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
const mockQueryWorkflowRuns = jest.fn();
|
|
4
|
+
const mockGetRunSteps = jest.fn();
|
|
5
|
+
const mockGetAllAssistants = jest.fn();
|
|
6
|
+
const mockGetAgent = jest.fn();
|
|
7
|
+
|
|
8
|
+
jest.mock("@axiom-lattice/core", () => ({
|
|
9
|
+
getStoreLattice: (_key: string, type: string) =>
|
|
10
|
+
type === "workflowTracking"
|
|
11
|
+
? { store: { queryWorkflowRuns: mockQueryWorkflowRuns, getRunSteps: mockGetRunSteps } }
|
|
12
|
+
: { store: { getAllAssistants: mockGetAllAssistants } },
|
|
13
|
+
agentInstanceManager: { getAgent: mockGetAgent },
|
|
14
|
+
ThreadStatus: { IDLE: "IDLE", BUSY: "BUSY", INTERRUPTED: "INTERRUPTED" },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function makeRun(id: string, startedAt: string) {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
tenantId: "t1",
|
|
21
|
+
assistantId: "a1",
|
|
22
|
+
threadId: `th_${id}`,
|
|
23
|
+
status: "interrupted" as const,
|
|
24
|
+
topologyEdges: [],
|
|
25
|
+
totalEdges: 2,
|
|
26
|
+
completedEdges: 1,
|
|
27
|
+
startedAt: new Date(startedAt),
|
|
28
|
+
createdAt: new Date(startedAt),
|
|
29
|
+
updatedAt: new Date(startedAt),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeRequest(query: Record<string, string> = {}) {
|
|
34
|
+
return {
|
|
35
|
+
headers: { "x-tenant-id": "t1" },
|
|
36
|
+
query,
|
|
37
|
+
log: { error: jest.fn(), warn: jest.fn() },
|
|
38
|
+
} as never;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const mockReply = { status: jest.fn().mockReturnThis(), send: jest.fn() } as never;
|
|
42
|
+
|
|
43
|
+
describe("getInboxItems", () => {
|
|
44
|
+
let getInboxItems: (req: never, reply: never) => Promise<{ data?: Record<string, unknown> }>;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
mockGetAllAssistants.mockResolvedValue([{ id: "a1", name: "Assistant One" }] as never);
|
|
49
|
+
mockGetRunSteps.mockResolvedValue([] as never);
|
|
50
|
+
mockGetAgent.mockImplementation(() => { throw new Error("no agent"); }); // fallback path
|
|
51
|
+
const mod = await import("../../controllers/workflow-tracking");
|
|
52
|
+
getInboxItems = mod.getInboxItems as never;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("legacy mode: no query params returns all items without pagination fields", async () => {
|
|
56
|
+
mockQueryWorkflowRuns.mockResolvedValue({
|
|
57
|
+
records: [makeRun("r1", "2026-01-01"), makeRun("r2", "2026-01-02")],
|
|
58
|
+
total: 2,
|
|
59
|
+
} as never);
|
|
60
|
+
|
|
61
|
+
const res = await getInboxItems(makeRequest(), mockReply);
|
|
62
|
+
|
|
63
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted" });
|
|
64
|
+
expect(res.data).toBeDefined();
|
|
65
|
+
const records = res.data!.records as unknown[];
|
|
66
|
+
expect(records).toHaveLength(2);
|
|
67
|
+
expect(res.data!.total).toBeUndefined();
|
|
68
|
+
expect(res.data!.page).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("paged mode: passes limit/offset to store and returns total/page/pageSize", async () => {
|
|
72
|
+
mockQueryWorkflowRuns.mockResolvedValue({
|
|
73
|
+
records: [makeRun("r3", "2026-01-03")],
|
|
74
|
+
total: 42,
|
|
75
|
+
} as never);
|
|
76
|
+
|
|
77
|
+
const res = await getInboxItems(makeRequest({ page: "2", pageSize: "20" }), mockReply);
|
|
78
|
+
|
|
79
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted", limit: 20, offset: 20 });
|
|
80
|
+
expect(res.data!.total).toBe(42);
|
|
81
|
+
expect(res.data!.page).toBe(2);
|
|
82
|
+
expect(res.data!.pageSize).toBe(20);
|
|
83
|
+
expect(res.data!.records).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("paged mode triggers when only pageSize is passed", async () => {
|
|
87
|
+
mockQueryWorkflowRuns.mockResolvedValue({ records: [], total: 0 } as never);
|
|
88
|
+
|
|
89
|
+
const res = await getInboxItems(makeRequest({ pageSize: "10" }), mockReply);
|
|
90
|
+
|
|
91
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted", limit: 10, offset: 0 });
|
|
92
|
+
expect(res.data!.page).toBe(1);
|
|
93
|
+
expect(res.data!.pageSize).toBe(10);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("sanitizes invalid page and caps pageSize at 100", async () => {
|
|
97
|
+
mockQueryWorkflowRuns.mockResolvedValue({ records: [], total: 0 } as never);
|
|
98
|
+
|
|
99
|
+
const res = await getInboxItems(makeRequest({ page: "abc", pageSize: "500" }), mockReply);
|
|
100
|
+
|
|
101
|
+
expect(mockQueryWorkflowRuns).toHaveBeenCalledWith("t1", { status: "interrupted", limit: 100, offset: 0 });
|
|
102
|
+
expect(res.data!.page).toBe(1);
|
|
103
|
+
expect(res.data!.pageSize).toBe(100);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns empty records with pagination fields in paged mode", async () => {
|
|
107
|
+
mockQueryWorkflowRuns.mockResolvedValue({ records: [], total: 0 } as never);
|
|
108
|
+
|
|
109
|
+
const res = await getInboxItems(makeRequest({ page: "3", pageSize: "20" }), mockReply);
|
|
110
|
+
|
|
111
|
+
expect(res.data).toEqual({ records: [], total: 0, page: 3, pageSize: 20 });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -16,6 +16,13 @@ const testOrDiscoverBody = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
function getTenant(request: FastifyRequest): string {
|
|
19
|
+
// First try to get from authenticated user context
|
|
20
|
+
const userTenantId = (request as any).user?.tenantId;
|
|
21
|
+
if (userTenantId) {
|
|
22
|
+
return userTenantId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Fallback to request headers for backward compatibility
|
|
19
26
|
return (request.headers["x-tenant-id"] as string) || "default";
|
|
20
27
|
}
|
|
21
28
|
|
package/src/controllers/eval.ts
CHANGED
|
@@ -25,12 +25,27 @@ export async function createProject(
|
|
|
25
25
|
const store = getEvalStore();
|
|
26
26
|
const id = uuidv4();
|
|
27
27
|
const data = request.body as any;
|
|
28
|
+
const serverCfg = (data.targetServerConfig || {}) as Record<string, unknown>;
|
|
29
|
+
const judgeCfg = (data.judgeModelConfig || {}) as Record<string, unknown>;
|
|
30
|
+
const modelKey = (judgeCfg.modelKey as string) || "";
|
|
31
|
+
if (modelKey) {
|
|
32
|
+
const { modelLatticeManager } = await import("@axiom-lattice/core");
|
|
33
|
+
if (!modelLatticeManager.hasLattice(modelKey)) {
|
|
34
|
+
return reply.status(400).send({ success: false, message: `Judge model "${modelKey}" is not registered` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
const project = await store.createProject(tenantId, id, {
|
|
29
38
|
name: data.name,
|
|
30
39
|
description: data.description,
|
|
31
40
|
version: data.version,
|
|
32
|
-
judgeModelConfig:
|
|
33
|
-
|
|
41
|
+
judgeModelConfig: {
|
|
42
|
+
modelKey,
|
|
43
|
+
displayName: (judgeCfg.displayName as string) || "",
|
|
44
|
+
},
|
|
45
|
+
targetServerConfig: {
|
|
46
|
+
base_url: (serverCfg.base_url as string) || "",
|
|
47
|
+
api_key: (serverCfg.api_key as string) || "",
|
|
48
|
+
},
|
|
34
49
|
concurrency: data.concurrency ?? 1,
|
|
35
50
|
reportConfig: data.reportConfig,
|
|
36
51
|
});
|
|
@@ -62,7 +77,7 @@ export async function listProjects(
|
|
|
62
77
|
const first = models[0];
|
|
63
78
|
const judgeModel = first
|
|
64
79
|
? { modelKey: first.key }
|
|
65
|
-
: {
|
|
80
|
+
: {};
|
|
66
81
|
|
|
67
82
|
const host = request.hostname || "localhost";
|
|
68
83
|
const baseUrl = `http://${host}:${process.env.PORT || 4001}`;
|
|
@@ -125,7 +140,32 @@ export async function updateProject(
|
|
|
125
140
|
if (!existing) {
|
|
126
141
|
return reply.status(404).send({ success: false, message: "Project not found" });
|
|
127
142
|
}
|
|
128
|
-
const
|
|
143
|
+
const body = request.body as Record<string, unknown>;
|
|
144
|
+
const updateData: Record<string, unknown> = { ...body };
|
|
145
|
+
// Strip in_process from user input — it is server-controlled
|
|
146
|
+
const serverCfg = (body.targetServerConfig || {}) as Record<string, unknown>;
|
|
147
|
+
if (body.targetServerConfig !== undefined) {
|
|
148
|
+
updateData.targetServerConfig = {
|
|
149
|
+
base_url: (serverCfg.base_url as string) || "",
|
|
150
|
+
api_key: (serverCfg.api_key as string) || "",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Only allow modelKey/displayName — never raw provider/apiKey/baseURL
|
|
154
|
+
if (body.judgeModelConfig !== undefined) {
|
|
155
|
+
const judgeCfg = (body.judgeModelConfig || {}) as Record<string, unknown>;
|
|
156
|
+
const modelKey = (judgeCfg.modelKey as string) || "";
|
|
157
|
+
if (modelKey) {
|
|
158
|
+
const { modelLatticeManager } = await import("@axiom-lattice/core");
|
|
159
|
+
if (!modelLatticeManager.hasLattice(modelKey)) {
|
|
160
|
+
return reply.status(400).send({ success: false, message: `Judge model "${modelKey}" is not registered` });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
updateData.judgeModelConfig = {
|
|
164
|
+
modelKey,
|
|
165
|
+
displayName: (judgeCfg.displayName as string) || "",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const updated = await store.updateProject(tenantId, pid, updateData);
|
|
129
169
|
return {
|
|
130
170
|
success: true,
|
|
131
171
|
message: "Successfully updated project",
|
|
@@ -464,8 +464,7 @@ export class ResourceController {
|
|
|
464
464
|
private _resolveSafeSubPath(entryFile: string, subPath: string): string {
|
|
465
465
|
if (!subPath) return entryFile;
|
|
466
466
|
if (subPath.includes("..")) throw new Error("Path traversal denied");
|
|
467
|
-
|
|
468
|
-
// If entry is a directory (no extension), keep it as base.
|
|
467
|
+
if (subPath.startsWith(entryFile + "/") || subPath === entryFile) return subPath;
|
|
469
468
|
const entryDir = this._isFilePath(entryFile)
|
|
470
469
|
? entryFile.replace(/[^/]+$/, "")
|
|
471
470
|
: entryFile + "/";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
-
import { getStoreLattice, agentInstanceManager, ThreadStatus } from "@axiom-lattice/core";
|
|
2
|
+
import { getStoreLattice, agentInstanceManager, ThreadStatus, abortWorkflowRun as signalAbort } from "@axiom-lattice/core";
|
|
3
3
|
import type { WorkflowTrackingStore, WorkflowRun, RunStep } from "@axiom-lattice/protocols";
|
|
4
4
|
import { MessageChunkTypes } from "@axiom-lattice/protocols";
|
|
5
5
|
|
|
@@ -189,10 +189,18 @@ export async function getAllWorkflowRuns(
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// GET /api/workflows/inbox
|
|
192
|
+
// Pagination semantics:
|
|
193
|
+
// - No page/pageSize → legacy mode: all interrupted items, data = { records } (unchanged shape).
|
|
194
|
+
// - page and/or pageSize → paged mode: store-level pagination over interrupted runs,
|
|
195
|
+
// data = { records, total, page, pageSize }. total = number of interrupted RUNS
|
|
196
|
+
// (each run expands to 0..N inbox items after the agent-state check).
|
|
197
|
+
const INBOX_DEFAULT_PAGE_SIZE = 20;
|
|
198
|
+
const INBOX_MAX_PAGE_SIZE = 100;
|
|
199
|
+
|
|
192
200
|
export async function getInboxItems(
|
|
193
|
-
request: FastifyRequest
|
|
201
|
+
request: FastifyRequest<{ Querystring: { page?: string; pageSize?: string } }>,
|
|
194
202
|
reply: FastifyReply
|
|
195
|
-
): Promise<ApiResponse<{ records: any[] }>> {
|
|
203
|
+
): Promise<ApiResponse<{ records: any[]; total?: number; page?: number; pageSize?: number }>> {
|
|
196
204
|
const tenantId = getTenantId(request);
|
|
197
205
|
|
|
198
206
|
try {
|
|
@@ -214,10 +222,32 @@ export async function getInboxItems(
|
|
|
214
222
|
// names unavailable, will fall back to ID
|
|
215
223
|
}
|
|
216
224
|
|
|
217
|
-
const
|
|
218
|
-
const
|
|
225
|
+
const { page: pageParam, pageSize: pageSizeParam } = request.query;
|
|
226
|
+
const paged = pageParam !== undefined || pageSizeParam !== undefined;
|
|
227
|
+
const page = Math.max(1, parseInt(pageParam ?? "", 10) || 1);
|
|
228
|
+
const pageSize = Math.min(INBOX_MAX_PAGE_SIZE, Math.max(1, parseInt(pageSizeParam ?? "", 10) || INBOX_DEFAULT_PAGE_SIZE));
|
|
229
|
+
|
|
230
|
+
let pendingRuns: WorkflowRun[];
|
|
231
|
+
let total: number | undefined;
|
|
232
|
+
if (paged) {
|
|
233
|
+
const result = await store.queryWorkflowRuns(tenantId, {
|
|
234
|
+
status: "interrupted",
|
|
235
|
+
limit: pageSize,
|
|
236
|
+
offset: (page - 1) * pageSize,
|
|
237
|
+
});
|
|
238
|
+
pendingRuns = result.records;
|
|
239
|
+
total = result.total;
|
|
240
|
+
} else {
|
|
241
|
+
const result = await store.queryWorkflowRuns(tenantId, { status: "interrupted" });
|
|
242
|
+
pendingRuns = result.records;
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
if (pendingRuns.length === 0) {
|
|
220
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
success: true,
|
|
248
|
+
message: "No pending workflows",
|
|
249
|
+
data: paged ? { records: [], total, page, pageSize } : { records: [] },
|
|
250
|
+
};
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
// Parallelize agent checks across interrupted workflows
|
|
@@ -283,15 +313,13 @@ export async function getInboxItems(
|
|
|
283
313
|
|
|
284
314
|
const results = await Promise.all(checkPromises);
|
|
285
315
|
const inboxItems = results.flat();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
289
|
-
);
|
|
316
|
+
// Runs are already sorted by updatedAt DESC, id DESC from queryWorkflowRuns.
|
|
317
|
+
// flatMap preserves run order within each run.
|
|
290
318
|
|
|
291
319
|
return {
|
|
292
320
|
success: true,
|
|
293
321
|
message: "Successfully retrieved inbox items",
|
|
294
|
-
data: { records: inboxItems },
|
|
322
|
+
data: paged ? { records: inboxItems, total, page, pageSize } : { records: inboxItems },
|
|
295
323
|
};
|
|
296
324
|
} catch (error) {
|
|
297
325
|
request.log.error(error, "Failed to get inbox items");
|
|
@@ -597,3 +625,69 @@ export async function replyInboxTask(
|
|
|
597
625
|
});
|
|
598
626
|
}
|
|
599
627
|
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Abort a running workflow.
|
|
631
|
+
*
|
|
632
|
+
* Stops the main graph execution AND signals all in-progress
|
|
633
|
+
* sub-agent invokes to cancel.
|
|
634
|
+
*/
|
|
635
|
+
export async function abortWorkflowRun(
|
|
636
|
+
request: FastifyRequest,
|
|
637
|
+
reply: FastifyReply
|
|
638
|
+
): Promise<void> {
|
|
639
|
+
try {
|
|
640
|
+
const { runId } = request.params as { runId: string };
|
|
641
|
+
const tenantId = getTenantId(request);
|
|
642
|
+
|
|
643
|
+
const store = getTrackingStore();
|
|
644
|
+
if (!store) {
|
|
645
|
+
return reply.status(404).send({
|
|
646
|
+
success: false,
|
|
647
|
+
message: "No workflow tracking store configured",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const run = await store.getWorkflowRun(runId);
|
|
652
|
+
if (!run) {
|
|
653
|
+
return reply.status(404).send({
|
|
654
|
+
success: false,
|
|
655
|
+
message: "Workflow run not found",
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Persist abort intent FIRST — prevents restart after server crash
|
|
660
|
+
await store.updateWorkflowRun(runId, {
|
|
661
|
+
status: "cancelled",
|
|
662
|
+
errorMessage: "Aborted by user",
|
|
663
|
+
completedAt: new Date(),
|
|
664
|
+
}).catch(() => {});
|
|
665
|
+
|
|
666
|
+
// Stop the main graph execution
|
|
667
|
+
const workspace_id = request.headers["x-workspace-id"] as string;
|
|
668
|
+
const project_id = request.headers["x-project-id"] as string;
|
|
669
|
+
const agent = agentInstanceManager.getAgent({
|
|
670
|
+
assistant_id: run.assistantId,
|
|
671
|
+
thread_id: run.threadId,
|
|
672
|
+
tenant_id: tenantId,
|
|
673
|
+
workspace_id,
|
|
674
|
+
project_id,
|
|
675
|
+
});
|
|
676
|
+
await agent.abort();
|
|
677
|
+
|
|
678
|
+
// Signal sub-agents via AbortController registry
|
|
679
|
+
signalAbort(runId);
|
|
680
|
+
|
|
681
|
+
return reply.status(200).send({
|
|
682
|
+
success: true,
|
|
683
|
+
message: "Workflow aborted",
|
|
684
|
+
data: { runId, assistantId: run.assistantId, threadId: run.threadId },
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
request.log.error(error, "Failed to abort workflow run");
|
|
688
|
+
return reply.status(500).send({
|
|
689
|
+
success: false,
|
|
690
|
+
message: `Abort failed: ${(error as Error).message}`,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,10 +12,11 @@ import type { ChannelInstallationStore, BindingRegistry } from "@axiom-lattice/p
|
|
|
12
12
|
import { MessageRouter } from "./router/MessageRouter";
|
|
13
13
|
import { ChannelAdapterRegistry } from "./channels/registry";
|
|
14
14
|
import { createDeduplicationMiddleware, createRateLimitMiddleware, createAuditLoggerMiddleware } from "./router/middlewares";
|
|
15
|
-
import { setBindingRegistry, setMenuRegistry } from "@axiom-lattice/core";
|
|
15
|
+
import { setBindingRegistry, setMenuRegistry, setEvalRunService } from "@axiom-lattice/core";
|
|
16
16
|
import { larkChannelAdapter } from "./channels/lark/LarkChannelAdapter";
|
|
17
17
|
import { extractUserFromAuthHeader } from "./controllers/auth";
|
|
18
18
|
import { setChannelControllerDeps } from "./controllers/channel-installations";
|
|
19
|
+
import { evalRunner } from "./services/eval-runner";
|
|
19
20
|
import { configureSwagger } from "./swagger";
|
|
20
21
|
import {
|
|
21
22
|
setQueueServiceType,
|
|
@@ -387,6 +388,8 @@ const start = async (config?: LatticeGatewayConfig) => {
|
|
|
387
388
|
});
|
|
388
389
|
}
|
|
389
390
|
|
|
391
|
+
setEvalRunService(evalRunner);
|
|
392
|
+
|
|
390
393
|
// Initialize MenuRegistry for custom menu items
|
|
391
394
|
try {
|
|
392
395
|
const menuStore = getStoreLattice("default", "menu").store;
|
|
@@ -466,13 +469,16 @@ const start = async (config?: LatticeGatewayConfig) => {
|
|
|
466
469
|
}
|
|
467
470
|
|
|
468
471
|
// Restore agent instances with pending messages (fire-and-forget to avoid blocking startup)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
472
|
+
// Temporarily disabled: do not auto-consume pending messages on startup.
|
|
473
|
+
if (process.env.AXIOM_RESTORE_PENDING_ON_STARTUP === "true") {
|
|
474
|
+
agentInstanceManager.restore()
|
|
475
|
+
.then((stats) => {
|
|
476
|
+
logger.info(`Agent recovery complete: ${stats.restored} threads restored, ${stats.errors} errors`);
|
|
477
|
+
})
|
|
478
|
+
.catch((error) => {
|
|
479
|
+
logger.error("Agent recovery failed", { error });
|
|
480
|
+
});
|
|
481
|
+
}
|
|
476
482
|
|
|
477
483
|
// Restore MCP server connections that were connected before shutdown
|
|
478
484
|
restoreMcpConnections().catch((error) => {
|