import { randomUUID } from "node:crypto"; import { Effect, Option, Schema } from "effect";import { FetchHttpClient, HttpClient } from "@effect/platform";import type { Layer } from "effect"; import { buildAuthorizationUrl, createPkceCodeVerifier, exchangeAuthorizationCode, storeOAuthTokens, withRefreshedAccessToken, type OAuth2TokenResponse,} from "@executor/plugin-oauth2"; import { SecretId, SetSecretInput, SourceDetectionResult, definePlugin, type StorageFailure, type ToolAnnotations, type ToolRow,} from "@executor/sdk"; import { headersToConfigValues, type ConfigFileSink, type OpenApiSourceConfig,} from "@executor/config"; import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError,} from "./errors";import { parse, resolveSpecText } from "./parse";import { extract } from "./extract";import { compileToolDefinitions, type ToolDefinition } from "./definitions";import { annotationsForOperation, invokeWithLayer, resolveHeaders,} from "./invoke";import { resolveBaseUrl } from "./openapi-utils";import { previewSpec, SpecPreview } from "./preview";import { makeDefaultOpenapiStore, openapiSchema, type OpenapiStore, type SourceConfig, type StoredOperation, type StoredSource,} from "./store";import { HeaderValue as HeaderValueSchema, InvocationConfig, OAuth2Auth, OpenApiOAuthSession, OperationBinding, type HeaderValue as HeaderValueValue,} from "./types"; // ---------------------------------------------------------------------------// Plugin config// --------------------------------------------------------------------------- export type HeaderValue = HeaderValueValue; export interface OpenApiSpecConfig { readonly spec: string; readonly name?: string; readonly baseUrl?: string; readonly namespace?: string; readonly headers?: Record<string, HeaderValue>; readonly oauth2?: OAuth2Auth;} export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, HeaderValue>;} // ---------------------------------------------------------------------------// OAuth2 onboarding inputs / outputs// --------------------------------------------------------------------------- export interface OpenApiStartOAuthInput { readonly displayName: string; readonly securitySchemeName: string; readonly flow: "authorizationCode"; readonly authorizationUrl: string; readonly tokenUrl: string; readonly redirectUrl: string; readonly clientIdSecretId: string; readonly clientSecretSecretId?: string | null; readonly scopes: readonly string[];} export interface OpenApiStartOAuthResponse { readonly sessionId: string; readonly authorizationUrl: string; readonly scopes: readonly string[];} export interface OpenApiCompleteOAuthInput { readonly state: string; readonly code?: string; readonly error?: string;} /** * Errors any OpenAPI extension method may surface. The first three are * plugin-domain tagged errors that flow directly to clients (4xx, each * carrying its own `HttpApiSchema` status). `StorageFailure` covers * raw backend failures (`StorageError`) plus `UniqueViolationError`; * the HTTP edge (`@executor/api`'s `withCapture`) translates * `StorageError` to the opaque `InternalError({ traceId })` at Layer * composition. `UniqueViolationError` passes through — plugins can * `Effect.catchTag` it if they want a friendlier user-facing error. */export type OpenApiExtensionFailure = | OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure; export interface OpenApiPluginExtension { readonly previewSpec: ( specText: string, ) => Effect.Effect<SpecPreview, OpenApiParseError | OpenApiExtractionError>; readonly addSpec: ( config: OpenApiSpecConfig, ) => Effect.Effect< { readonly sourceId: string; readonly toolCount: number }, OpenApiParseError | OpenApiExtractionError | StorageFailure >; readonly removeSpec: (namespace: string) => Effect.Effect<void, StorageFailure>; readonly getSource: ( namespace: string, ) => Effect.Effect<StoredSource | null, StorageFailure>; readonly updateSource: ( namespace: string, input: OpenApiUpdateSourceInput, ) => Effect.Effect<void, StorageFailure>; readonly startOAuth: ( input: OpenApiStartOAuthInput, ) => Effect.Effect<OpenApiStartOAuthResponse, OpenApiOAuthError>; readonly completeOAuth: ( input: OpenApiCompleteOAuthInput, ) => Effect.Effect<OAuth2Auth, OpenApiOAuthError>;} // ---------------------------------------------------------------------------// Control-tool input/output schemas// --------------------------------------------------------------------------- const PreviewSpecInputSchema = Schema.Struct({ spec: Schema.String,});type PreviewSpecInput = typeof PreviewSpecInputSchema.Type; const AddSourceInputSchema = Schema.Struct({ spec: Schema.String, baseUrl: Schema.optional(Schema.String), namespace: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record({ key: Schema.String, value: HeaderValueSchema })),});type AddSourceInput = typeof AddSourceInputSchema.Type; // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- /** Rewrite OpenAPI `#/components/schemas/X` refs to standard `#/$defs/X`. */const normalizeOpenApiRefs = (node: unknown): unknown => { if (node == null || typeof node !== "object") return node; if (Array.isArray(node)) { let changed = false; const out = node.map((item) => { const n = normalizeOpenApiRefs(item); if (n !== item) changed = true; return n; }); return changed ? out : node; } const obj = node as Record<string, unknown>; if (typeof obj.$ref === "string") { const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/); if (match) return { ...obj, $ref: `#/$defs/${match[1]}` }; return obj; } let changed = false; const result: Record<string, unknown> = {}; for (const [k, v] of Object.entries(obj)) { const n = normalizeOpenApiRefs(v); if (n !== v) changed = true; result[k] = n; } return changed ? result : obj;}; const toBinding = (def: ToolDefinition): OperationBinding => new OperationBinding({ method: def.operation.method, pathTemplate: def.operation.pathTemplate, parameters: [...def.operation.parameters], requestBody: def.operation.requestBody, }); const descriptionFor = (def: ToolDefinition): string => { const op = def.operation; return Option.getOrElse(op.description, () => Option.getOrElse(op.summary, () => `${op.method.toUpperCase()} ${op.pathTemplate}`), );}; // ---------------------------------------------------------------------------// Plugin factory// --------------------------------------------------------------------------- export interface OpenApiPluginOptions { readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; /** If provided, source add/remove is mirrored to executor.jsonc * (best-effort — file errors are logged, not raised). */ readonly configFile?: ConfigFileSink;} const toOpenApiSourceConfig = ( namespace: string, config: OpenApiSpecConfig,): OpenApiSourceConfig => ({ kind: "openapi", spec: config.spec, baseUrl: config.baseUrl, namespace, headers: headersToConfigValues(config.headers),}); export const openApiPlugin = definePlugin( (options?: OpenApiPluginOptions) => { const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer; return { id: "openapi" as const, schema: openapiSchema, storage: (deps): OpenapiStore => makeDefaultOpenapiStore(deps), extension: (ctx) => { // Wraps ctx.secrets.set for the oauth2 helper so createSecret // returns the freshly-minted id. const createOAuthSecret = (args: { readonly idPrefix: string; readonly name: string; readonly value: string; readonly purpose: string; }) => ctx.secrets .set( new SetSecretInput({ id: SecretId.make(`${args.idPrefix}_${randomUUID().slice(0, 8)}`), name: args.name, value: args.value, }), ) .pipe(Effect.map((ref) => ({ id: ref.id as string }))); const addSpecInternal = (config: OpenApiSpecConfig) => Effect.gen(function* () { // Resolve URL → text and parse BEFORE opening a transaction. // Holding `BEGIN` on the pool=1 Postgres connection across a // network fetch is the Hyperdrive deadlock path in production. const specText = yield* resolveSpecText(config.spec).pipe( Effect.provide(httpClientLayer), ); const doc = yield* parse(specText); const result = yield* extract(doc); const namespace = config.namespace ?? Option.getOrElse(result.title, () => "api") .toLowerCase() .replace(/[^a-z0-9]+/g, "_"); const hoistedDefs: Record<string, unknown> = {}; if (doc.components?.schemas) { for (const [k, v] of Object.entries(doc.components.schemas)) { hoistedDefs[k] = normalizeOpenApiRefs(v); } } const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers); const oauth2 = config.oauth2 ?? undefined; const invocationConfig = new InvocationConfig({ baseUrl, headers: config.headers ?? {}, oauth2: oauth2 ? Option.some(oauth2) : Option.none(), }); const definitions = compileToolDefinitions(result.operations); const sourceName = config.name ?? Option.getOrElse(result.title, () => namespace); const sourceConfig: SourceConfig = { spec: config.spec, baseUrl: config.baseUrl, namespace: config.namespace, headers: config.headers, oauth2, }; const storedSource: StoredSource = { namespace, name: sourceName, config: sourceConfig, invocationConfig, }; const storedOps: StoredOperation[] = definitions.map((def) => ({ toolId: `${namespace}.${def.toolPath}`, sourceId: namespace, binding: toBinding(def), })); yield* ctx.transaction( Effect.gen(function* () { yield* ctx.storage.upsertSource(storedSource, storedOps); yield* ctx.core.sources.register({ id: namespace, kind: "openapi", name: sourceName, url: baseUrl || undefined, canRemove: true, canRefresh: false, canEdit: true, tools: definitions.map((def) => ({ name: def.toolPath, description: descriptionFor(def), inputSchema: normalizeOpenApiRefs( Option.getOrUndefined(def.operation.inputSchema), ), outputSchema: normalizeOpenApiRefs( Option.getOrUndefined(def.operation.outputSchema), ), })), }); if (Object.keys(hoistedDefs).length > 0) { yield* ctx.core.definitions.register({ sourceId: namespace, definitions: hoistedDefs, }); } }), ); return { sourceId: namespace, toolCount: definitions.length }; }); const configFile = options?.configFile; return { previewSpec: (specText) => previewSpec(specText).pipe(Effect.provide(httpClientLayer)), addSpec: (config) => Effect.gen(function* () { const result = yield* addSpecInternal(config); if (configFile) { yield* configFile.upsertSource( toOpenApiSourceConfig(result.sourceId, config), ); } return result; }), removeSpec: (namespace) => Effect.gen(function* () { yield* ctx.transaction( Effect.gen(function* () { yield* ctx.storage.removeSource(namespace); yield* ctx.core.sources.unregister(namespace); }), ); if (configFile) { yield* configFile.removeSource(namespace); } }), getSource: (namespace) => ctx.storage.getSource(namespace), updateSource: (namespace, input) => ctx.storage.updateSourceMeta(namespace, { name: input.name?.trim() || undefined, baseUrl: input.baseUrl, headers: input.headers, }), startOAuth: (input) => Effect.gen(function* () { const sessionId = randomUUID(); const codeVerifier = createPkceCodeVerifier(); const scopesArray = [...input.scopes]; yield* ctx.storage .putOAuthSession( sessionId, new OpenApiOAuthSession({ displayName: input.displayName, securitySchemeName: input.securitySchemeName, flow: input.flow, tokenUrl: input.tokenUrl, redirectUrl: input.redirectUrl, clientIdSecretId: input.clientIdSecretId, clientSecretSecretId: input.clientSecretSecretId ?? null, scopes: scopesArray, codeVerifier, }), ) .pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); const clientId = yield* ctx.secrets.get(input.clientIdSecretId).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (clientId === null) { return yield* new OpenApiOAuthError({ message: `Missing client ID secret: ${input.clientIdSecretId}`, }); } const authorizationUrl = buildAuthorizationUrl({ authorizationUrl: input.authorizationUrl, clientId, redirectUrl: input.redirectUrl, scopes: scopesArray, state: sessionId, codeVerifier, }); return { sessionId, authorizationUrl, scopes: scopesArray, }; }), completeOAuth: (input) => ctx.transaction( Effect.gen(function* () { const session = yield* ctx.storage.getOAuthSession(input.state).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (!session) { return yield* new OpenApiOAuthError({ message: "OAuth session not found or has expired", }); } yield* ctx.storage.deleteOAuthSession(input.state).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (input.error) { return yield* new OpenApiOAuthError({ message: input.error }); } if (!input.code) { return yield* new OpenApiOAuthError({ message: "OAuth callback did not include an authorization code", }); } const clientId = yield* ctx.secrets.get(session.clientIdSecretId).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (clientId === null) { return yield* new OpenApiOAuthError({ message: `Missing client ID secret: ${session.clientIdSecretId}`, }); } const clientSecret = session.clientSecretSecretId ? yield* ctx.secrets.get(session.clientSecretSecretId).pipe( Effect.mapError( (err) => new OpenApiOAuthError({ message: err.message }), ), ) : null; const tokenResponse: OAuth2TokenResponse = yield* exchangeAuthorizationCode({ tokenUrl: session.tokenUrl, clientId, clientSecret, redirectUrl: session.redirectUrl,Lines 1–500 of 747. Load more
import { randomUUID } from "node:crypto"; import { Effect, Option, Schema } from "effect"; import { FetchHttpClient, HttpClient } from "@effect/platform"; import type { Layer } from "effect"; import { buildAuthorizationUrl, createPkceCodeVerifier, exchangeAuthorizationCode, storeOAuthTokens, withRefreshedAccessToken, type OAuth2TokenResponse, } from "@executor/plugin-oauth2"; import { SecretId, SetSecretInput, SourceDetectionResult, definePlugin, type StorageFailure, type ToolAnnotations, type ToolRow, } from "@executor/sdk"; import { headersToConfigValues, type ConfigFileSink, type OpenApiSourceConfig, } from "@executor/config"; import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError, } from "./errors"; import { parse, resolveSpecText } from "./parse"; import { extract } from "./extract"; import { compileToolDefinitions, type ToolDefinition } from "./definitions"; import { annotationsForOperation, invokeWithLayer, resolveHeaders, } from "./invoke"; import { resolveBaseUrl } from "./openapi-utils"; import { previewSpec, SpecPreview } from "./preview"; import { makeDefaultOpenapiStore, openapiSchema, type OpenapiStore, type SourceConfig, type StoredOperation, type StoredSource, } from "./store"; import { HeaderValue as HeaderValueSchema, InvocationConfig, OAuth2Auth, OpenApiOAuthSession, OperationBinding, type HeaderValue as HeaderValueValue, } from "./types"; // --------------------------------------------------------------------------- // Plugin config // --------------------------------------------------------------------------- export type HeaderValue = HeaderValueValue; export interface OpenApiSpecConfig { readonly spec: string; readonly name?: string; readonly baseUrl?: string; readonly namespace?: string; readonly headers?: Record<string, HeaderValue>; readonly oauth2?: OAuth2Auth; } export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, HeaderValue>; } // --------------------------------------------------------------------------- // OAuth2 onboarding inputs / outputs // --------------------------------------------------------------------------- export interface OpenApiStartOAuthInput { readonly displayName: string; readonly securitySchemeName: string; readonly flow: "authorizationCode"; readonly authorizationUrl: string; readonly tokenUrl: string; readonly redirectUrl: string; readonly clientIdSecretId: string; readonly clientSecretSecretId?: string | null; readonly scopes: readonly string[]; } export interface OpenApiStartOAuthResponse { readonly sessionId: string; readonly authorizationUrl: string; readonly scopes: readonly string[]; } export interface OpenApiCompleteOAuthInput { readonly state: string; readonly code?: string; readonly error?: string; } /** * Errors any OpenAPI extension method may surface. The first three are * plugin-domain tagged errors that flow directly to clients (4xx, each * carrying its own `HttpApiSchema` status). `StorageFailure` covers * raw backend failures (`StorageError`) plus `UniqueViolationError`; * the HTTP edge (`@executor/api`'s `withCapture`) translates * `StorageError` to the opaque `InternalError({ traceId })` at Layer * composition. `UniqueViolationError` passes through — plugins can * `Effect.catchTag` it if they want a friendlier user-facing error. */ export type OpenApiExtensionFailure = | OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure; export interface OpenApiPluginExtension { readonly previewSpec: ( specText: string, ) => Effect.Effect<SpecPreview, OpenApiParseError | OpenApiExtractionError>; readonly addSpec: ( config: OpenApiSpecConfig, ) => Effect.Effect< { readonly sourceId: string; readonly toolCount: number }, OpenApiParseError | OpenApiExtractionError | StorageFailure >; readonly removeSpec: (namespace: string) => Effect.Effect<void, StorageFailure>; readonly getSource: ( namespace: string, ) => Effect.Effect<StoredSource | null, StorageFailure>; readonly updateSource: ( namespace: string, input: OpenApiUpdateSourceInput, ) => Effect.Effect<void, StorageFailure>; readonly startOAuth: ( input: OpenApiStartOAuthInput, ) => Effect.Effect<OpenApiStartOAuthResponse, OpenApiOAuthError>; readonly completeOAuth: ( input: OpenApiCompleteOAuthInput, ) => Effect.Effect<OAuth2Auth, OpenApiOAuthError>; } // --------------------------------------------------------------------------- // Control-tool input/output schemas // --------------------------------------------------------------------------- const PreviewSpecInputSchema = Schema.Struct({ spec: Schema.String, }); type PreviewSpecInput = typeof PreviewSpecInputSchema.Type; const AddSourceInputSchema = Schema.Struct({ spec: Schema.String, baseUrl: Schema.optional(Schema.String), namespace: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record({ key: Schema.String, value: HeaderValueSchema })), }); type AddSourceInput = typeof AddSourceInputSchema.Type; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Rewrite OpenAPI `#/components/schemas/X` refs to standard `#/$defs/X`. */ const normalizeOpenApiRefs = (node: unknown): unknown => { if (node == null || typeof node !== "object") return node; if (Array.isArray(node)) { let changed = false; const out = node.map((item) => { const n = normalizeOpenApiRefs(item); if (n !== item) changed = true; return n; }); return changed ? out : node; } const obj = node as Record<string, unknown>; if (typeof obj.$ref === "string") { const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/); if (match) return { ...obj, $ref: `#/$defs/${match[1]}` }; return obj; } let changed = false; const result: Record<string, unknown> = {}; for (const [k, v] of Object.entries(obj)) { const n = normalizeOpenApiRefs(v); if (n !== v) changed = true; result[k] = n; } return changed ? result : obj; }; const toBinding = (def: ToolDefinition): OperationBinding => new OperationBinding({ method: def.operation.method, pathTemplate: def.operation.pathTemplate, parameters: [...def.operation.parameters], requestBody: def.operation.requestBody, }); const descriptionFor = (def: ToolDefinition): string => { const op = def.operation; return Option.getOrElse(op.description, () => Option.getOrElse(op.summary, () => `${op.method.toUpperCase()} ${op.pathTemplate}`), ); }; // --------------------------------------------------------------------------- // Plugin factory // --------------------------------------------------------------------------- export interface OpenApiPluginOptions { readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; /** If provided, source add/remove is mirrored to executor.jsonc * (best-effort — file errors are logged, not raised). */ readonly configFile?: ConfigFileSink; } const toOpenApiSourceConfig = ( namespace: string, config: OpenApiSpecConfig, ): OpenApiSourceConfig => ({ kind: "openapi", spec: config.spec, baseUrl: config.baseUrl, namespace, headers: headersToConfigValues(config.headers), }); export const openApiPlugin = definePlugin( (options?: OpenApiPluginOptions) => { const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer; return { id: "openapi" as const, schema: openapiSchema, storage: (deps): OpenapiStore => makeDefaultOpenapiStore(deps), extension: (ctx) => { // Wraps ctx.secrets.set for the oauth2 helper so createSecret // returns the freshly-minted id. const createOAuthSecret = (args: { readonly idPrefix: string; readonly name: string; readonly value: string; readonly purpose: string; }) => ctx.secrets .set( new SetSecretInput({ id: SecretId.make(`${args.idPrefix}_${randomUUID().slice(0, 8)}`), name: args.name, value: args.value, }), ) .pipe(Effect.map((ref) => ({ id: ref.id as string }))); const addSpecInternal = (config: OpenApiSpecConfig) => Effect.gen(function* () { // Resolve URL → text and parse BEFORE opening a transaction. // Holding `BEGIN` on the pool=1 Postgres connection across a // network fetch is the Hyperdrive deadlock path in production. const specText = yield* resolveSpecText(config.spec).pipe( Effect.provide(httpClientLayer), ); const doc = yield* parse(specText); const result = yield* extract(doc); const namespace = config.namespace ?? Option.getOrElse(result.title, () => "api") .toLowerCase() .replace(/[^a-z0-9]+/g, "_"); const hoistedDefs: Record<string, unknown> = {}; if (doc.components?.schemas) { for (const [k, v] of Object.entries(doc.components.schemas)) { hoistedDefs[k] = normalizeOpenApiRefs(v); } } const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers); const oauth2 = config.oauth2 ?? undefined; const invocationConfig = new InvocationConfig({ baseUrl, headers: config.headers ?? {}, oauth2: oauth2 ? Option.some(oauth2) : Option.none(), }); const definitions = compileToolDefinitions(result.operations); const sourceName = config.name ?? Option.getOrElse(result.title, () => namespace); const sourceConfig: SourceConfig = { spec: config.spec, baseUrl: config.baseUrl, namespace: config.namespace, headers: config.headers, oauth2, }; const storedSource: StoredSource = { namespace, name: sourceName, config: sourceConfig, invocationConfig, }; const storedOps: StoredOperation[] = definitions.map((def) => ({ toolId: `${namespace}.${def.toolPath}`, sourceId: namespace, binding: toBinding(def), })); yield* ctx.transaction( Effect.gen(function* () { yield* ctx.storage.upsertSource(storedSource, storedOps); yield* ctx.core.sources.register({ id: namespace, kind: "openapi", name: sourceName, url: baseUrl || undefined, canRemove: true, canRefresh: false, canEdit: true, tools: definitions.map((def) => ({ name: def.toolPath, description: descriptionFor(def), inputSchema: normalizeOpenApiRefs( Option.getOrUndefined(def.operation.inputSchema), ), outputSchema: normalizeOpenApiRefs( Option.getOrUndefined(def.operation.outputSchema), ), })), }); if (Object.keys(hoistedDefs).length > 0) { yield* ctx.core.definitions.register({ sourceId: namespace, definitions: hoistedDefs, }); } }), ); return { sourceId: namespace, toolCount: definitions.length }; }); const configFile = options?.configFile; return { previewSpec: (specText) => previewSpec(specText).pipe(Effect.provide(httpClientLayer)), addSpec: (config) => Effect.gen(function* () { const result = yield* addSpecInternal(config); if (configFile) { yield* configFile.upsertSource( toOpenApiSourceConfig(result.sourceId, config), ); } return result; }), removeSpec: (namespace) => Effect.gen(function* () { yield* ctx.transaction( Effect.gen(function* () { yield* ctx.storage.removeSource(namespace); yield* ctx.core.sources.unregister(namespace); }), ); if (configFile) { yield* configFile.removeSource(namespace); } }), getSource: (namespace) => ctx.storage.getSource(namespace), updateSource: (namespace, input) => ctx.storage.updateSourceMeta(namespace, { name: input.name?.trim() || undefined, baseUrl: input.baseUrl, headers: input.headers, }), startOAuth: (input) => Effect.gen(function* () { const sessionId = randomUUID(); const codeVerifier = createPkceCodeVerifier(); const scopesArray = [...input.scopes]; yield* ctx.storage .putOAuthSession( sessionId, new OpenApiOAuthSession({ displayName: input.displayName, securitySchemeName: input.securitySchemeName, flow: input.flow, tokenUrl: input.tokenUrl, redirectUrl: input.redirectUrl, clientIdSecretId: input.clientIdSecretId, clientSecretSecretId: input.clientSecretSecretId ?? null, scopes: scopesArray, codeVerifier, }), ) .pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); const clientId = yield* ctx.secrets.get(input.clientIdSecretId).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (clientId === null) { return yield* new OpenApiOAuthError({ message: `Missing client ID secret: ${input.clientIdSecretId}`, }); } const authorizationUrl = buildAuthorizationUrl({ authorizationUrl: input.authorizationUrl, clientId, redirectUrl: input.redirectUrl, scopes: scopesArray, state: sessionId, codeVerifier, }); return { sessionId, authorizationUrl, scopes: scopesArray, }; }), completeOAuth: (input) => ctx.transaction( Effect.gen(function* () { const session = yield* ctx.storage.getOAuthSession(input.state).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (!session) { return yield* new OpenApiOAuthError({ message: "OAuth session not found or has expired", }); } yield* ctx.storage.deleteOAuthSession(input.state).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (input.error) { return yield* new OpenApiOAuthError({ message: input.error }); } if (!input.code) { return yield* new OpenApiOAuthError({ message: "OAuth callback did not include an authorization code", }); } const clientId = yield* ctx.secrets.get(session.clientIdSecretId).pipe( Effect.mapError((err) => new OpenApiOAuthError({ message: err.message })), ); if (clientId === null) { return yield* new OpenApiOAuthError({ message: `Missing client ID secret: ${session.clientIdSecretId}`, }); } const clientSecret = session.clientSecretSecretId ? yield* ctx.secrets.get(session.clientSecretSecretId).pipe( Effect.mapError( (err) => new OpenApiOAuthError({ message: err.message }), ), ) : null; const tokenResponse: OAuth2TokenResponse = yield* exchangeAuthorizationCode({ tokenUrl: session.tokenUrl, clientId, clientSecret, redirectUrl: session.redirectUrl,