// --------------------------------------------------------------------------- // Auth-method LIST editor — the add-flow's "How does this API authenticate?" // section, as a list. Every plugin registers EVERY declared method (P6: add // without auth, connect later), so the add flow edits a list of generic // `AuthTemplateEditorValue` rows seeded from detection (spec analysis, endpoint // probe, …) with add/remove and a per-row `AuthTemplateEditor`. // // Composition: `useAuthMethodList` is the headless row state (seeding, // edit/add/remove); `AuthMethodListEditor` is the presentation. Plugins own // only the codec at the edges — seeds in (detection → editor values) and // submit out (editor values → wire templates). // --------------------------------------------------------------------------- import { useCallback, useEffect, useRef, useState } from "react"; import { LockIcon, PlusIcon, XIcon } from "lucide-react"; import { PlacementLine } from "../lib/auth-placements"; import { Button } from "./button"; import { FieldLabel } from "./field"; import { AuthTemplateEditor, emptyApiKeyValue, type AuthTemplateEditorKind, type AuthTemplateEditorPreset, type AuthTemplateEditorValue, } from "./auth-template-editor"; export interface AuthMethodSeed { readonly value: AuthTemplateEditorValue; /** The detected method's stable slug — an unedited seeded row submits with * its EXACT original slug (preserving connections bound against it). */ readonly slug?: string; /** Detection label (e.g. the spec's security-scheme name) shown on the row. */ readonly label?: string; } export interface AuthMethodRow { readonly value: AuthTemplateEditorValue; /** True when this row came from detection (a seed), false when the user added * it. Detected rows are immutable — the spec/probe declared them — so the * editor renders them read-only. Not inferred from `seedSlug`: some plugins * (MCP) seed a detected method with a label but no slug. */ readonly seeded: boolean; readonly seedSlug?: string; readonly seedLabel?: string; } export interface AuthMethodListState { readonly rows: readonly AuthMethodRow[]; readonly setRowAt: (index: number, next: AuthTemplateEditorValue) => void; readonly removeRowAt: (index: number) => void; readonly addRow: () => void; } /** Headless row state for the method list. Re-seeds whenever `seeds` changes * identity (detection results are stable per analysis), discarding edits — * fresh detection means a fresh starting set. */ export function useAuthMethodList(seeds: readonly AuthMethodSeed[]): AuthMethodListState { const [rows, setRows] = useState([]); const seededFromRef = useRef(null); useEffect(() => { if (seededFromRef.current === seeds) return; seededFromRef.current = seeds; setRows( seeds.map( (seed: AuthMethodSeed): AuthMethodRow => ({ value: seed.value, seeded: true, ...(seed.slug !== undefined ? { seedSlug: seed.slug } : {}), ...(seed.label !== undefined ? { seedLabel: seed.label } : {}), }), ), ); }, [seeds]); const setRowAt = useCallback((index: number, next: AuthTemplateEditorValue) => { setRows((current: readonly AuthMethodRow[]) => current.map((row: AuthMethodRow, i: number) => (i === index ? { ...row, value: next } : row)), ); }, []); const removeRowAt = useCallback((index: number) => { setRows((current: readonly AuthMethodRow[]) => current.filter((_row: AuthMethodRow, i: number) => i !== index), ); }, []); const addRow = useCallback(() => { setRows((current: readonly AuthMethodRow[]) => [ ...current, { value: emptyApiKeyValue(), seeded: false }, ]); }, []); return { rows, setRowAt, removeRowAt, addRow }; } export interface AuthMethodListEditorProps { readonly list: AuthMethodListState; readonly title?: string; /** Shown when the list is empty (e.g. "No authentication detected. …"). */ readonly emptyHint?: string; /** Shown under the list when at least one row exists. */ readonly footerHint?: string; /** Per-row editor restrictions — see `AuthTemplateEditorProps`. */ readonly allowedKinds?: readonly AuthTemplateEditorKind[]; readonly presets?: readonly AuthTemplateEditorPreset[]; readonly oauthMetadata?: "editable" | "discovered"; } export function AuthMethodListEditor(props: AuthMethodListEditorProps) { const { list, allowedKinds, presets, oauthMetadata } = props; return (
{props.title ?? "How does this API authenticate?"}
{list.rows.length === 0 ? ( props.emptyHint ? (

{props.emptyHint}

) : null ) : (
{list.rows.map((row: AuthMethodRow, index: number) => { // A row seeded from detection is the spec's own auth declaration: // it's IMMUTABLE here. We render it read-only (no kind selector, no // editable fields) so a user can't silently retype the spec's // method into something nothing backs (e.g. flipping a Bearer-token // API to OAuth with empty endpoints). The escape hatch is to remove // the row and add a custom one. Manually added rows (no seed) get // the full editor. const detected = row.seeded; return (
{detected ? : null} {/* Named by what it is. "Method 1" told the reader nothing except that there might be a Method 2, and next to a masked value it read as a locked credential field — people tried to paste their key into it. */} {detected ? detectedMethodTitle(row) : `Method ${index + 1}`}
{detected ? ( ) : ( list.setRowAt(index, next)} {...(allowedKinds ? { allowedKinds } : {})} {...(presets ? { presets } : {})} {...(oauthMetadata ? { oauthMetadata } : {})} /> )}
); })}
)} {list.rows.length > 0 && props.footerHint ? (

{props.footerHint}

) : null}
); } /** What a spec-detected method should be called: the credential it wants, plus * the spec's own name for it when there is more than one to tell apart. */ function detectedMethodTitle(row: AuthMethodRow): string { const kind = row.value.kind; const base = kind === "oauth" ? "OAuth" : kind === "apikey" ? "API key" : "No authentication"; return row.seedLabel ? `${base} · ${row.seedLabel}` : base; } /** One read-only `label value` line, mono value, for the detected summary. */ function SpecField(props: { readonly label: string; readonly value: string }) { return (
{props.label} {props.value}
); } /** Read-only view of a spec-detected method: shows what the spec declared * (placements / OAuth endpoints) as a DISABLED, non-interactive block. The * detected method is immutable here, so the summary is styled like a disabled * field (muted, not-allowed cursor, text not selectable) to communicate that * plainly. The only action is to remove the row (the header's X) and add a * custom method to override. */ function DetectedMethodSummary(props: { readonly value: AuthTemplateEditorValue; readonly oauthMetadata?: "editable" | "discovered"; }) { const { value, oauthMetadata } = props; // Name the auth kind explicitly: a detection label like MCP's "Detected" // doesn't say whether it's OAuth or an API key, so surface it here. return (
{value.kind === "none" && (

No credential — tools are callable without an account.

)} {value.kind === "apikey" && (value.placements.length > 0 ? (

This API takes a key, sent as:

{value.placements.map((placement, i: number) => ( ))}
) : null)} {value.kind === "oauth" && (oauthMetadata === "discovered" ? (

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

) : (
{value.authorizationUrl ? ( ) : null} {value.tokenUrl ? : null} {value.scopes.length > 0 ? ( ) : null}
))}
{/* The question this screen used to leave open. Nothing on the add form takes a credential; the key is entered once the integration exists, from its own page. Saying so is the difference between a confident next click and hunting for a field that is not here. */}

{/* "Declared by this API", not "read from the spec": GraphQL surfaces have no spec — their methods arrive from the registry record — and the claim that matters is the same either way. */} {value.kind === "oauth" ? "Declared by this API. You'll sign in after adding this integration." : "Declared by this API. You'll enter your key after adding this integration."}

); }