import { type Placement } from "../lib/auth-placements"; import { Button } from "./button"; import { FilterTabs } from "./filter-tabs"; import { Input } from "./input"; import { Label } from "./label"; import { PlacementEditor } from "./placement-editor"; // --------------------------------------------------------------------------- // Auth-template editor — the single "How does this API authenticate?" editor. // // Add-time only: this declares an integration's auth TEMPLATE (where the // credential goes / which OAuth endpoints), never a secret and never a network // call. Plugin-agnostic — each plugin owns the mapping from this generic value // to its own stored wire template (mirroring the `Placement` model and the // per-plugin `auth-method-config` converters). One editor, three plugins. // // Tabs: // - `none` — open / no credential. // - `apikey` — one secret sent at one or more PLACEMENTS (header / query), // edited through the shared `PlacementEditor`. When `presets` // are supplied (OpenAPI spec-detected methods) a pill row offers // editable defaults: clicking a pill pre-fills the value. // - `oauth` — provider authorize/token URLs + scopes. No client id/secret — // that lives on the OAuth client, registered at connect time. // --------------------------------------------------------------------------- export type AuthTemplateEditorKind = "none" | "apikey" | "oauth"; export type AuthTemplateEditorValue = | { readonly kind: "none" } | { readonly kind: "apikey"; readonly placements: readonly Placement[] } | { readonly kind: "oauth"; readonly authorizationUrl: string; readonly tokenUrl: string; readonly resource?: string | null; readonly scopes: readonly string[]; readonly supportsClientIdMetadataDocument?: boolean; }; export interface AuthTemplateEditorPreset { /** Pill text, e.g. "Bearer token", "OAuth2". */ readonly label: string; /** Clicking the pill applies this value (an editable default). */ readonly value: AuthTemplateEditorValue; } const DEFAULT_ALLOWED: readonly AuthTemplateEditorKind[] = ["none", "apikey", "oauth"]; const TAB_LABELS: Readonly> = { none: "None", apikey: "API key", oauth: "OAuth", }; /** A fresh, empty header placement for the apiKey tab's first row. */ export const emptyApiKeyValue = (): AuthTemplateEditorValue => ({ kind: "apikey", placements: [{ carrier: "header", name: "Authorization", prefix: "" }], }); export const emptyOAuthValue = (): AuthTemplateEditorValue => ({ kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [], }); /** The empty default value for a given tab kind. Switching tabs replaces the * value with this so the editor below always has the right shape to edit. */ export const emptyValueForKind = (kind: AuthTemplateEditorKind): AuthTemplateEditorValue => { if (kind === "none") return { kind: "none" }; if (kind === "apikey") return emptyApiKeyValue(); return emptyOAuthValue(); }; /** Parse a comma-separated scope string into a trimmed, non-empty list. */ export const parseScopes = (raw: string): readonly string[] => raw .split(",") .map((scope: string) => scope.trim()) .filter((scope: string) => scope.length > 0); export interface AuthTemplateEditorProps { readonly value: AuthTemplateEditorValue; readonly onChange: (value: AuthTemplateEditorValue) => void; /** Restrict which tabs render. Default `["none","apikey","oauth"]`. MCP * OAuth-only servers pass `["oauth"]`; GraphQL passes `["none","apikey"]`. */ readonly allowedKinds?: readonly AuthTemplateEditorKind[]; /** Spec-detected presets (OpenAPI) → editable-default pills on the apiKey/OAuth * tabs. Clicking a pill applies its value as an editable default. */ readonly presets?: readonly AuthTemplateEditorPreset[]; /** Some integrations, notably MCP, discover OAuth metadata from the server at * connection time. In that case the add form should declare OAuth without * editable provider URLs or scopes. */ readonly oauthMetadata?: "editable" | "discovered"; } export function AuthTemplateEditor(props: AuthTemplateEditorProps) { const { value, onChange, allowedKinds = DEFAULT_ALLOWED, presets, oauthMetadata = "editable", } = props; const tabs = allowedKinds.map((kind: AuthTemplateEditorKind) => ({ value: kind, label: TAB_LABELS[kind], })); // Switching tab replaces the value with an empty default of that kind (so the // editor below has the right shape to edit). Re-selecting the current tab is a // no-op (it would otherwise clobber the user's in-progress edits). const handleTab = (next: AuthTemplateEditorKind): void => { if (next === value.kind) return; onChange(emptyValueForKind(next)); }; // Presets that match the active tab's kind — rendered as editable-default pills. const tabPresets = (presets ?? []).filter( (preset: AuthTemplateEditorPreset) => preset.value.kind === value.kind, ); return (
{tabs.length > 1 && ( tabs={tabs} value={value.kind} onChange={handleTab} /> )} {value.kind === "none" && (

No credential — this integration is open and tools are callable without an account.

)} {value.kind === "apikey" && (
{tabPresets.length > 0 && (
{tabPresets.map((preset: AuthTemplateEditorPreset, index: number) => ( ))}
)} onChange({ kind: "apikey", placements })} />
)} {value.kind === "oauth" && (
{tabPresets.length > 0 && (
{tabPresets.map((preset: AuthTemplateEditorPreset, index: number) => ( ))}
)} {oauthMetadata === "discovered" ? (

OAuth metadata is discovered from this server when you connect an account.

) : ( <>
) => onChange({ ...value, authorizationUrl: e.target.value }) } className="font-mono text-sm" />
) => onChange({ ...value, tokenUrl: e.target.value }) } className="font-mono text-sm" />
) => onChange({ ...value, scopes: parseScopes(e.target.value) }) } className="font-mono text-sm" />
)}
)}
); }