import { HttpApiBuilder, HttpServerResponse } from "@effect/platform"; import { Context, Effect } from "effect"; import { runOAuthCallback } from "@executor/plugin-oauth2/http"; import { addGroup, capture, InternalError } from "@executor/api"; import { OpenApiOAuthError } from "../sdk/errors"; import type { ConfiguredHeaderValue, OpenApiPluginExtension, HeaderValue, OpenApiUpdateSourceInput, } from "../sdk/plugin"; import { OAuth2Auth } from "../sdk/types"; import { OpenApiGroup } from "./group"; const OPENAPI_OAUTH_CHANNEL = "executor:openapi-oauth-result"; const toPopupErrorMessage = (error: unknown): string => { if (error instanceof OpenApiOAuthError) return error.message; return "Authentication failed"; }; // --------------------------------------------------------------------------- // Service tag // // Holds the `Captured` shape — every method's `StorageFailure` // channel has been swapped for `InternalError({ traceId })`. The cloud // app provides an already-wrapped extension via // `Layer.succeed(OpenApiExtensionService, withCapture(executor.openapi))`. // Handlers see `InternalError` in the error union, which matches // `.addError(InternalError)` on the group — no per-handler translation. // --------------------------------------------------------------------------- export class OpenApiExtensionService extends Context.Tag("OpenApiExtensionService")< OpenApiExtensionService, OpenApiPluginExtension >() {} // --------------------------------------------------------------------------- // Composed API — core + openapi group // --------------------------------------------------------------------------- const ExecutorApiWithOpenApi = addGroup(OpenApiGroup); // --------------------------------------------------------------------------- // Handlers // // Each handler is exactly: yield the extension service, call the method, // return. Plugin SDK errors flow through the typed channel and are // schema-encoded to 4xx by HttpApi (see group.ts `.addError(...)` calls). // Defects bubble up and are captured + downgraded to `InternalError(traceId)` // by the API-level observability middleware. // --------------------------------------------------------------------------- export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "openapi", (handlers) => handlers .handle("previewSpec", ({ payload }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; return yield* ext.previewSpec(payload.spec); })), ) .handle("addSpec", ({ path, payload }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; const result = yield* ext.addSpec({ spec: payload.spec, scope: path.scopeId, name: payload.name, baseUrl: payload.baseUrl, namespace: payload.namespace, headers: payload.headers as | Record | undefined, oauth2: payload.oauth2, }); return { toolCount: result.toolCount, namespace: result.sourceId, }; })), ) .handle("getSource", ({ path }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; return yield* ext.getSource(path.namespace, path.scopeId); })), ) .handle("updateSource", ({ path, payload }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; yield* ext.updateSource(path.namespace, path.scopeId, { name: payload.name, baseUrl: payload.baseUrl, headers: payload.headers as | Record | undefined, oauth2: payload.oauth2, } as OpenApiUpdateSourceInput); return { updated: true }; })), ) .handle("listSourceBindings", ({ path }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; return yield* ext.listSourceBindings(path.namespace, path.sourceScopeId); })), ) .handle("setSourceBinding", ({ payload }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; return yield* ext.setSourceBinding(payload); })), ) .handle("removeSourceBinding", ({ payload }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; yield* ext.removeSourceBinding( payload.sourceId, payload.sourceScope, payload.slot, payload.scope, ); return { removed: true }; })), ) .handle("startOAuth", ({ payload }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; // No tokenScope -> plugin defaults to ctx.scopes[0].id // (innermost) for both OAuth flows. const tokenScope = payload.tokenScope as string | undefined; if (payload.flow === "clientCredentials") { return yield* ext.startOAuth({ flow: "clientCredentials", sourceId: payload.sourceId, displayName: payload.displayName, securitySchemeName: payload.securitySchemeName, tokenUrl: payload.tokenUrl, clientIdSecretId: payload.clientIdSecretId, clientSecretSecretId: payload.clientSecretSecretId, scopes: [...payload.scopes], tokenScope, connectionId: payload.connectionId, }); } return yield* ext.startOAuth({ flow: "authorizationCode", sourceId: payload.sourceId, displayName: payload.displayName, securitySchemeName: payload.securitySchemeName, authorizationUrl: payload.authorizationUrl, tokenUrl: payload.tokenUrl, redirectUrl: payload.redirectUrl, clientIdSecretId: payload.clientIdSecretId, clientSecretSecretId: payload.clientSecretSecretId ?? null, scopes: [...payload.scopes], tokenScope, connectionId: payload.connectionId, }); })), ) .handle("completeOAuth", ({ payload }) => capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; return yield* ext.completeOAuth({ state: payload.state, code: payload.code, error: payload.error, }); })), ) .handle("oauthCallback", ({ urlParams }) => // OAuth popup is special: it always returns 200 HTML and renders the // failure into the popup body so the parent window's listener gets a // structured result. capture(Effect.gen(function* () { const ext = yield* OpenApiExtensionService; const html = yield* runOAuthCallback({ complete: ({ state, code, error }) => ext.completeOAuth({ state, code: code ?? undefined, error: error ?? undefined, }), urlParams, toErrorMessage: toPopupErrorMessage, channelName: OPENAPI_OAUTH_CHANNEL, }); return yield* HttpServerResponse.html(html); })), ), );