import { describe, it, expect } from "@effect/vitest";import { Effect, Predicate } from "effect";import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import { ConnectionId, CreateConnectionInput, createExecutor, definePlugin, ElicitationResponse, RemoveSecretInput, Scope, ScopeId, SecretId, TokenMaterial,} from "@executor-js/sdk";import { makeTestConfig, serveTestHttpApp } from "@executor-js/sdk/testing";import { memorySecretsPlugin } from "@executor-js/sdk/testing"; import { graphqlPlugin } from "./plugin";import { endpointForTelemetry } from "./invoke";import { introspect } from "./introspect";import { GRAPHQL_OAUTH_CONNECTION_SLOT, graphqlHeaderSlot, graphqlQueryParamSlot } from "./types";import type { IntrospectionResult } from "./introspect";import { makeGreetingGraphqlSchema, serveGraphqlFailureTestServer, serveGraphqlTestServer,} from "../testing"; const TEST_SCOPE = "test-scope";const graphqlOAuth2Config = { kind: "oauth2" as const, securitySchemeName: "OAuth2", flow: "authorizationCode" as const, tokenUrl: "https://auth.example.test/token", authorizationUrl: "https://auth.example.test/authorize", clientIdSlot: "auth:oauth2:client-id", clientSecretSlot: null, connectionSlot: GRAPHQL_OAUTH_CONNECTION_SLOT, scopes: [],}; // ---------------------------------------------------------------------------// Mock introspection response// --------------------------------------------------------------------------- const introspectionResult: IntrospectionResult = { __schema: { queryType: { name: "Query" }, mutationType: { name: "Mutation" }, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "hello", description: "Say hello", args: [ { name: "name", description: null, type: { kind: "SCALAR", name: "String", ofType: null }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "OBJECT", name: "Mutation", description: null, fields: [ { name: "setGreeting", description: "Set greeting message", args: [ { name: "message", description: null, type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null }, }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], },}; const introspectionJson = JSON.stringify({ data: introspectionResult });const serveGreetingServer = serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema() });const declineAll = () => Effect.succeed(ElicitationResponse.make({ action: "decline" })); const sampleDataPlugin = definePlugin(() => ({ id: "sample-read-test" as const, storage: () => ({}), staticSources: () => [ { id: "sample", kind: "in-memory", name: "Sample", tools: [ { name: "read", description: "Read sample data", inputSchema: { type: "object", properties: {}, additionalProperties: false, }, handler: () => Effect.succeed("sample-value"), }, ], }, ],})); describe("graphqlPlugin real protocol server", () => { it("uses query-free endpoints for invocation attributes", () => { expect(endpointForTelemetry("https://api.example.test/graphql?token=secret#section")).toBe( "https://api.example.test/graphql", ); }); it.effect("includes redacted upstream text in introspection status errors", () => Effect.gen(function* () { const server = yield* serveGraphqlFailureTestServer({ status: 500, body: 'upstream failed {"access_token":"secret-value"} token=another-secret', }); const error = yield* introspect(server.endpoint).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", 'Introspection failed with status 500: upstream failed {"access_token":"[redacted]"} token=[redacted]', ); expect(error).not.toHaveProperty("message", expect.stringContaining("secret-value")); expect(error).not.toHaveProperty("message", expect.stringContaining("another-secret")); }), ); it.effect("includes safe upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Resource protected by organization SSO" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Resource protected by organization SSO", ); }), ); it.effect("redacts secrets from upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Authorization: Bearer github-secret-token" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Authorization: [redacted]", ); expect(error).not.toHaveProperty("message", expect.stringContaining("github-secret-token")); }), ); it.effect("accepts standard introspection responses with omitted deepest ofType", () => Effect.gen(function* () { const deepType = { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", }, }, }, }, }, }; const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe({ data: { __schema: { queryType: { name: "Query" }, mutationType: null, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "deep", description: null, args: [], type: deepType, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], }, }, }), ), ); const result = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), ); expect(result.__schema.queryType?.name).toBe("Query"); }), ); it.effect("adds a source by introspecting the live GraphQL endpoint", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* createExecutor( makeTestConfig({ plugins: [graphqlPlugin()] as const }), ); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "live_graph", }); expect(result).toEqual({ toolCount: 2, namespace: "live_graph" }); const tools = yield* executor.tools.list(); expect(tools.map((tool) => tool.id)).toEqual( expect.arrayContaining(["live_graph.query.hello", "live_graph.mutation.setGreeting"]), ); const requests = yield* server.requests; expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true); }), ); it.effect("uses initial credential bindings for add-time introspection", () => Effect.gen(function* () { const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer secret-token"), }, }); const executor = yield* createExecutor( makeTestConfig({ plugins: [memorySecretsPlugin(), graphqlPlugin()] as const }), ); yield* executor.secrets.set({ id: SecretId.make("github-token"), scope: ScopeId.make(TEST_SCOPE), name: "GitHub token", value: "secret-token", provider: "memory", }); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "initial_credentials", headers: { Authorization: { kind: "secret", prefix: "Bearer " }, }, credentials: { scope: TEST_SCOPE, headers: { Authorization: { kind: "secret", secretId: "github-token" }, }, }, }); expect(result).toEqual({ toolCount: 2, namespace: "initial_credentials" }); const requests = yield* server.requests; expect( requests.some((request) => request.headers.authorization === "Bearer secret-token"), ).toBe(true); }), ); it.effect( "uses user-scoped initial credential bindings for org-scope add-time introspection", () => Effect.gen(function* () { const orgScope = "org-scope"; const userScope = "user-scope"; const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer user-secret-token"), }, }); const executor = yield* createExecutor( makeTestConfig({ scopes: [ Scope.make({ id: ScopeId.make(userScope), name: "user", createdAt: new Date(), }), Scope.make({ id: ScopeId.make(orgScope), name: "org", createdAt: new Date(), }), ], plugins: [memorySecretsPlugin(), graphqlPlugin()] as const, }), ); yield* executor.secrets.set({ id: SecretId.make("github-graphql-authorization"), scope: ScopeId.make(userScope), name: "GitHub GraphQL Authorization", value: "user-secret-token", provider: "memory", }); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: orgScope, name: "Github GraphQL", namespace: "github_graphql", headers: { Authorization: { kind: "secret", prefix: "Bearer " }, }, credentials: { scope: userScope, headers: { Authorization: { kind: "secret", secretId: "github-graphql-authorization", secretScope: userScope, prefix: "Bearer ", }, }, }, }); expect(result).toEqual({ toolCount: 2, namespace: "github_graphql" }); const requests = yield* server.requests; expect( requests.some((request) => request.headers.authorization === "Bearer user-secret-token"), ).toBe(true); }), ); it.effect("marks source oauth-backed when add-time credentials include oauth", () => Effect.gen(function* () { const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer oauth-token"), }, }); const executor = yield* createExecutor( makeTestConfig({ plugins: [memorySecretsPlugin(), graphqlPlugin()] as const }), ); const connectionId = ConnectionId.make("graphql-oauth2-initial"); yield* executor.connections.create( CreateConnectionInput.make({ id: connectionId, scope: ScopeId.make(TEST_SCOPE), provider: "oauth2", identityLabel: "Initial GraphQL OAuth", accessToken: TokenMaterial.make({ secretId: SecretId.make(`${connectionId}.access_token`), name: "Initial GraphQL OAuth Access Token", value: "oauth-token", }), refreshToken: null, expiresAt: null, oauthScope: null, providerState: null, }), ); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "initial_oauth_graphql", credentials: { scope: TEST_SCOPE, auth: { oauth2: { connection: { kind: "connection", connectionId }, }, }, }, }); expect(result).toEqual({ toolCount: 2, namespace: "initial_oauth_graphql" }); const source = yield* executor.graphql.getSource("initial_oauth_graphql", TEST_SCOPE); expect(source?.auth.kind).toBe("oauth2"); if (source?.auth.kind !== "oauth2") return; expect(source.auth.connectionSlot).toBe(GRAPHQL_OAUTH_CONNECTION_SLOT); }), ); it.effect("invokes a live query with headers and query params", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* createExecutor( makeTestConfig({ plugins: [graphqlPlugin()] as const }), ); yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "live_invoke", headers: { "x-static": "abc" }, queryParams: { token: "qp-token" }, }); yield* server.clearRequests; const result = yield* executor.tools.invoke("live_invoke.query.hello", { name: "Ada", }); expect(result).toEqual({ ok: true, data: { hello: "Hello Ada" }, }); Lines 1–500 of 1629. Load more
import { describe, it, expect } from "@effect/vitest"; import { Effect, Predicate } from "effect"; import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import { ConnectionId, CreateConnectionInput, createExecutor, definePlugin, ElicitationResponse, RemoveSecretInput, Scope, ScopeId, SecretId, TokenMaterial, } from "@executor-js/sdk"; import { makeTestConfig, serveTestHttpApp } from "@executor-js/sdk/testing"; import { memorySecretsPlugin } from "@executor-js/sdk/testing"; import { graphqlPlugin } from "./plugin"; import { endpointForTelemetry } from "./invoke"; import { introspect } from "./introspect"; import { GRAPHQL_OAUTH_CONNECTION_SLOT, graphqlHeaderSlot, graphqlQueryParamSlot } from "./types"; import type { IntrospectionResult } from "./introspect"; import { makeGreetingGraphqlSchema, serveGraphqlFailureTestServer, serveGraphqlTestServer, } from "../testing"; const TEST_SCOPE = "test-scope"; const graphqlOAuth2Config = { kind: "oauth2" as const, securitySchemeName: "OAuth2", flow: "authorizationCode" as const, tokenUrl: "https://auth.example.test/token", authorizationUrl: "https://auth.example.test/authorize", clientIdSlot: "auth:oauth2:client-id", clientSecretSlot: null, connectionSlot: GRAPHQL_OAUTH_CONNECTION_SLOT, scopes: [], }; // --------------------------------------------------------------------------- // Mock introspection response // --------------------------------------------------------------------------- const introspectionResult: IntrospectionResult = { __schema: { queryType: { name: "Query" }, mutationType: { name: "Mutation" }, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "hello", description: "Say hello", args: [ { name: "name", description: null, type: { kind: "SCALAR", name: "String", ofType: null }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "OBJECT", name: "Mutation", description: null, fields: [ { name: "setGreeting", description: "Set greeting message", args: [ { name: "message", description: null, type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null }, }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], }, }; const introspectionJson = JSON.stringify({ data: introspectionResult }); const serveGreetingServer = serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema() }); const declineAll = () => Effect.succeed(ElicitationResponse.make({ action: "decline" })); const sampleDataPlugin = definePlugin(() => ({ id: "sample-read-test" as const, storage: () => ({}), staticSources: () => [ { id: "sample", kind: "in-memory", name: "Sample", tools: [ { name: "read", description: "Read sample data", inputSchema: { type: "object", properties: {}, additionalProperties: false, }, handler: () => Effect.succeed("sample-value"), }, ], }, ], })); describe("graphqlPlugin real protocol server", () => { it("uses query-free endpoints for invocation attributes", () => { expect(endpointForTelemetry("https://api.example.test/graphql?token=secret#section")).toBe( "https://api.example.test/graphql", ); }); it.effect("includes redacted upstream text in introspection status errors", () => Effect.gen(function* () { const server = yield* serveGraphqlFailureTestServer({ status: 500, body: 'upstream failed {"access_token":"secret-value"} token=another-secret', }); const error = yield* introspect(server.endpoint).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", 'Introspection failed with status 500: upstream failed {"access_token":"[redacted]"} token=[redacted]', ); expect(error).not.toHaveProperty("message", expect.stringContaining("secret-value")); expect(error).not.toHaveProperty("message", expect.stringContaining("another-secret")); }), ); it.effect("includes safe upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Resource protected by organization SSO" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Resource protected by organization SSO", ); }), ); it.effect("redacts secrets from upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Authorization: Bearer github-secret-token" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Authorization: [redacted]", ); expect(error).not.toHaveProperty("message", expect.stringContaining("github-secret-token")); }), ); it.effect("accepts standard introspection responses with omitted deepest ofType", () => Effect.gen(function* () { const deepType = { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", }, }, }, }, }, }; const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe({ data: { __schema: { queryType: { name: "Query" }, mutationType: null, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "deep", description: null, args: [], type: deepType, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], }, }, }), ), ); const result = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), ); expect(result.__schema.queryType?.name).toBe("Query"); }), ); it.effect("adds a source by introspecting the live GraphQL endpoint", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* createExecutor( makeTestConfig({ plugins: [graphqlPlugin()] as const }), ); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "live_graph", }); expect(result).toEqual({ toolCount: 2, namespace: "live_graph" }); const tools = yield* executor.tools.list(); expect(tools.map((tool) => tool.id)).toEqual( expect.arrayContaining(["live_graph.query.hello", "live_graph.mutation.setGreeting"]), ); const requests = yield* server.requests; expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true); }), ); it.effect("uses initial credential bindings for add-time introspection", () => Effect.gen(function* () { const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer secret-token"), }, }); const executor = yield* createExecutor( makeTestConfig({ plugins: [memorySecretsPlugin(), graphqlPlugin()] as const }), ); yield* executor.secrets.set({ id: SecretId.make("github-token"), scope: ScopeId.make(TEST_SCOPE), name: "GitHub token", value: "secret-token", provider: "memory", }); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "initial_credentials", headers: { Authorization: { kind: "secret", prefix: "Bearer " }, }, credentials: { scope: TEST_SCOPE, headers: { Authorization: { kind: "secret", secretId: "github-token" }, }, }, }); expect(result).toEqual({ toolCount: 2, namespace: "initial_credentials" }); const requests = yield* server.requests; expect( requests.some((request) => request.headers.authorization === "Bearer secret-token"), ).toBe(true); }), ); it.effect( "uses user-scoped initial credential bindings for org-scope add-time introspection", () => Effect.gen(function* () { const orgScope = "org-scope"; const userScope = "user-scope"; const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer user-secret-token"), }, }); const executor = yield* createExecutor( makeTestConfig({ scopes: [ Scope.make({ id: ScopeId.make(userScope), name: "user", createdAt: new Date(), }), Scope.make({ id: ScopeId.make(orgScope), name: "org", createdAt: new Date(), }), ], plugins: [memorySecretsPlugin(), graphqlPlugin()] as const, }), ); yield* executor.secrets.set({ id: SecretId.make("github-graphql-authorization"), scope: ScopeId.make(userScope), name: "GitHub GraphQL Authorization", value: "user-secret-token", provider: "memory", }); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: orgScope, name: "Github GraphQL", namespace: "github_graphql", headers: { Authorization: { kind: "secret", prefix: "Bearer " }, }, credentials: { scope: userScope, headers: { Authorization: { kind: "secret", secretId: "github-graphql-authorization", secretScope: userScope, prefix: "Bearer ", }, }, }, }); expect(result).toEqual({ toolCount: 2, namespace: "github_graphql" }); const requests = yield* server.requests; expect( requests.some((request) => request.headers.authorization === "Bearer user-secret-token"), ).toBe(true); }), ); it.effect("marks source oauth-backed when add-time credentials include oauth", () => Effect.gen(function* () { const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer oauth-token"), }, }); const executor = yield* createExecutor( makeTestConfig({ plugins: [memorySecretsPlugin(), graphqlPlugin()] as const }), ); const connectionId = ConnectionId.make("graphql-oauth2-initial"); yield* executor.connections.create( CreateConnectionInput.make({ id: connectionId, scope: ScopeId.make(TEST_SCOPE), provider: "oauth2", identityLabel: "Initial GraphQL OAuth", accessToken: TokenMaterial.make({ secretId: SecretId.make(`${connectionId}.access_token`), name: "Initial GraphQL OAuth Access Token", value: "oauth-token", }), refreshToken: null, expiresAt: null, oauthScope: null, providerState: null, }), ); const result = yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "initial_oauth_graphql", credentials: { scope: TEST_SCOPE, auth: { oauth2: { connection: { kind: "connection", connectionId }, }, }, }, }); expect(result).toEqual({ toolCount: 2, namespace: "initial_oauth_graphql" }); const source = yield* executor.graphql.getSource("initial_oauth_graphql", TEST_SCOPE); expect(source?.auth.kind).toBe("oauth2"); if (source?.auth.kind !== "oauth2") return; expect(source.auth.connectionSlot).toBe(GRAPHQL_OAUTH_CONNECTION_SLOT); }), ); it.effect("invokes a live query with headers and query params", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* createExecutor( makeTestConfig({ plugins: [graphqlPlugin()] as const }), ); yield* executor.graphql.addSource({ endpoint: server.endpoint, scope: TEST_SCOPE, namespace: "live_invoke", headers: { "x-static": "abc" }, queryParams: { token: "qp-token" }, }); yield* server.clearRequests; const result = yield* executor.tools.invoke("live_invoke.query.hello", { name: "Ada", }); expect(result).toEqual({ ok: true, data: { hello: "Hello Ada" }, });