import { HttpApiBuilder } from "effect/unstable/httpapi"; import { HttpRouter, HttpServer } from "effect/unstable/http"; import { describe, expect, it } from "@effect/vitest"; import { Context, Effect, Layer } from "effect"; import { IntegrationSlug, createExecutor, definePlugin, type AuthMethodDescriptor, type Executor, type IntegrationRecord, } from "@executor-js/sdk"; import { makeTestConfig } from "@executor-js/sdk/testing"; import { ExecutorApi } from "../api"; import { observabilityMiddleware } from "../observability"; import { CoreHandlers, ExecutionEngineService, ExecutorService } from "../server"; // --------------------------------------------------------------------------- // The catalog response surfaces each plugin's DECLARED auth methods (projected // from the integration's stored config via `describeAuthMethods`). This proves // the visible bug is fixed: an OAuth integration with ZERO connections still // advertises an `oauth` method, and an apikey/header integration advertises an // `apikey` method — the client no longer has to infer from connections. // // We register integrations through a lightweight inline plugin so the test // stays in `@executor-js/api` without a cross-package dependency on the MCP // plugin; the MCP-specific projection is covered by the MCP plugin's own // `describe-auth-methods` test. // --------------------------------------------------------------------------- const OAUTH_METHOD: AuthMethodDescriptor = { id: "oauth2", label: "OAuth", kind: "oauth", template: "oauth2", oauth: { discoveryUrl: "https://x.example/oauth/mcp", supportsDynamicRegistration: true }, }; const APIKEY_METHOD: AuthMethodDescriptor = { id: "header", label: "API key (header)", kind: "apikey", template: "header", placements: [{ carrier: "header", name: "Authorization", prefix: "" }], }; // A plugin that projects its stored `config.methods` blob straight back as the // declared auth methods, exercising the catalog wiring end-to-end. const declaringPlugin = definePlugin(() => ({ id: "declaring" as const, storage: () => ({}), describeAuthMethods: (record: IntegrationRecord): readonly AuthMethodDescriptor[] => { const config = record.config as { readonly methods?: readonly AuthMethodDescriptor[]; readonly displayUrl?: string; }; return config?.methods ?? []; }, describeIntegrationDisplay: (record: IntegrationRecord) => { const config = record.config as { readonly methods?: readonly AuthMethodDescriptor[]; readonly displayUrl?: string; }; return { url: config?.displayUrl }; }, extension: (ctx) => ({ seed: (slug: IntegrationSlug, methods: readonly AuthMethodDescriptor[], displayUrl?: string) => ctx.core.integrations.register({ slug, description: String(slug), config: { methods, displayUrl }, }), }), }))(); const webHandlerFor = (executor: Executor) => Effect.acquireRelease( Effect.sync(() => HttpRouter.toWebHandler( HttpApiBuilder.layer(ExecutorApi).pipe( Layer.provide(CoreHandlers), Layer.provide(observabilityMiddleware(ExecutorApi)), Layer.provide(Layer.succeed(ExecutorService)(executor)), Layer.provide( Layer.succeed(ExecutionEngineService)({} as ExecutionEngineService["Service"]), ), Layer.provideMerge(HttpServer.layerServices), Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })), ), { disableLogger: true }, ), ), (web) => Effect.promise(() => web.dispose()), ); const handlerContextFor = (executor: Executor) => Context.make(ExecutorService, executor).pipe( Context.add(ExecutionEngineService, {} as ExecutionEngineService["Service"]), ); interface IntegrationResponseBody { readonly slug: string; readonly authMethods: readonly AuthMethodDescriptor[]; readonly displayUrl?: string; } describe("catalog surfaces declared auth methods", () => { it.effect("an OAuth integration with zero connections advertises an oauth method", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: [declaringPlugin] })); const slug = IntegrationSlug.make("oauth-server"); yield* executor.declaring.seed(slug, [OAUTH_METHOD]); const web = yield* webHandlerFor(executor); const context = handlerContextFor(executor); const response = yield* Effect.promise(() => web.handler( new Request(`http://localhost/integrations/${encodeURIComponent(String(slug))}`), context, ), ); expect(response.status).toBe(200); const body = (yield* Effect.promise(() => response.json())) as IntegrationResponseBody; expect(body.authMethods).toEqual([OAUTH_METHOD]); expect(body.authMethods[0]?.kind).toBe("oauth"); expect(body.authMethods[0]?.oauth?.supportsDynamicRegistration).toBe(true); }), ); it.effect("an apikey integration advertises an apikey method", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: [declaringPlugin] })); const slug = IntegrationSlug.make("apikey-server"); yield* executor.declaring.seed(slug, [APIKEY_METHOD]); const web = yield* webHandlerFor(executor); const context = handlerContextFor(executor); const response = yield* Effect.promise(() => web.handler( new Request(`http://localhost/integrations/${encodeURIComponent(String(slug))}`), context, ), ); expect(response.status).toBe(200); const body = (yield* Effect.promise(() => response.json())) as IntegrationResponseBody; expect(body.authMethods).toEqual([APIKEY_METHOD]); expect(body.authMethods[0]?.kind).toBe("apikey"); }), ); it.effect("list surfaces authMethods and a plugin with no projector yields []", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: [declaringPlugin] })); yield* executor.declaring.seed(IntegrationSlug.make("oauth-server"), [OAUTH_METHOD]); yield* executor.declaring.seed(IntegrationSlug.make("bare-server"), []); const web = yield* webHandlerFor(executor); const context = handlerContextFor(executor); const response = yield* Effect.promise(() => web.handler(new Request("http://localhost/integrations"), context), ); expect(response.status).toBe(200); const body = (yield* Effect.promise(() => response.json(), )) as readonly IntegrationResponseBody[]; const oauth = body.find((i) => i.slug === "oauth-server"); const bare = body.find((i) => i.slug === "bare-server"); expect(oauth?.authMethods).toEqual([OAUTH_METHOD]); expect(bare?.authMethods).toEqual([]); }), ); it.effect("surfaces plugin-derived display URLs without exposing config", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: [declaringPlugin] })); const slug = IntegrationSlug.make("autumn"); yield* executor.declaring.seed(slug, [], "https://api.useautumn.com"); const web = yield* webHandlerFor(executor); const context = handlerContextFor(executor); const response = yield* Effect.promise(() => web.handler( new Request(`http://localhost/integrations/${encodeURIComponent(String(slug))}`), context, ), ); expect(response.status).toBe(200); const body = (yield* Effect.promise(() => response.json())) as IntegrationResponseBody; expect(body.displayUrl).toBe("https://api.useautumn.com"); expect("config" in body).toBe(false); }), ); });