70 lines
4.0 KiB
TypeScript
70 lines
4.0 KiB
TypeScript
import { expect, it } from "bun:test";
|
|
import express, { type Request } from "express";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdtemp, mkdir, writeFile, rm, symlink } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { config } from "../../src/config.js";
|
|
import { attachTrustedAgentContext, getLocalAgentContext, ProductContextError } from "../../src/context/localContext.js";
|
|
import { registerArtifactRoutes } from "../../src/routes/artifact-routes.js";
|
|
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
|
|
|
|
it("requires verified actor, project and session ownership for exports and rejects symlinks", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalEnv = config.NODE_ENV;
|
|
const originalRoot = config.CONVERSATION_WORKSPACE_ROOT;
|
|
const root = await mkdtemp(join(tmpdir(), "artifact-route-"));
|
|
const store = new SessionMetadataStore(join(root, "metadata"));
|
|
const app = express();
|
|
let calls = 0;
|
|
globalThis.fetch = (async (input: Parameters<typeof fetch>[0], init: Parameters<typeof fetch>[1]) => {
|
|
if (String(input).endsWith("/api/v1/agent-auth-context")) {
|
|
const headers = new Headers(init?.headers);
|
|
return Response.json({ user_id: headers.get("Authorization") === "Bearer OWNER" ? "owner" : "other", project_id: headers.get("X-Project-Id"), network: "supply" });
|
|
}
|
|
return originalFetch(input, init);
|
|
}) as typeof fetch;
|
|
config.NODE_ENV = "production";
|
|
config.CONVERSATION_WORKSPACE_ROOT = root;
|
|
const owner = { header: (name: string) => ({ authorization: "Bearer OWNER", "x-project-id": "project" })[name.toLowerCase() as "authorization" | "x-project-id"] } as Request;
|
|
await attachTrustedAgentContext(owner);
|
|
const context = getLocalAgentContext(owner);
|
|
const session = "export-session";
|
|
await store.ensure({ ...context, sessionId: session });
|
|
const directory = join(root, context.actorKey, context.projectKey, `session-${createHash("sha256").update(session).digest("hex")}`, "workspace");
|
|
await mkdir(join(directory, "files"), { recursive: true });
|
|
const filename = "mcp-11111111-1111-1111-1111-111111111111.zip";
|
|
await writeFile(join(directory, "files", filename), "fixture");
|
|
const router = express.Router();
|
|
router.use(async (req, res, next) => {
|
|
try { await attachTrustedAgentContext(req); next(); }
|
|
catch (error) { res.status(error instanceof ProductContextError ? error.status : 503).end(); }
|
|
});
|
|
registerArtifactRoutes(router, { sessionMetadataStore: store, runtime: {
|
|
ensureSessionWorkspace: async () => { calls++; return directory; },
|
|
} });
|
|
app.use("/api/v1", router);
|
|
const server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/api/v1/session/${session}/files/${filename}`;
|
|
const headers = { Authorization: "Bearer OWNER", "X-Project-Id": "project" };
|
|
try {
|
|
expect((await originalFetch(url)).status).toBe(401);
|
|
expect((await originalFetch(url, { headers: { ...headers, Authorization: "Bearer OTHER" } })).status).toBe(404);
|
|
expect((await originalFetch(url, { headers: { ...headers, "X-Project-Id": "other" } })).status).toBe(404);
|
|
expect(calls).toBe(0);
|
|
const response = await originalFetch(url, { headers });
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
|
expect(response.headers.get("Content-Disposition")).toContain("attachment");
|
|
expect(await response.text()).toBe("fixture");
|
|
await rm(join(directory, "files", filename));
|
|
await symlink(join(root, "metadata"), join(directory, "files", filename));
|
|
expect((await originalFetch(url, { headers })).status).toBe(404);
|
|
} finally {
|
|
globalThis.fetch = originalFetch; config.NODE_ENV = originalEnv; config.CONVERSATION_WORKSPACE_ROOT = originalRoot;
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|