import { Effect, Option, Predicate, Schema } from "effect";import type { Layer } from "effect";import { HttpClient } from "effect/unstable/http"; import { ScopeId, SecretId, SourceDetectionResult, StorageError, definePlugin, tool, resolveSecretBackedMap, type CredentialBindingRef, 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 } 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, ConfiguredHeaderValue as ConfiguredHeaderValueSchema, ConfiguredHeaderBinding, OAuth2SourceConfig, OpenApiCredentialInput as OpenApiCredentialInputSchema, OpenApiSourceBindingInput, OpenApiSourceBindingRef, type OpenApiCredentialInput as OpenApiCredentialInputValue, 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 OpenApiCredentialInput = OpenApiCredentialInputValue;export type OpenApiOAuthInput = OAuth2SourceConfig; export interface OpenApiSpecFetchCredentialsInput { readonly headers?: Record<string, HeaderValue>; readonly queryParams?: Record<string, HeaderValue>;} export interface OpenApiPreviewInput { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentialsInput;} export interface OpenApiSpecConfig { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentialsInput; /** * 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, OpenApiCredentialInput>; readonly queryParams?: Record<string, OpenApiCredentialInput>; readonly oauth2?: OpenApiOAuthInput; readonly credentialTargetScope?: string;} export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, OpenApiCredentialInput>; readonly queryParams?: Record<string, OpenApiCredentialInput>; readonly credentialTargetScope?: string; /** 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)), }), ),}); const OpenApiHeaderInputSchema = Schema.Union([HeaderValueSchema, ConfiguredHeaderValueSchema]);const OpenApiOAuthInputSchema = OAuth2SourceConfig; const AddSourceInputSchema = Schema.Struct({ scope: Schema.String, spec: Schema.String, name: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), namespace: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, OpenApiHeaderInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, OpenApiCredentialInputSchema)), oauth2: Schema.optional(OpenApiOAuthInputSchema), credentialTargetScope: Schema.optional(Schema.String), specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), }), ),}); const AddSourceOutputSchema = Schema.Struct({ sourceId: Schema.String, toolCount: Schema.Number,}); const PreviewSpecInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(PreviewSpecInputSchema),);const AddSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(AddSourceInputSchema),);const AddSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(AddSourceOutputSchema),); // ---------------------------------------------------------------------------// 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 => OperationBinding.make({ 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 slotPart = (value: string): string => value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default"; const headerSlotFromName = (name: string): string => `header:${slotPart(name)}`; const queryParamSlotFromName = (name: string): string => `query_param:${slotPart(name)}`; const specFetchHeaderSlotFromName = (name: string): string => `spec_fetch_header:${slotPart(name)}`; const specFetchQueryParamSlotFromName = (name: string): string => `spec_fetch_query_param:${slotPart(name)}`; const canonicalizeHeaders = ( headers: Record<string, OpenApiCredentialInput> | undefined,): { readonly headers: Record<string, ConfiguredHeaderValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; readonly targetScope?: string; }>;} => { const nextHeaders: Record<string, ConfiguredHeaderValue> = {}; const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue; targetScope?: string; }> = []; 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] = ConfiguredHeaderBinding.make({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, targetScope: "targetScope" in value ? value.targetScope : undefined, value: { kind: "secret", secretId: SecretId.make(value.secretId), ...("secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}), }, }); } return { headers: nextHeaders, bindings };}; const canonicalizeCredentialMap = ( values: Record<string, OpenApiCredentialInput> | undefined, slotForName: (name: string) => string,): { readonly values: Record<string, ConfiguredHeaderValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; readonly targetScope?: string; }>;} => { const nextValues: Record<string, ConfiguredHeaderValue> = {}; const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue; targetScope?: string; }> = []; for (const [name, value] of Object.entries(values ?? {})) { if (typeof value === "string") { nextValues[name] = value; continue; } if ("kind" in value) { nextValues[name] = value; continue; } const slot = slotForName(name); nextValues[name] = ConfiguredHeaderBinding.make({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, targetScope: "targetScope" in value ? value.targetScope : undefined, value: { kind: "secret", secretId: SecretId.make(value.secretId), ...("secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}), }, }); } return { values: nextValues, bindings };}; const canonicalizeSpecFetchCredentials = ( credentials: | { readonly headers?: Record<string, OpenApiCredentialInput>; readonly queryParams?: Record<string, OpenApiCredentialInput>; } | undefined,): { readonly credentials?: SourceConfig["specFetchCredentials"]; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; readonly targetScope?: string; }>;} => { const headers = canonicalizeCredentialMap(credentials?.headers, specFetchHeaderSlotFromName); const queryParams = canonicalizeCredentialMap( credentials?.queryParams, specFetchQueryParamSlotFromName, ); const nextCredentials = Object.keys(headers.values).length === 0 && Object.keys(queryParams.values).length === 0 ? undefined : { ...(Object.keys(headers.values).length > 0 ? { headers: headers.values } : {}), ...(Object.keys(queryParams.values).length > 0 ? { queryParams: queryParams.values } : {}), }; return { credentials: nextCredentials, bindings: [...headers.bindings, ...queryParams.bindings], };}; const canonicalizeOAuth2 = ( oauth2: OpenApiOAuthInput | undefined,): { readonly oauth2?: OAuth2SourceConfig; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; }>;} => { if (!oauth2) return { bindings: [] }; return { oauth2, bindings: [], };}; interface EffectiveSourceConfig { readonly config: SourceConfig; readonly headersSource: StoredSource; readonly queryParamsSource: StoredSource; readonly specFetchCredentialsSource: StoredSource; readonly oauth2Source: StoredSource;} const OPENAPI_PLUGIN_ID = "openapi"; const scopeRanks = (ctx: PluginCtx<OpenapiStore>): ReadonlyMap<string, number> => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index])); const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: string): number => ranks.get(scopeId) ?? Infinity; const coreBindingToOpenApiBinding = (binding: CredentialBindingRef): OpenApiSourceBindingRef => OpenApiSourceBindingRef.make({ sourceId: binding.sourceId, sourceScopeId: binding.sourceScopeId, scopeId: binding.scopeId, slot: binding.slotKey, value: binding.value, createdAt: binding.createdAt, updatedAt: binding.updatedAt, }); const listOpenApiSourceBindings = ( ctx: PluginCtx<OpenapiStore>, sourceId: string, sourceScope: string,): Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return []; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: OPENAPI_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); return bindings .filter((binding) => scopeRank(ranks, binding.scopeId) <= sourceSourceRank) .map(coreBindingToOpenApiBinding); }); const resolveOpenApiSourceBinding = ( ctx: PluginCtx<OpenapiStore>, sourceId: string, sourceScope: string, slot: string,): Effect.Effect<OpenApiSourceBindingRef | null, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return null; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: OPENAPI_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); const binding = bindings .filter( (candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank, ) .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0]; return binding ? coreBindingToOpenApiBinding(binding) : null; }); const validateOpenApiBindingTarget = ( ctx: PluginCtx<OpenapiStore>, input: { readonly sourceScope: string; readonly targetScope: string; readonly sourceId: string; },): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, input.sourceScope);Lines 1–500 of 1395. Load more
import { Effect, Option, Predicate, Schema } from "effect"; import type { Layer } from "effect"; import { HttpClient } from "effect/unstable/http"; import { ScopeId, SecretId, SourceDetectionResult, StorageError, definePlugin, tool, resolveSecretBackedMap, type CredentialBindingRef, 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 } 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, ConfiguredHeaderValue as ConfiguredHeaderValueSchema, ConfiguredHeaderBinding, OAuth2SourceConfig, OpenApiCredentialInput as OpenApiCredentialInputSchema, OpenApiSourceBindingInput, OpenApiSourceBindingRef, type OpenApiCredentialInput as OpenApiCredentialInputValue, 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 OpenApiCredentialInput = OpenApiCredentialInputValue; export type OpenApiOAuthInput = OAuth2SourceConfig; export interface OpenApiSpecFetchCredentialsInput { readonly headers?: Record<string, HeaderValue>; readonly queryParams?: Record<string, HeaderValue>; } export interface OpenApiPreviewInput { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentialsInput; } export interface OpenApiSpecConfig { readonly spec: string; readonly specFetchCredentials?: OpenApiSpecFetchCredentialsInput; /** * 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, OpenApiCredentialInput>; readonly queryParams?: Record<string, OpenApiCredentialInput>; readonly oauth2?: OpenApiOAuthInput; readonly credentialTargetScope?: string; } export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, OpenApiCredentialInput>; readonly queryParams?: Record<string, OpenApiCredentialInput>; readonly credentialTargetScope?: string; /** 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)), }), ), }); const OpenApiHeaderInputSchema = Schema.Union([HeaderValueSchema, ConfiguredHeaderValueSchema]); const OpenApiOAuthInputSchema = OAuth2SourceConfig; const AddSourceInputSchema = Schema.Struct({ scope: Schema.String, spec: Schema.String, name: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), namespace: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, OpenApiHeaderInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, OpenApiCredentialInputSchema)), oauth2: Schema.optional(OpenApiOAuthInputSchema), credentialTargetScope: Schema.optional(Schema.String), specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)), }), ), }); const AddSourceOutputSchema = Schema.Struct({ sourceId: Schema.String, toolCount: Schema.Number, }); const PreviewSpecInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(PreviewSpecInputSchema), ); const AddSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(AddSourceInputSchema), ); const AddSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(AddSourceOutputSchema), ); // --------------------------------------------------------------------------- // 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 => OperationBinding.make({ 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 slotPart = (value: string): string => value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "default"; const headerSlotFromName = (name: string): string => `header:${slotPart(name)}`; const queryParamSlotFromName = (name: string): string => `query_param:${slotPart(name)}`; const specFetchHeaderSlotFromName = (name: string): string => `spec_fetch_header:${slotPart(name)}`; const specFetchQueryParamSlotFromName = (name: string): string => `spec_fetch_query_param:${slotPart(name)}`; const canonicalizeHeaders = ( headers: Record<string, OpenApiCredentialInput> | undefined, ): { readonly headers: Record<string, ConfiguredHeaderValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; readonly targetScope?: string; }>; } => { const nextHeaders: Record<string, ConfiguredHeaderValue> = {}; const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue; targetScope?: string; }> = []; 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] = ConfiguredHeaderBinding.make({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, targetScope: "targetScope" in value ? value.targetScope : undefined, value: { kind: "secret", secretId: SecretId.make(value.secretId), ...("secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}), }, }); } return { headers: nextHeaders, bindings }; }; const canonicalizeCredentialMap = ( values: Record<string, OpenApiCredentialInput> | undefined, slotForName: (name: string) => string, ): { readonly values: Record<string, ConfiguredHeaderValue>; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; readonly targetScope?: string; }>; } => { const nextValues: Record<string, ConfiguredHeaderValue> = {}; const bindings: Array<{ slot: string; value: OpenApiSourceBindingValue; targetScope?: string; }> = []; for (const [name, value] of Object.entries(values ?? {})) { if (typeof value === "string") { nextValues[name] = value; continue; } if ("kind" in value) { nextValues[name] = value; continue; } const slot = slotForName(name); nextValues[name] = ConfiguredHeaderBinding.make({ kind: "binding", slot, prefix: value.prefix, }); bindings.push({ slot, targetScope: "targetScope" in value ? value.targetScope : undefined, value: { kind: "secret", secretId: SecretId.make(value.secretId), ...("secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}), }, }); } return { values: nextValues, bindings }; }; const canonicalizeSpecFetchCredentials = ( credentials: | { readonly headers?: Record<string, OpenApiCredentialInput>; readonly queryParams?: Record<string, OpenApiCredentialInput>; } | undefined, ): { readonly credentials?: SourceConfig["specFetchCredentials"]; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; readonly targetScope?: string; }>; } => { const headers = canonicalizeCredentialMap(credentials?.headers, specFetchHeaderSlotFromName); const queryParams = canonicalizeCredentialMap( credentials?.queryParams, specFetchQueryParamSlotFromName, ); const nextCredentials = Object.keys(headers.values).length === 0 && Object.keys(queryParams.values).length === 0 ? undefined : { ...(Object.keys(headers.values).length > 0 ? { headers: headers.values } : {}), ...(Object.keys(queryParams.values).length > 0 ? { queryParams: queryParams.values } : {}), }; return { credentials: nextCredentials, bindings: [...headers.bindings, ...queryParams.bindings], }; }; const canonicalizeOAuth2 = ( oauth2: OpenApiOAuthInput | undefined, ): { readonly oauth2?: OAuth2SourceConfig; readonly bindings: ReadonlyArray<{ readonly slot: string; readonly value: OpenApiSourceBindingValue; }>; } => { if (!oauth2) return { bindings: [] }; return { oauth2, bindings: [], }; }; interface EffectiveSourceConfig { readonly config: SourceConfig; readonly headersSource: StoredSource; readonly queryParamsSource: StoredSource; readonly specFetchCredentialsSource: StoredSource; readonly oauth2Source: StoredSource; } const OPENAPI_PLUGIN_ID = "openapi"; const scopeRanks = (ctx: PluginCtx<OpenapiStore>): ReadonlyMap<string, number> => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index])); const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: string): number => ranks.get(scopeId) ?? Infinity; const coreBindingToOpenApiBinding = (binding: CredentialBindingRef): OpenApiSourceBindingRef => OpenApiSourceBindingRef.make({ sourceId: binding.sourceId, sourceScopeId: binding.sourceScopeId, scopeId: binding.scopeId, slot: binding.slotKey, value: binding.value, createdAt: binding.createdAt, updatedAt: binding.updatedAt, }); const listOpenApiSourceBindings = ( ctx: PluginCtx<OpenapiStore>, sourceId: string, sourceScope: string, ): Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return []; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: OPENAPI_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); return bindings .filter((binding) => scopeRank(ranks, binding.scopeId) <= sourceSourceRank) .map(coreBindingToOpenApiBinding); }); const resolveOpenApiSourceBinding = ( ctx: PluginCtx<OpenapiStore>, sourceId: string, sourceScope: string, slot: string, ): Effect.Effect<OpenApiSourceBindingRef | null, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return null; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: OPENAPI_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); const binding = bindings .filter( (candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank, ) .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0]; return binding ? coreBindingToOpenApiBinding(binding) : null; }); const validateOpenApiBindingTarget = ( ctx: PluginCtx<OpenapiStore>, input: { readonly sourceScope: string; readonly targetScope: string; readonly sourceId: string; }, ): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, input.sourceScope);