import type { IntegrationSlug } from "./ids"; /* Core knows only an integration's catalog identity — slug + description + which * plugin (`kind`) owns it. The type-specific shape (openapi auth templates + spec, * an mcp url, …) lives in the plugin and is stored as an opaque `config` blob core * never parses. An integration is one API surface; multi-API providers (Google) * are bundled into a single integration by their plugin, so one credential covers * the whole provider. */ // --------------------------------------------------------------------------- // Declared auth methods — a plugin-agnostic projection of an integration's // stored `config` into the catalog response. Each plugin derives these from its // own opaque config (`describeAuthMethods`); core never parses config itself. // The client renders these as the integration's selectable auth methods, so the // catalog is authoritative even when the integration has zero connections. // // This is a DERIVED projection — there is no DB column. A plugin that declares // no projector contributes `[]`, and the client falls through to its existing // connection-inference behavior (no regression). // --------------------------------------------------------------------------- export interface IntegrationDisplayDescriptor { /** Non-secret URL suitable for display metadata such as favicons. */ readonly url?: string; /** Catalog family derived from plugin config for grouped display. */ readonly family?: string; } /** Where a credential value is carried. `header`/`query` place it on an * outbound HTTP request (mirrors the client's `Placement`); `env` injects it * as an environment variable for a stdio (subprocess) integration. */ export interface AuthPlacementDescriptor { readonly carrier: "header" | "query" | "env"; readonly name: string; /** Literal prepended to the value (e.g. `"Bearer "`). Empty when bare. */ readonly prefix: string; /** The input variable this placement renders from. `token` for single-input * methods; a distinct name per input for multi-input ones (e.g. Datadog). * Absent → treated as `token`. */ readonly variable?: string; /** Set when the placement renders this exact value instead of a credential * (a static header/param the method carries). Such placements reference no * input variable. */ readonly literal?: string; } /** OAuth specifics for an `oauth` auth method. For probe-at-connect providers * (MCP) only `discoveryUrl` + `supportsDynamicRegistration` are known up front; * the authorize/token endpoints are discovered live at connect time. For * providers that store endpoints (OpenAPI) the resolved URLs are carried. */ export interface AuthMethodOAuthDescriptor { /** For probe-at-connect providers (MCP): the endpoint to discover metadata * from (RFC 9728 PRM → RFC 8414 AS metadata). */ readonly discoveryUrl?: string; readonly authorizationUrl?: string; readonly tokenUrl?: string; readonly resource?: string | null; readonly scopes?: readonly string[]; readonly registrationEndpoint?: string; /** True when the integration is known to support RFC 7591 dynamic client * registration (drives the transparent auto-register connect flow). */ readonly supportsDynamicRegistration?: boolean; /** True when the authorization server supports Client ID Metadata Document * clients. The UI can create a local public OAuth client using this host's * metadata-document URL as `client_id`, with no provider app registration. */ readonly supportsClientIdMetadataDocument?: boolean; } /** A single declared auth method on an integration's catalog response. */ export interface AuthMethodDescriptor { /** Stable id within the integration (e.g. the auth template slug). */ readonly id: string; readonly label: string; readonly kind: "oauth" | "apikey" | "header" | "none"; /** The auth-template slug a connection binds against. */ readonly template: string; readonly placements?: readonly AuthPlacementDescriptor[]; readonly oauth?: AuthMethodOAuthDescriptor; } /** Public projection of an integration — what `integrations.list/get` return. * Carries no credentials and no plugin-internal config. */ export interface Integration { readonly slug: IntegrationSlug; /** Display name. Pre-split rows stored the name in `description`; readers * fall back, so this is always populated. */ readonly name: string; /** Agent-visible context ("what this API is and when to reach for it"). * Distinct from the display name; may equal it on legacy rows. */ readonly description: string; /** The plugin that owns this integration kind (e.g. "openapi", "mcp"). */ readonly kind: string; /** Whether the user can remove this integration from the catalog. `false` * for static / built-in integrations declared by a plugin at startup. */ readonly canRemove: boolean; /** Whether the owning plugin supports re-resolving a connection's tools * (`connections.refresh`). */ readonly canRefresh: boolean; /** Declared auth methods derived from the owning plugin's stored config (a * derived projection, not a DB column). Always present, possibly empty. */ readonly authMethods: readonly AuthMethodDescriptor[]; /** Non-secret display URL derived by the owning plugin from opaque config. * Used for catalog favicons; never includes credentials or plugin config. */ readonly displayUrl?: string; /** Catalog family derived by the owning plugin from opaque config. */ readonly family?: string; } /** Plugin-owned, opaque-to-core configuration stored on the integration row. The * owning plugin writes it at register time and reads it back at execute time to * render auth / produce tools. Core treats it as an opaque JSON blob. */ export type IntegrationConfig = unknown; // --------------------------------------------------------------------------- // Auth-template merge — shared by every plugin whose config carries a slugged // `authenticationTemplate` array (openapi, graphql, mcp). The custom-method // flow merge-appends: an incoming entry with a matching slug replaces the // existing entry in place; entries lacking a slug (or colliding with another // entry added in the same call) get a fresh `custom_` slug. // --------------------------------------------------------------------------- const shortId = (): string => Math.random().toString(36).slice(2, 8); export const freshCustomAuthSlug = (taken: ReadonlySet): string => { let candidate = `custom_${shortId()}`; while (taken.has(candidate)) candidate = `custom_${shortId()}`; return candidate; }; export const mergeAuthTemplates = ( existing: readonly T[], incoming: readonly T[], ): readonly T[] => { const result: T[] = existing.map((entry: T) => entry); const taken = new Set(result.map((entry: T) => String(entry.slug))); for (const entry of incoming) { // `slug` may be branded-required in the plugin's schema, but JSON callers // can submit it empty/blank — read defensively and backfill so every // stored template has a stable slug. const rawSlug = (entry as { readonly slug?: unknown }).slug; const requested = typeof rawSlug === "string" ? rawSlug.trim() : ""; const existingIndex = result.findIndex((current: T) => String(current.slug) === requested); if (requested.length > 0 && existingIndex >= 0) { result[existingIndex] = entry; continue; } const slug = requested.length > 0 && !taken.has(requested) ? requested : freshCustomAuthSlug(taken); taken.add(slug); result.push({ ...entry, slug } as T); } return result; }; /** * A durable change to the integration catalog, emitted through * `ExecutorConfig.onIntegrationChange`: a row was created (`added` — upsert * re-registers of an existing slug do not fire) or removed. The hook is a * neutral notification seam — hosts decide what to do with it (the non-cloud * hosts feed product analytics); core carries no analytics vocabulary. */ export interface IntegrationChangeEvent { readonly kind: "added" | "removed"; /** The owning plugin's id (`openapi`, `mcp`, `graphql`, ...). */ readonly pluginKey: string; readonly slug: IntegrationSlug; } /** What a plugin's extension method passes to `ctx.core.integrations.register`. * The v2 analog of v1's `SourceInput`, minus the per-source tool list (tools are * produced per-connection now). */ export interface RegisterIntegrationInput { readonly slug: IntegrationSlug; /** Display name. Falls back to `description` then the slug when omitted * (legacy callers registered with description-as-name). */ readonly name?: string; readonly description: string; /** Opaque plugin config (auth templates, spec ref, mcp url, …). */ readonly config: IntegrationConfig; readonly canRemove?: boolean; readonly canRefresh?: boolean; }