202 lines
7.1 KiB
TypeScript
202 lines
7.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
connectionTokenRequestSchema,
|
|
connectToolAppSchema,
|
|
createToolMcpGatewayTokenSchema,
|
|
createToolConnectionSchema,
|
|
startConnectionAuthorizationSchema,
|
|
toolCredentialSecretRefSchema,
|
|
toolRedactedValueSummarySchema,
|
|
toolTransportConfigSchema,
|
|
} from "./tool-access.js";
|
|
|
|
describe("tool access validators", () => {
|
|
it("treats a gateway token owner note as optional", () => {
|
|
const parsed = createToolMcpGatewayTokenSchema.parse({
|
|
name: "cursor-client",
|
|
clientLabel: "cursor-client",
|
|
expiresAt: "2026-12-01T00:00:00.000Z",
|
|
});
|
|
|
|
expect(parsed.ownerNote).toBe("");
|
|
});
|
|
|
|
it("defaults connection token subjects to app", () => {
|
|
expect(connectionTokenRequestSchema.parse({})).toEqual({ subject: { type: "app" } });
|
|
});
|
|
|
|
it("accepts user subjects, grant selection, and authorization input", () => {
|
|
const request = connectionTokenRequestSchema.parse({
|
|
subject: { type: "user", userId: "user-123" },
|
|
grantId: "11111111-1111-4111-8111-111111111111",
|
|
});
|
|
expect(request.subject).toEqual({ type: "user", userId: "user-123" });
|
|
expect(startConnectionAuthorizationSchema.parse({ subjectUserId: "user-123", scopes: ["read"] })).toEqual({
|
|
subjectUserId: "user-123",
|
|
scopes: ["read"],
|
|
});
|
|
});
|
|
|
|
it("accepts multi-key credential annotations", () => {
|
|
const parsed = toolCredentialSecretRefSchema.parse({
|
|
secretId: "11111111-1111-4111-8111-111111111111",
|
|
configPath: "credentials.apiKey",
|
|
keyScope: "production",
|
|
expiresAt: "2027-01-01T00:00:00Z",
|
|
});
|
|
expect(parsed.keyScope).toBe("production");
|
|
});
|
|
it("rejects raw credential-looking fields in transport config", () => {
|
|
const parsed = toolTransportConfigSchema.safeParse({
|
|
url: "https://example.test/mcp",
|
|
headers: {
|
|
Authorization: "Bearer raw-token",
|
|
},
|
|
});
|
|
|
|
expect(parsed.success).toBe(false);
|
|
if (!parsed.success) {
|
|
expect(parsed.error.issues[0]?.message).toContain("credentialSecretRefs");
|
|
}
|
|
});
|
|
|
|
it("keeps app method configuration separate from secrets", () => {
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "posthog",
|
|
connectionMethodKey: "mcp-api-key",
|
|
configValues: { projectId: "12345", readOnly: true, features: "insights" },
|
|
}).success).toBe(true);
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "posthog",
|
|
configValues: { projectId: "12345", apiKey: "phx_raw" },
|
|
}).success).toBe(false);
|
|
});
|
|
|
|
it("accepts only UUID connection request links during app setup", () => {
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "posthog",
|
|
interactionId: "11111111-1111-4111-8111-111111111111",
|
|
}).success).toBe(true);
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "posthog",
|
|
interactionId: "not-an-interaction",
|
|
}).success).toBe(false);
|
|
});
|
|
|
|
it("accepts a curated provider-generated URL while still requiring a source", () => {
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "zapier",
|
|
connectionMethodKey: "generated-url",
|
|
link: "https://mcp.zapier.com/api/v1/connect?token=secret-token",
|
|
}).success).toBe(true);
|
|
expect(connectToolAppSchema.safeParse({}).success).toBe(false);
|
|
});
|
|
|
|
// PAP-17087: the guided generic flow and paste-config both reach the connect
|
|
// endpoint, so unsafe header names/values are rejected once at this boundary.
|
|
it("accepts generic advanced-authentication input for a pasted URL", () => {
|
|
const parsed = connectToolAppSchema.safeParse({
|
|
link: "https://mcp.example.test/mcp",
|
|
authMode: "custom_headers",
|
|
credentialValues: {
|
|
"headers.X-Api-Key": "phx_abc123",
|
|
"headers.X-PostHog-Project-Id": "12345",
|
|
},
|
|
});
|
|
expect(parsed.success).toBe(true);
|
|
|
|
const manualClient = connectToolAppSchema.safeParse({
|
|
link: "https://mcp.example.test/mcp",
|
|
authMode: "oauth",
|
|
oauthClient: { clientId: "client-abc", clientSecret: "shhh" },
|
|
});
|
|
expect(manualClient.success).toBe(true);
|
|
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "asana",
|
|
oauthClient: { clientId: "customer-client", clientSecret: "customer-secret" },
|
|
}).success).toBe(true);
|
|
});
|
|
|
|
it("rejects header credentials Paperclip refuses to send", () => {
|
|
for (const configPath of ["headers.Host", "headers.Cookie", "headers.Transfer-Encoding", "headers.Sec-Fetch-Mode"]) {
|
|
const parsed = connectToolAppSchema.safeParse({
|
|
link: "https://mcp.example.test/mcp",
|
|
credentialValues: { [configPath]: "value" },
|
|
});
|
|
expect(parsed.success, configPath).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("rejects header names and values that could split the outbound request", () => {
|
|
const badName = connectToolAppSchema.safeParse({
|
|
link: "https://mcp.example.test/mcp",
|
|
credentialValues: { "headers.X-Bad\r\nX-Injected": "value" },
|
|
});
|
|
expect(badName.success).toBe(false);
|
|
|
|
const badValue = connectToolAppSchema.safeParse({
|
|
link: "https://mcp.example.test/mcp",
|
|
credentialValues: { "headers.X-Api-Key": "abc\r\nX-Injected: 1" },
|
|
});
|
|
expect(badValue.success).toBe(false);
|
|
if (!badValue.success) {
|
|
// The message names the header but must never echo the rejected value.
|
|
const message = badValue.error.issues[0]?.message ?? "";
|
|
expect(message).toContain("X-Api-Key");
|
|
expect(message).not.toContain("X-Injected");
|
|
}
|
|
});
|
|
|
|
it("keeps generic auth-mode selection off curated apps while allowing owned OAuth clients", () => {
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "posthog",
|
|
authMode: "bearer",
|
|
}).success).toBe(false);
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "posthog",
|
|
oauthClient: { clientId: "client-abc" },
|
|
}).success).toBe(true);
|
|
});
|
|
|
|
it("allows only curated app setup to resume an exact draft", () => {
|
|
const resumeConnectionId = "11111111-1111-4111-8111-111111111111";
|
|
expect(connectToolAppSchema.safeParse({
|
|
galleryKey: "notion",
|
|
resumeConnectionId,
|
|
}).success).toBe(true);
|
|
expect(connectToolAppSchema.safeParse({
|
|
link: "https://mcp.example.test/mcp",
|
|
resumeConnectionId,
|
|
}).success).toBe(false);
|
|
});
|
|
|
|
it("accepts secret references for connection credentials", () => {
|
|
const parsed = createToolConnectionSchema.safeParse({
|
|
applicationId: "11111111-1111-4111-8111-111111111111",
|
|
name: "GitHub fixture",
|
|
connectionKind: "managed",
|
|
transportConfig: { url: "https://example.test/mcp" },
|
|
credentialSecretRefs: [
|
|
{
|
|
secretId: "22222222-2222-4222-8222-222222222222",
|
|
configPath: "headers.Authorization",
|
|
versionSelector: "latest",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(parsed.success).toBe(true);
|
|
});
|
|
|
|
it("keeps invocation payload summaries redacted and bounded", () => {
|
|
const parsed = toolRedactedValueSummarySchema.parse({
|
|
summary: "Redacted arguments: 2 fields omitted.",
|
|
sha256: "a".repeat(64),
|
|
redactedFields: ["headers.Authorization", "body.token"],
|
|
});
|
|
|
|
expect(parsed.redactedFields).toEqual(["headers.Authorization", "body.token"]);
|
|
});
|
|
});
|