import { randomUUID } from "node:crypto"; import { Effect, Option, Schema } from "effect";import { FetchHttpClient, HttpClient } from "@effect/platform";import type { Layer } from "effect"; import { OAuth2Error, buildAuthorizationUrl, createPkceCodeVerifier, exchangeAuthorizationCode, exchangeClientCredentials, refreshAccessToken, type OAuth2TokenResponse,} from "@executor/plugin-oauth2"; import { ConnectionId, ConnectionRefreshError, CreateConnectionInput, ScopeId, SecretId, SourceDetectionResult, TokenMaterial, definePlugin, type ConnectionProvider, type ConnectionRefreshInput, type ConnectionRefreshResult, type PluginCtx, 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, ConfiguredHeaderBinding, OAuth2Auth, OAuth2SourceConfig, OpenApiOAuthSession, 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 OpenApiSpecConfig { readonly spec: string; /** * 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 oauth2?: OpenApiOAuthInput;} export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, OpenApiHeaderInput>; /** Refresh the source's stored OAuth2 metadata after a successful * re-authenticate. */ readonly oauth2?: OpenApiOAuthInput;} // ---------------------------------------------------------------------------// OAuth2 onboarding inputs / outputs — callers pre-decide identity knobs// (display name, scheme name, scopes, target scope) and the SDK mints a// Connection when the flow completes. The caller receives an OAuth2Auth// carrying just the resulting connection id.// --------------------------------------------------------------------------- interface StartOAuthIdentity { readonly displayName: string; readonly securitySchemeName: string; readonly clientIdSecretId: string; readonly scopes: readonly string[]; /** Stable logical Connection id this source should resolve at invoke time. * Physical ownership still comes from `tokenScope`; the same id can * have separate rows at user/org scopes. Defaults to the legacy * source-derived id for compatibility. */ readonly connectionId?: string; /** * Source (namespace) the resulting Connection will back. Used as the * compatibility default for the stable Connection *name* so repeat * sign-ins refresh a single row per scope instead of spawning a * fresh UUID every click: * * clientCredentials → `openapi-oauth2-app-${sourceId}` * authorizationCode → `openapi-oauth2-user-${sourceId}` * * The resulting Connection is written at the innermost executor * scope so per-user credentials (secrets shadowed at user scope via * `ctx.secrets.get`'s scope-stacked resolution) and per-user consent * (authorizationCode) both produce per-user rows. Because * `findInnermostConnectionRow` resolves by id across the caller's * stack, the single `source.oauth2.connectionId` string on a shared * org source still lets every user reach their own physical row. */ readonly sourceId: string; /** Executor scope that will own the resulting Connection (and its * backing token secrets). Defaults to `ctx.scopes[0].id`. Callers * can override to write at a different stack scope (e.g. an admin * writing an org-wide shared connection). */ readonly tokenScope?: string;} const defaultOAuthConnectionId = ( flow: "authorizationCode" | "clientCredentials", sourceId: string,): string => flow === "clientCredentials" ? `openapi-oauth2-app-${sourceId}` : `openapi-oauth2-user-${sourceId}`; export interface StartAuthorizationCodeOAuthInput extends StartOAuthIdentity { readonly flow: "authorizationCode"; readonly authorizationUrl: string; readonly tokenUrl: string; readonly redirectUrl: string; readonly clientSecretSecretId?: string | null;} /** * RFC 6749 §4.4 has no user-interactive step. `startOAuth` exchanges * tokens inline, creates the Connection, and returns a completed * `OAuth2Auth` pointing at it. No `authorizationUrl`, no session row, * no `completeOAuth`. */export interface StartClientCredentialsOAuthInput extends StartOAuthIdentity { readonly flow: "clientCredentials"; readonly tokenUrl: string; /** RFC 6749 §2.3.1 — client_credentials is unusable without the secret. */ readonly clientSecretSecretId: string;} export type OpenApiStartOAuthInput = | StartAuthorizationCodeOAuthInput | StartClientCredentialsOAuthInput; export interface StartAuthorizationCodeOAuthResponse { readonly flow: "authorizationCode"; readonly sessionId: string; readonly authorizationUrl: string; readonly scopes: readonly string[];} export interface StartClientCredentialsOAuthResponse { readonly flow: "clientCredentials"; /** Completed auth ready to attach to the source's `OAuth2Auth`. */ readonly auth: OAuth2Auth; readonly scopes: readonly string[];} export type OpenApiStartOAuthResponse = | StartAuthorizationCodeOAuthResponse | StartClientCredentialsOAuthResponse; 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, 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>; 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}`), );}; 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; 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, oauth2: base.config.oauth2 ?? fallback.config.oauth2, }, headersSource: hasBaseHeaders ? base : fallback,Lines 1–500 of 1570. 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 { OAuth2Error, buildAuthorizationUrl, createPkceCodeVerifier, exchangeAuthorizationCode, exchangeClientCredentials, refreshAccessToken, type OAuth2TokenResponse, } from "@executor/plugin-oauth2"; import { ConnectionId, ConnectionRefreshError, CreateConnectionInput, ScopeId, SecretId, SourceDetectionResult, TokenMaterial, definePlugin, type ConnectionProvider, type ConnectionRefreshInput, type ConnectionRefreshResult, type PluginCtx, 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, ConfiguredHeaderBinding, OAuth2Auth, OAuth2SourceConfig, OpenApiOAuthSession, 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 OpenApiSpecConfig { readonly spec: string; /** * 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 oauth2?: OpenApiOAuthInput; } export interface OpenApiUpdateSourceInput { readonly name?: string; readonly baseUrl?: string; readonly headers?: Record<string, OpenApiHeaderInput>; /** Refresh the source's stored OAuth2 metadata after a successful * re-authenticate. */ readonly oauth2?: OpenApiOAuthInput; } // --------------------------------------------------------------------------- // OAuth2 onboarding inputs / outputs — callers pre-decide identity knobs // (display name, scheme name, scopes, target scope) and the SDK mints a // Connection when the flow completes. The caller receives an OAuth2Auth // carrying just the resulting connection id. // --------------------------------------------------------------------------- interface StartOAuthIdentity { readonly displayName: string; readonly securitySchemeName: string; readonly clientIdSecretId: string; readonly scopes: readonly string[]; /** Stable logical Connection id this source should resolve at invoke time. * Physical ownership still comes from `tokenScope`; the same id can * have separate rows at user/org scopes. Defaults to the legacy * source-derived id for compatibility. */ readonly connectionId?: string; /** * Source (namespace) the resulting Connection will back. Used as the * compatibility default for the stable Connection *name* so repeat * sign-ins refresh a single row per scope instead of spawning a * fresh UUID every click: * * clientCredentials → `openapi-oauth2-app-${sourceId}` * authorizationCode → `openapi-oauth2-user-${sourceId}` * * The resulting Connection is written at the innermost executor * scope so per-user credentials (secrets shadowed at user scope via * `ctx.secrets.get`'s scope-stacked resolution) and per-user consent * (authorizationCode) both produce per-user rows. Because * `findInnermostConnectionRow` resolves by id across the caller's * stack, the single `source.oauth2.connectionId` string on a shared * org source still lets every user reach their own physical row. */ readonly sourceId: string; /** Executor scope that will own the resulting Connection (and its * backing token secrets). Defaults to `ctx.scopes[0].id`. Callers * can override to write at a different stack scope (e.g. an admin * writing an org-wide shared connection). */ readonly tokenScope?: string; } const defaultOAuthConnectionId = ( flow: "authorizationCode" | "clientCredentials", sourceId: string, ): string => flow === "clientCredentials" ? `openapi-oauth2-app-${sourceId}` : `openapi-oauth2-user-${sourceId}`; export interface StartAuthorizationCodeOAuthInput extends StartOAuthIdentity { readonly flow: "authorizationCode"; readonly authorizationUrl: string; readonly tokenUrl: string; readonly redirectUrl: string; readonly clientSecretSecretId?: string | null; } /** * RFC 6749 §4.4 has no user-interactive step. `startOAuth` exchanges * tokens inline, creates the Connection, and returns a completed * `OAuth2Auth` pointing at it. No `authorizationUrl`, no session row, * no `completeOAuth`. */ export interface StartClientCredentialsOAuthInput extends StartOAuthIdentity { readonly flow: "clientCredentials"; readonly tokenUrl: string; /** RFC 6749 §2.3.1 — client_credentials is unusable without the secret. */ readonly clientSecretSecretId: string; } export type OpenApiStartOAuthInput = | StartAuthorizationCodeOAuthInput | StartClientCredentialsOAuthInput; export interface StartAuthorizationCodeOAuthResponse { readonly flow: "authorizationCode"; readonly sessionId: string; readonly authorizationUrl: string; readonly scopes: readonly string[]; } export interface StartClientCredentialsOAuthResponse { readonly flow: "clientCredentials"; /** Completed auth ready to attach to the source's `OAuth2Auth`. */ readonly auth: OAuth2Auth; readonly scopes: readonly string[]; } export type OpenApiStartOAuthResponse = | StartAuthorizationCodeOAuthResponse | StartClientCredentialsOAuthResponse; 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, 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>; 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}`), ); }; 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; 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, oauth2: base.config.oauth2 ?? fallback.config.oauth2, }, headersSource: hasBaseHeaders ? base : fallback,