import { Effect, Option, Schema } from "effect";import type { Layer } from "effect";import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import { OpenApiGroup } from "../api/group";import { OpenApiExtensionService, OpenApiHandlers } from "../api/handlers"; import { ConnectionId, ScopeId, SecretId, SourceDetectionResult, definePlugin, resolveSecretBackedMap, type PluginCtx, type StorageFailure, type ToolAnnotations, type ToolRow,} from "@executor-js/sdk/core"; import { headersToConfigValues, type ConfigFileSink, type OpenApiSourceConfig,} from "@executor-js/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, ConfiguredHeaderBinding, OAuth2Auth, OAuth2SourceConfig, OpenApiSourceBindingInput, type OpenApiSourceBindingRef, type OpenApiSourceBindingValue, OperationBinding, type ConfiguredHeaderValue as ConfiguredHeaderValueValue, type HeaderValue as HeaderValueValue,} from "./types"; // ---------------------------------------------------------------------------// Plugin config// --------------------------------------------------------------------------- export type HeaderValue = HeaderValueValue;export type ConfiguredHeaderValue = ConfiguredHeaderValueValue;export type OpenApiHeaderInput = HeaderValue | ConfiguredHeaderValue;export type OpenApiOAuthInput = OAuth2Auth | OAuth2SourceConfig; export interface OpenApiSpecFetchCredentials { readonly headers?: Record<string, HeaderValue>; readonly queryParams?: Record<string, HeaderValue>;} export interface OpenApiPreviewInput { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentials;} export interface OpenApiSpecConfig { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentials; /** * Executor scope id that owns this source row. Must be one of the * executor's configured scopes. Typical shape: an admin adds the * source at the outermost (organization) scope so it's visible to * every inner (per-user) scope via fall-through reads. */ readonly scope: string; readonly name?: string; readonly baseUrl?: string; readonly namespace?: string; readonly headers?: Record<string, OpenApiHeaderInput>; readonly queryParams?: Record<string, HeaderValue>; readonly oauth2?: OpenApiOAuthInput;} export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, OpenApiHeaderInput>; readonly queryParams?: Record<string, HeaderValue>; /** Refresh the source's stored OAuth2 metadata after a successful * re-authenticate. */ readonly oauth2?: OpenApiOAuthInput;} /** * 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-js/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: ( input: string | OpenApiPreviewInput, ) => Effect.Effect< SpecPreview, OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure >; readonly addSpec: ( config: OpenApiSpecConfig, ) => Effect.Effect< { readonly sourceId: string; readonly toolCount: number }, OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure >; readonly removeSpec: (namespace: string, scope: string) => Effect.Effect<void, StorageFailure>; readonly getSource: ( namespace: string, scope: string, ) => Effect.Effect<StoredSource | null, StorageFailure>; readonly updateSource: ( namespace: string, scope: string, input: OpenApiUpdateSourceInput, ) => Effect.Effect<void, StorageFailure>; readonly listSourceBindings: ( sourceId: string, sourceScope: string, ) => Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure>; readonly setSourceBinding: ( input: OpenApiSourceBindingInput, ) => Effect.Effect<OpenApiSourceBindingRef, StorageFailure>; readonly removeSourceBinding: ( sourceId: string, sourceScope: string, slot: string, scope: string, ) => Effect.Effect<void, StorageFailure>;} // ---------------------------------------------------------------------------// Control-tool input/output schemas// --------------------------------------------------------------------------- const PreviewSpecInputSchema = Schema.Struct({ spec: Schema.String, specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), }), ),});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(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, 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}`), );}; const headerSlotFromName = (name: string): string => `header:${ name .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }`; const oauthClientIdSlot = (securitySchemeName: string): string => `oauth2:${ securitySchemeName .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }:client-id`; const oauthClientSecretSlot = (securitySchemeName: string): string => `oauth2:${ securitySchemeName .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }:client-secret`; const oauthConnectionSlot = (securitySchemeName: string): string => `oauth2:${ securitySchemeName .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }:connection`; const canonicalizeHeaders = ( headers: Record<string, OpenApiHeaderInput> | undefined,): { readonly headers: Record<string, ConfiguredHeaderValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; }>;} => { const nextHeaders: Record<string, ConfiguredHeaderValue> = {}; const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue }> = []; for (const [name, value] of Object.entries(headers ?? {})) { if (typeof value === "string") { nextHeaders[name] = value; continue; } if ("kind" in value) { nextHeaders[name] = value; continue; } const slot = headerSlotFromName(name); nextHeaders[name] = new ConfiguredHeaderBinding({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, value: { kind: "secret", secretId: SecretId.make(value.secretId), }, }); } return { headers: nextHeaders, bindings };}; const canonicalizeOAuth2 = ( oauth2: OpenApiOAuthInput | undefined,): { readonly oauth2?: OAuth2SourceConfig; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; }>;} => { if (!oauth2) return { bindings: [] }; if ("connectionSlot" in oauth2) { return { oauth2, bindings: [] }; } const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue }> = [ { slot: oauthClientIdSlot(oauth2.securitySchemeName), value: { kind: "secret", secretId: SecretId.make(oauth2.clientIdSecretId), }, }, ]; if (oauth2.clientSecretSecretId) { bindings.push({ slot: oauthClientSecretSlot(oauth2.securitySchemeName), value: { kind: "secret", secretId: SecretId.make(oauth2.clientSecretSecretId), }, }); } if (oauth2.connectionId) { bindings.push({ slot: oauthConnectionSlot(oauth2.securitySchemeName), value: { kind: "connection", connectionId: ConnectionId.make(oauth2.connectionId), }, }); } return { oauth2: new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: oauth2.securitySchemeName, flow: oauth2.flow, tokenUrl: oauth2.tokenUrl, authorizationUrl: oauth2.authorizationUrl, clientIdSlot: oauthClientIdSlot(oauth2.securitySchemeName), clientSecretSlot: oauth2.clientSecretSecretId ? oauthClientSecretSlot(oauth2.securitySchemeName) : null, connectionSlot: oauthConnectionSlot(oauth2.securitySchemeName), scopes: [...oauth2.scopes], }), bindings, };}; interface EffectiveSourceConfig { readonly config: SourceConfig; readonly headersSource: StoredSource; readonly oauth2Source: StoredSource;} const resolveEffectiveSourceConfig = ( ctx: PluginCtx<OpenapiStore>, base: StoredSource,): Effect.Effect<EffectiveSourceConfig, StorageFailure> => Effect.gen(function* () { const rank = new Map(ctx.scopes.map((scope, index) => [scope.id as string, index] as const)); const baseRank = rank.get(base.scope) ?? Infinity; let fallback: StoredSource | null = null; for (let index = baseRank + 1; index < ctx.scopes.length; index++) { const scope = ctx.scopes[index]; if (!scope) continue; fallback = yield* ctx.storage.getSource(base.namespace, scope.id as string); if (fallback) break; } if (!fallback) { return { config: base.config, headersSource: base, oauth2Source: base, }; } const hasBaseHeaders = Object.keys(base.config.headers ?? {}).length > 0; const hasBaseQueryParams = Object.keys(base.config.queryParams ?? {}).length > 0; return { config: { ...base.config, sourceUrl: base.config.sourceUrl ?? fallback.config.sourceUrl, baseUrl: base.config.baseUrl || fallback.config.baseUrl, namespace: base.config.namespace ?? fallback.config.namespace, headers: hasBaseHeaders ? base.config.headers : fallback.config.headers, queryParams: hasBaseQueryParams ? base.config.queryParams : fallback.config.queryParams, specFetchCredentials: base.config.specFetchCredentials ?? fallback.config.specFetchCredentials, oauth2: base.config.oauth2 ?? fallback.config.oauth2, }, headersSource: hasBaseHeaders ? base : fallback, oauth2Source: base.config.oauth2 ? base : fallback, }; }); const resolveConfiguredHeaders = ( ctx: PluginCtx<OpenapiStore>, params: { readonly sourceId: string; readonly sourceScope: string; readonly headers: Record<string, ConfiguredHeaderValue>; readonly legacyHeaders?: Record<string, HeaderValue>; },): Effect.Effect<Record<string, string>, OpenApiOAuthError | StorageFailure> => Effect.gen(function* () { const resolved: Record<string, string> = {}; for (const [name, value] of Object.entries(params.headers)) { if (typeof value === "string") { resolved[name] = value; continue; } const binding = yield* ctx.storage.resolveSourceBinding( params.sourceId, params.sourceScope, value.slot, ); if (binding?.value.kind === "secret") { const secret = yield* ctx.secrets.get(binding.value.secretId as string).pipe( Effect.mapError((err) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? new OpenApiOAuthError({ message: `Secret not found for header "${name}"`, }) : err, ), ); if (secret === null) { return yield* new OpenApiOAuthError({ message: `Missing secret "${binding.value.secretId}" for header "${name}"`, }); } resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret; continue; } if (binding?.value.kind === "text") { resolved[name] = value.prefix ? `${value.prefix}${binding.value.text}` : binding.value.text; continue; } const legacy = params.legacyHeaders?.[name]; if (legacy) { const fallback = yield* resolveHeaders({ [name]: legacy }, ctx.secrets).pipe( Effect.map((headers) => headers[name]!), Effect.mapError((err) => err instanceof OpenApiOAuthError ? err : new OpenApiOAuthError({ message: err.message }), ), ); resolved[name] = fallback; continue; } return yield* new OpenApiOAuthError({ message: `Missing binding for header "${name}"`, }); } return resolved; }); const resolveHeaderValues = ( ctx: PluginCtx<OpenapiStore>, values: Record<string, HeaderValue> | undefined,): Effect.Effect<Record<string, string>, OpenApiOAuthError | StorageFailure> => resolveSecretBackedMap({ values, getSecret: ctx.secrets.get, onMissing: (name) => new OpenApiOAuthError({ message: `Secret not found for "${name}"`, }), onError: (err, name) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? new OpenApiOAuthError({ message: `Secret not found for "${name}"`, }) : err, }).pipe( Effect.mapError((err) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? new OpenApiOAuthError({ message: "Secret resolution failed" }) : err, ),Lines 1–500 of 1081. Load more
import { Effect, Option, Schema } from "effect"; import type { Layer } from "effect"; import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import { OpenApiGroup } from "../api/group"; import { OpenApiExtensionService, OpenApiHandlers } from "../api/handlers"; import { ConnectionId, ScopeId, SecretId, SourceDetectionResult, definePlugin, resolveSecretBackedMap, type PluginCtx, type StorageFailure, type ToolAnnotations, type ToolRow, } from "@executor-js/sdk/core"; import { headersToConfigValues, type ConfigFileSink, type OpenApiSourceConfig, } from "@executor-js/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, ConfiguredHeaderBinding, OAuth2Auth, OAuth2SourceConfig, OpenApiSourceBindingInput, type OpenApiSourceBindingRef, type OpenApiSourceBindingValue, OperationBinding, type ConfiguredHeaderValue as ConfiguredHeaderValueValue, type HeaderValue as HeaderValueValue, } from "./types"; // --------------------------------------------------------------------------- // Plugin config // --------------------------------------------------------------------------- export type HeaderValue = HeaderValueValue; export type ConfiguredHeaderValue = ConfiguredHeaderValueValue; export type OpenApiHeaderInput = HeaderValue | ConfiguredHeaderValue; export type OpenApiOAuthInput = OAuth2Auth | OAuth2SourceConfig; export interface OpenApiSpecFetchCredentials { readonly headers?: Record<string, HeaderValue>; readonly queryParams?: Record<string, HeaderValue>; } export interface OpenApiPreviewInput { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentials; } export interface OpenApiSpecConfig { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentials; /** * Executor scope id that owns this source row. Must be one of the * executor's configured scopes. Typical shape: an admin adds the * source at the outermost (organization) scope so it's visible to * every inner (per-user) scope via fall-through reads. */ readonly scope: string; readonly name?: string; readonly baseUrl?: string; readonly namespace?: string; readonly headers?: Record<string, OpenApiHeaderInput>; readonly queryParams?: Record<string, HeaderValue>; readonly oauth2?: OpenApiOAuthInput; } export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, OpenApiHeaderInput>; readonly queryParams?: Record<string, HeaderValue>; /** Refresh the source's stored OAuth2 metadata after a successful * re-authenticate. */ readonly oauth2?: OpenApiOAuthInput; } /** * 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-js/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: ( input: string | OpenApiPreviewInput, ) => Effect.Effect< SpecPreview, OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure >; readonly addSpec: ( config: OpenApiSpecConfig, ) => Effect.Effect< { readonly sourceId: string; readonly toolCount: number }, OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure >; readonly removeSpec: (namespace: string, scope: string) => Effect.Effect<void, StorageFailure>; readonly getSource: ( namespace: string, scope: string, ) => Effect.Effect<StoredSource | null, StorageFailure>; readonly updateSource: ( namespace: string, scope: string, input: OpenApiUpdateSourceInput, ) => Effect.Effect<void, StorageFailure>; readonly listSourceBindings: ( sourceId: string, sourceScope: string, ) => Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure>; readonly setSourceBinding: ( input: OpenApiSourceBindingInput, ) => Effect.Effect<OpenApiSourceBindingRef, StorageFailure>; readonly removeSourceBinding: ( sourceId: string, sourceScope: string, slot: string, scope: string, ) => Effect.Effect<void, StorageFailure>; } // --------------------------------------------------------------------------- // Control-tool input/output schemas // --------------------------------------------------------------------------- const PreviewSpecInputSchema = Schema.Struct({ spec: Schema.String, specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), }), ), }); 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(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, 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}`), ); }; const headerSlotFromName = (name: string): string => `header:${ name .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }`; const oauthClientIdSlot = (securitySchemeName: string): string => `oauth2:${ securitySchemeName .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }:client-id`; const oauthClientSecretSlot = (securitySchemeName: string): string => `oauth2:${ securitySchemeName .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }:client-secret`; const oauthConnectionSlot = (securitySchemeName: string): string => `oauth2:${ securitySchemeName .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default" }:connection`; const canonicalizeHeaders = ( headers: Record<string, OpenApiHeaderInput> | undefined, ): { readonly headers: Record<string, ConfiguredHeaderValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; }>; } => { const nextHeaders: Record<string, ConfiguredHeaderValue> = {}; const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue }> = []; for (const [name, value] of Object.entries(headers ?? {})) { if (typeof value === "string") { nextHeaders[name] = value; continue; } if ("kind" in value) { nextHeaders[name] = value; continue; } const slot = headerSlotFromName(name); nextHeaders[name] = new ConfiguredHeaderBinding({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, value: { kind: "secret", secretId: SecretId.make(value.secretId), }, }); } return { headers: nextHeaders, bindings }; }; const canonicalizeOAuth2 = ( oauth2: OpenApiOAuthInput | undefined, ): { readonly oauth2?: OAuth2SourceConfig; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; }>; } => { if (!oauth2) return { bindings: [] }; if ("connectionSlot" in oauth2) { return { oauth2, bindings: [] }; } const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue }> = [ { slot: oauthClientIdSlot(oauth2.securitySchemeName), value: { kind: "secret", secretId: SecretId.make(oauth2.clientIdSecretId), }, }, ]; if (oauth2.clientSecretSecretId) { bindings.push({ slot: oauthClientSecretSlot(oauth2.securitySchemeName), value: { kind: "secret", secretId: SecretId.make(oauth2.clientSecretSecretId), }, }); } if (oauth2.connectionId) { bindings.push({ slot: oauthConnectionSlot(oauth2.securitySchemeName), value: { kind: "connection", connectionId: ConnectionId.make(oauth2.connectionId), }, }); } return { oauth2: new OAuth2SourceConfig({ kind: "oauth2", securitySchemeName: oauth2.securitySchemeName, flow: oauth2.flow, tokenUrl: oauth2.tokenUrl, authorizationUrl: oauth2.authorizationUrl, clientIdSlot: oauthClientIdSlot(oauth2.securitySchemeName), clientSecretSlot: oauth2.clientSecretSecretId ? oauthClientSecretSlot(oauth2.securitySchemeName) : null, connectionSlot: oauthConnectionSlot(oauth2.securitySchemeName), scopes: [...oauth2.scopes], }), bindings, }; }; interface EffectiveSourceConfig { readonly config: SourceConfig; readonly headersSource: StoredSource; readonly oauth2Source: StoredSource; } const resolveEffectiveSourceConfig = ( ctx: PluginCtx<OpenapiStore>, base: StoredSource, ): Effect.Effect<EffectiveSourceConfig, StorageFailure> => Effect.gen(function* () { const rank = new Map(ctx.scopes.map((scope, index) => [scope.id as string, index] as const)); const baseRank = rank.get(base.scope) ?? Infinity; let fallback: StoredSource | null = null; for (let index = baseRank + 1; index < ctx.scopes.length; index++) { const scope = ctx.scopes[index]; if (!scope) continue; fallback = yield* ctx.storage.getSource(base.namespace, scope.id as string); if (fallback) break; } if (!fallback) { return { config: base.config, headersSource: base, oauth2Source: base, }; } const hasBaseHeaders = Object.keys(base.config.headers ?? {}).length > 0; const hasBaseQueryParams = Object.keys(base.config.queryParams ?? {}).length > 0; return { config: { ...base.config, sourceUrl: base.config.sourceUrl ?? fallback.config.sourceUrl, baseUrl: base.config.baseUrl || fallback.config.baseUrl, namespace: base.config.namespace ?? fallback.config.namespace, headers: hasBaseHeaders ? base.config.headers : fallback.config.headers, queryParams: hasBaseQueryParams ? base.config.queryParams : fallback.config.queryParams, specFetchCredentials: base.config.specFetchCredentials ?? fallback.config.specFetchCredentials, oauth2: base.config.oauth2 ?? fallback.config.oauth2, }, headersSource: hasBaseHeaders ? base : fallback, oauth2Source: base.config.oauth2 ? base : fallback, }; }); const resolveConfiguredHeaders = ( ctx: PluginCtx<OpenapiStore>, params: { readonly sourceId: string; readonly sourceScope: string; readonly headers: Record<string, ConfiguredHeaderValue>; readonly legacyHeaders?: Record<string, HeaderValue>; }, ): Effect.Effect<Record<string, string>, OpenApiOAuthError | StorageFailure> => Effect.gen(function* () { const resolved: Record<string, string> = {}; for (const [name, value] of Object.entries(params.headers)) { if (typeof value === "string") { resolved[name] = value; continue; } const binding = yield* ctx.storage.resolveSourceBinding( params.sourceId, params.sourceScope, value.slot, ); if (binding?.value.kind === "secret") { const secret = yield* ctx.secrets.get(binding.value.secretId as string).pipe( Effect.mapError((err) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? new OpenApiOAuthError({ message: `Secret not found for header "${name}"`, }) : err, ), ); if (secret === null) { return yield* new OpenApiOAuthError({ message: `Missing secret "${binding.value.secretId}" for header "${name}"`, }); } resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret; continue; } if (binding?.value.kind === "text") { resolved[name] = value.prefix ? `${value.prefix}${binding.value.text}` : binding.value.text; continue; } const legacy = params.legacyHeaders?.[name]; if (legacy) { const fallback = yield* resolveHeaders({ [name]: legacy }, ctx.secrets).pipe( Effect.map((headers) => headers[name]!), Effect.mapError((err) => err instanceof OpenApiOAuthError ? err : new OpenApiOAuthError({ message: err.message }), ), ); resolved[name] = fallback; continue; } return yield* new OpenApiOAuthError({ message: `Missing binding for header "${name}"`, }); } return resolved; }); const resolveHeaderValues = ( ctx: PluginCtx<OpenapiStore>, values: Record<string, HeaderValue> | undefined, ): Effect.Effect<Record<string, string>, OpenApiOAuthError | StorageFailure> => resolveSecretBackedMap({ values, getSecret: ctx.secrets.get, onMissing: (name) => new OpenApiOAuthError({ message: `Secret not found for "${name}"`, }), onError: (err, name) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? new OpenApiOAuthError({ message: `Secret not found for "${name}"`, }) : err, }).pipe( Effect.mapError((err) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? new OpenApiOAuthError({ message: "Secret resolution failed" }) : err, ),