import type { IntegrationSlug } from "./ids";import type { EnterpriseIdentityProviderDescriptor } from "./oauth-client";/* 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; /** The enterprise identity provider this integration is configured to obtain * identity assertions from (MCP Enterprise-Managed Authorization). Present * only when the deployment has declared one for this integration; the * connect path still verifies at discovery time that the server advertises * the ID-JAG grant profile, and falls back to the interactive flow when it * does not. */ readonly enterpriseIdentityProvider?: EnterpriseIdentityProviderDescriptor;}/** 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_<id>` slug.// ---------------------------------------------------------------------------const shortId = (): string => Math.random().toString(36).slice(2, 8);export const freshCustomAuthSlug = (taken: ReadonlySet<string>): string => { let candidate = `custom_${shortId()}`; while (taken.has(candidate)) candidate = `custom_${shortId()}`; return candidate;};export const mergeAuthTemplates = <T extends { readonly slug: string }>( existing: readonly T[], incoming: readonly T[],): readonly T[] => { const result: T[] = existing.map((entry: T) => entry); const taken = new Set<string>(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;}
import type { IntegrationSlug } from "./ids";
import type { EnterpriseIdentityProviderDescriptor } from "./oauth-client";
/* 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;
/** The enterprise identity provider this integration is configured to obtain
* identity assertions from (MCP Enterprise-Managed Authorization). Present
* only when the deployment has declared one for this integration; the
* connect path still verifies at discovery time that the server advertises
* the ID-JAG grant profile, and falls back to the interactive flow when it
* does not. */
readonly enterpriseIdentityProvider?: EnterpriseIdentityProviderDescriptor;
}
/** 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_<id>` slug.
// ---------------------------------------------------------------------------
const shortId = (): string => Math.random().toString(36).slice(2, 8);
export const freshCustomAuthSlug = (taken: ReadonlySet<string>): string => {
let candidate = `custom_${shortId()}`;
while (taken.has(candidate)) candidate = `custom_${shortId()}`;
return candidate;
};
export const mergeAuthTemplates = <T extends { readonly slug: string }>(
existing: readonly T[],
incoming: readonly T[],
): readonly T[] => {
const result: T[] = existing.map((entry: T) => entry);
const taken = new Set<string>(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;
}