import { useEffect, useMemo, useState } from "react";import { useAtomSet, useAtomValue } from "@effect/atom-react";import * as Exit from "effect/Exit";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import { ConnectionName, IntegrationSlug, OAuthClientSlug, ProviderItemId, ProviderKey, type OAuthClientSummary, type Owner,} from "@executor-js/sdk/shared";import type { IntegrationAccountHandoff } from "@executor-js/sdk/client";import { toast } from "sonner"; import { addConnectionOptimistic, connectionsAllAtom, oauthClientsOptimisticAtom, probeOAuth, providerItemsAtom, providersAtom, registerDynamicOAuthClient, removeOAuthClientOptimistic, startOAuth,} from "../api/atoms";import { connectionWriteKeys, oauthClientWriteKeys } from "../api/reactivity-keys";import { messageFromExit } from "../api/error-reporting";import { trackEvent } from "../api/analytics";import { useOrganizationId } from "../api/organization-context";import { ownerLabel, ownerLabelForHost, useOwnerDisplay } from "../api/owner-display";import { ConnectionOwnerDropdown, connectionOwnerOptionsForHost, defaultConnectionOwnerForHost, normalizeConnectionOwner, resolveOAuthConnectionOwnerForHost, type ConnectionOwnerOption,} from "../plugins/connection-owner";import { oauthCallbackUrl, useOAuthPopupFlow } from "../plugins/oauth-sign-in";import { clientDisplayName, clientHost, uniqueClientSlug, useOAuthClientsForIntegration, type OAuthClientOption,} from "../plugins/use-effective-oauth-client";import { cn } from "../lib/utils";import { buildUsageMap, connectionsUsingClient } from "../lib/oauth-client-usage";import { OAuthClientForm } from "./oauth-client-form";import { RemoveOAuthAppDialog } from "./remove-oauth-app-dialog";import { AddCustomMethodForm, type CreateCustomMethod } from "./add-custom-method-modal";import { PlacementLine, type AuthMethod } from "../lib/auth-placements";import { connectionIdentifier } from "../lib/connection-name";import { Badge } from "./badge";import { Button } from "./button";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "./dropdown-menu";import { PlusIcon, XIcon } from "lucide-react";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,} from "./dialog";import { Input } from "./input";import { Label } from "./label";import { RadioGroup, RadioGroupItem } from "./radio-group";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"; // ---------------------------------------------------------------------------// Add-account modal — the connection-create form.//// Field order: (1) display name · (2) authentication method · (3) credential// · (4) saved-to owner. A connection is immutable once created. Step 2 collects// one value per distinct input the method declares — usually one, but a// multi-input method (e.g. Datadog's two keys) shows one field per variable.//// OAuth: step 2 lists the registered apps usable for this integration and lets// you PICK one (or "Register a new app"). While registering, name/saved-to are// hidden (they don't apply yet). Once an app is selected, the footer's "Connect// with OAuth" mints the connection with the name + saved-to. The CLIENT owner// (whose app) is distinct from the CONNECTION's saved-to owner.// --------------------------------------------------------------------------- const ONEPASSWORD_PROVIDER = ProviderKey.make("onepassword"); type CredentialOrigin = "paste" | "onepassword";type CredentialInput = { readonly variable: string; readonly label: string }; type CredentialPayloadOrigin = | { readonly values: Record<string, string> } | { readonly from: { readonly provider: ProviderKey; readonly id: ProviderItemId; }; }; export function createCredentialPayloadOrigin(args: { readonly origin: CredentialOrigin; readonly inputs: readonly CredentialInput[]; readonly values: Readonly<Record<string, string>>; readonly onePasswordItemId: string; readonly singleInput: boolean;}): CredentialPayloadOrigin | null { if (args.inputs.length === 0) return { values: { token: "" } }; if (args.origin === "onepassword") { const id = args.onePasswordItemId.trim(); if (!args.singleInput || id.length === 0) return null; return { from: { provider: ONEPASSWORD_PROVIDER, id: ProviderItemId.make(id) }, }; } const values = Object.fromEntries( args.inputs.map((input) => [input.variable, (args.values[input.variable] ?? "").trim()]), ); return Object.values(values).every((value) => value.length > 0) ? { values } : null;} const numberBadge = (n: number) => ( <span className="inline-grid size-[18px] shrink-0 place-items-center rounded-full border border-border bg-muted text-[11px] text-muted-foreground"> {n} </span>); function isOnePasswordRegistered( providers: AsyncResult.AsyncResult<readonly ProviderKey[], unknown>,) { return AsyncResult.match(providers, { onInitial: () => false, onFailure: () => false, onSuccess: ({ value }) => value.some((provider: ProviderKey) => String(provider) === String(ONEPASSWORD_PROVIDER)), });} function PasteCredentialInputs(props: { readonly inputs: readonly CredentialInput[]; readonly singleInput: boolean; readonly values: Readonly<Record<string, string>>; readonly onChange: (values: Record<string, string>) => void;}) { return ( <div className="space-y-2"> {props.inputs.map((input) => ( <div key={input.variable} className="space-y-1"> {!props.singleInput && ( <Label className="text-xs text-muted-foreground">{input.label}</Label> )} <Input type="password" autoComplete="new-password" placeholder={props.singleInput ? "paste the value / token" : `paste ${input.label}`} value={props.values[input.variable] ?? ""} onChange={(e: React.ChangeEvent<HTMLInputElement>) => props.onChange({ ...props.values, [input.variable]: e.target.value, }) } className="font-mono" data-ph-block /> </div> ))} </div> );} function OnePasswordItemSelect(props: { readonly value: string; readonly onChange: (value: string) => void;}) { const itemsResult = useAtomValue(providerItemsAtom(ONEPASSWORD_PROVIDER)); const state = AsyncResult.matchWithError( itemsResult as AsyncResult.AsyncResult< readonly { readonly id: ProviderItemId; readonly name: string }[], Error >, { onInitial: () => ({ items: [] as readonly { readonly id: ProviderItemId; readonly name: string; }[], loading: true, error: null as string | null, }), onError: () => ({ items: [] as readonly { readonly id: ProviderItemId; readonly name: string; }[], loading: false, error: "Failed to load 1Password items", }), onDefect: () => ({ items: [] as readonly { readonly id: ProviderItemId; readonly name: string; }[], loading: false, error: "Failed to load 1Password items", }), onSuccess: ({ value }) => ({ items: value, loading: false, error: null }), }, ); if (state.loading) { return <p className="text-xs text-muted-foreground">Loading 1Password items…</p>; } if (state.error) { return <p className="text-xs text-destructive">{state.error}</p>; } if (state.items.length === 0) { return <p className="text-xs text-muted-foreground">No 1Password items found.</p>; } return ( <div className="space-y-1" data-ph-block> <Select value={props.value} onValueChange={props.onChange}> <SelectTrigger> <SelectValue placeholder="Select secret" /> </SelectTrigger> <SelectContent> {state.items.map((item) => ( <SelectItem key={String(item.id)} value={String(item.id)}> {item.name} </SelectItem> ))} </SelectContent> </Select> </div> );} function CredentialValueFields(props: { readonly inputs: readonly CredentialInput[]; readonly singleInput: boolean; readonly values: Readonly<Record<string, string>>; readonly onValuesChange: (values: Record<string, string>) => void; readonly origin: CredentialOrigin; readonly onOriginChange: (origin: CredentialOrigin) => void; readonly onePasswordItemId: string; readonly onOnePasswordItemIdChange: (value: string) => void;}) { const providers = useAtomValue(providersAtom); const onePasswordAvailable = props.singleInput && isOnePasswordRegistered(providers); return ( <div className="space-y-3"> {onePasswordAvailable ? ( <RadioGroup value={props.origin} onValueChange={(value) => props.onOriginChange(value as CredentialOrigin)} className="grid grid-cols-2 gap-2" > <Label htmlFor="credential-origin-paste" className="flex cursor-pointer items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm font-normal has-[:checked]:border-ring has-[:checked]:bg-accent/40" > <RadioGroupItem id="credential-origin-paste" value="paste" /> Paste value </Label> <Label htmlFor="credential-origin-onepassword" className="flex cursor-pointer items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm font-normal has-[:checked]:border-ring has-[:checked]:bg-accent/40" > <RadioGroupItem id="credential-origin-onepassword" value="onepassword" /> 1Password </Label> </RadioGroup> ) : null} {onePasswordAvailable && props.origin === "onepassword" ? ( <OnePasswordItemSelect value={props.onePasswordItemId} onChange={props.onOnePasswordItemIdChange} /> ) : ( <PasteCredentialInputs inputs={props.inputs} singleInput={props.singleInput} values={props.values} onChange={props.onValuesChange} /> )} </div> );} // ---------------------------------------------------------------------------// Step header — the label introducing each section of the form. Four style// variants are kept for design review; flip STEP_HEADER_VARIANT to preview each// one in isolation. The numbered-circle treatment implied a sequential wizard// the form isn't (every section shows at once), so the alternatives drop the// numbers.// - "numbered": numbered circle + label + inline hint (current).// - "eyebrow": uppercase micro-caps, hint on its own line. Matches the app's// existing section headers (e.g. AccountsSection).// - "sentence": plain form label in foreground weight, hint inline.// - "accent": a short leading rule for rhythm without implied sequence.// ---------------------------------------------------------------------------type StepHeaderVariant = "numbered" | "eyebrow" | "sentence" | "accent"; /** Swap this to preview each step-header style. */const STEP_HEADER_VARIANT: StepHeaderVariant = "eyebrow"; function StepHeader(props: { readonly index: number; readonly label: string; readonly hint?: string; readonly htmlFor?: string;}) { const { index, label, hint, htmlFor } = props; const variants: Record<StepHeaderVariant, React.ReactElement> = { numbered: ( <Label htmlFor={htmlFor} className="text-xs text-muted-foreground"> {numberBadge(index)} {label} {hint ? <span className="font-normal text-muted-foreground/70">{hint}</span> : null} </Label> ), eyebrow: ( <div className="flex flex-col gap-1"> <Label htmlFor={htmlFor} className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground" > {label} </Label> {hint ? <span className="text-xs text-muted-foreground/70">{hint}</span> : null} </div> ), sentence: ( <Label htmlFor={htmlFor} className="text-sm font-medium text-foreground"> {label} {hint ? <span className="text-xs font-normal text-muted-foreground">{hint}</span> : null} </Label> ), accent: ( <Label htmlFor={htmlFor} className="gap-2.5 text-xs text-muted-foreground"> <span className="h-3.5 w-0.5 shrink-0 rounded-full bg-primary/70" aria-hidden /> <span className="text-sm font-medium text-foreground">{label}</span> {hint ? <span className="font-normal text-muted-foreground/70">{hint}</span> : null} </Label> ), }; return variants[STEP_HEADER_VARIANT];} /** Derive the connection's display label from the user's free-text name (or a * default of "<owner> <integration>"). With an empty `label` this yields the * derived name shown as the name input's placeholder, so the optional-but- * prefilled intent is visible. */export const connectionLabel = (label: string, owner: Owner, integrationName: string): string => label.trim() || `${ownerLabel(owner)} ${integrationName}`; export const connectionLabelForHost = ( label: string, owner: Owner, integrationName: string, organizationId: string | null,): string => label.trim() || `${ownerLabelForHost(owner, organizationId)} ${integrationName}`; /** The default owner a new connection is saved under when the user makes no * explicit choice. Personal: a connection is most often a personal credential. */export const DEFAULT_CONNECTION_OWNER: Owner = "user"; const authMethodKey = (method: AuthMethod): string => method.source === "custom" ? `custom:${String(method.template)}` : `declared:${method.id}`; /** The selectable methods: the declared catalog methods plus any custom method * created in this session, deduped by stable method identity (custom appended * last). A just-created method shows + can be selected before the catalog * refresh lands. */export const mergeCustomMethods = ( declared: readonly AuthMethod[], created: readonly AuthMethod[],): readonly AuthMethod[] => { const keys = new Set(declared.map(authMethodKey)); return [...declared, ...created.filter((method: AuthMethod) => !keys.has(authMethodKey(method)))];}; /** Derive a stable JS-identifier-safe callable connection name from the label. */export const connectionNameFrom = ( label: string, owner: Owner, integrationName: string, organizationId: string | null,): ConnectionName => connectionIdentifier(connectionLabelForHost(label, owner, integrationName, organizationId)); // ---------------------------------------------------------------------------// Transparent DCR (RFC 7591) connect orchestration.//// For DCR-capable methods (MCP OAuth) the user clicks one "Connect" button and// we do everything: probe the integration's discovery URL → register a client// against the advertised registration endpoint → start// the OAuth flow with the minted client. No app picker, no pasted client id.//// This is extracted as a pure-ish orchestrator (injected `probe`/`register`/// `start`) so the SEQUENCE is unit-testable without React/atoms. The caller// supplies thin adapters over the `probeOAuth` / `registerDynamicOAuthClient` /// popup-start atoms.// --------------------------------------------------------------------------- /** Discovery result from the probe step (subset of the `probeOAuth` response). */type DcrProbeResult = { readonly authorizationUrl: string; readonly tokenUrl: string; readonly resource?: string | null; readonly scopesSupported?: readonly string[]; readonly registrationEndpoint?: string | null; readonly tokenEndpointAuthMethodsSupported?: readonly string[];}; type DcrRegisterArgs = { readonly owner: Owner; readonly slug: OAuthClientSlug; readonly registrationEndpoint: string; readonly authorizationUrl: string; readonly tokenUrl: string; readonly resource?: string | null; readonly scopes: readonly string[]; readonly tokenEndpointAuthMethodsSupported?: readonly string[]; readonly clientName: string; readonly redirectUri?: string; readonly originIntegration?: IntegrationSlug;}; type DcrStartArgs = { readonly client: OAuthClientSlug; readonly owner: Owner;}; /** Outcome of the DCR orchestration. `"started"` means the OAuth flow handed * off (the popup/inline start ran); `"fallback"` means we could not auto-set-up * (probe failed, or no registration endpoint) and the caller should fall back * to the bring-your-own-app picker. */type DcrOutcome = | { readonly kind: "started" } | { readonly kind: "fallback"; readonly reason: "probe-failed" | "no-registration-endpoint"; }; type RunDcrConnectDeps = { /** Probe the discovery URL → resolved endpoints + (maybe) a registration * endpoint. Resolves to null when the probe fails. */ readonly probe: (url: string) => Promise<DcrProbeResult | null>; /** Register a DCR client → the minted client slug, or null on failure. */ readonly register: (args: DcrRegisterArgs) => Promise<OAuthClientSlug | null>; /** Start the OAuth flow with the minted client (popup / inline). */ readonly start: (args: DcrStartArgs) => void;}; type RunDcrConnectInput = { readonly discoveryUrl: string; readonly owner: Owner; readonly integrationName: string; /** The owner's existing client slugs, so the minted slug stays unique. */ readonly existingSlugs: readonly string[]; /** Scopes declared by the integration's method (override the probed ones). */ readonly declaredScopes?: readonly string[]; /** Browser-facing callback URL registered with DCR when available. */ readonly redirectUri?: string; /** Integration that requested this DCR client. */ readonly integration: IntegrationSlug;}; /** * Run the transparent DCR connect sequence: probe → register → start. * * - Probe failure → `{ kind: "fallback", reason: "probe-failed" }` (caller shows BYO). * - No registration endpoint → `{ kind: "fallback", reason: "no-registration-endpoint" }`. * - Register failure → throws via the injected `register` rejecting; the caller * treats a thrown/rejected register as fallback (kept out of the happy path). * - Success → registers, calls `start`, returns `{ kind: "started" }`. */export async function runDcrConnect( deps: RunDcrConnectDeps, input: RunDcrConnectInput,): Promise<DcrOutcome> { const probe = await deps.probe(input.discoveryUrl); if (probe === null) return { kind: "fallback", reason: "probe-failed" }; const registrationEndpoint = probe.registrationEndpoint; if (!registrationEndpoint) return { kind: "fallback", reason: "no-registration-endpoint" };Lines 1–500 of 1530. Load more
import { useEffect, useMemo, useState } from "react"; import { useAtomSet, useAtomValue } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { ConnectionName, IntegrationSlug, OAuthClientSlug, ProviderItemId, ProviderKey, type OAuthClientSummary, type Owner, } from "@executor-js/sdk/shared"; import type { IntegrationAccountHandoff } from "@executor-js/sdk/client"; import { toast } from "sonner"; import { addConnectionOptimistic, connectionsAllAtom, oauthClientsOptimisticAtom, probeOAuth, providerItemsAtom, providersAtom, registerDynamicOAuthClient, removeOAuthClientOptimistic, startOAuth, } from "../api/atoms"; import { connectionWriteKeys, oauthClientWriteKeys } from "../api/reactivity-keys"; import { messageFromExit } from "../api/error-reporting"; import { trackEvent } from "../api/analytics"; import { useOrganizationId } from "../api/organization-context"; import { ownerLabel, ownerLabelForHost, useOwnerDisplay } from "../api/owner-display"; import { ConnectionOwnerDropdown, connectionOwnerOptionsForHost, defaultConnectionOwnerForHost, normalizeConnectionOwner, resolveOAuthConnectionOwnerForHost, type ConnectionOwnerOption, } from "../plugins/connection-owner"; import { oauthCallbackUrl, useOAuthPopupFlow } from "../plugins/oauth-sign-in"; import { clientDisplayName, clientHost, uniqueClientSlug, useOAuthClientsForIntegration, type OAuthClientOption, } from "../plugins/use-effective-oauth-client"; import { cn } from "../lib/utils"; import { buildUsageMap, connectionsUsingClient } from "../lib/oauth-client-usage"; import { OAuthClientForm } from "./oauth-client-form"; import { RemoveOAuthAppDialog } from "./remove-oauth-app-dialog"; import { AddCustomMethodForm, type CreateCustomMethod } from "./add-custom-method-modal"; import { PlacementLine, type AuthMethod } from "../lib/auth-placements"; import { connectionIdentifier } from "../lib/connection-name"; import { Badge } from "./badge"; import { Button } from "./button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./dropdown-menu"; import { PlusIcon, XIcon } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./dialog"; import { Input } from "./input"; import { Label } from "./label"; import { RadioGroup, RadioGroupItem } from "./radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"; // --------------------------------------------------------------------------- // Add-account modal — the connection-create form. // // Field order: (1) display name · (2) authentication method · (3) credential // · (4) saved-to owner. A connection is immutable once created. Step 2 collects // one value per distinct input the method declares — usually one, but a // multi-input method (e.g. Datadog's two keys) shows one field per variable. // // OAuth: step 2 lists the registered apps usable for this integration and lets // you PICK one (or "Register a new app"). While registering, name/saved-to are // hidden (they don't apply yet). Once an app is selected, the footer's "Connect // with OAuth" mints the connection with the name + saved-to. The CLIENT owner // (whose app) is distinct from the CONNECTION's saved-to owner. // --------------------------------------------------------------------------- const ONEPASSWORD_PROVIDER = ProviderKey.make("onepassword"); type CredentialOrigin = "paste" | "onepassword"; type CredentialInput = { readonly variable: string; readonly label: string }; type CredentialPayloadOrigin = | { readonly values: Record<string, string> } | { readonly from: { readonly provider: ProviderKey; readonly id: ProviderItemId; }; }; export function createCredentialPayloadOrigin(args: { readonly origin: CredentialOrigin; readonly inputs: readonly CredentialInput[]; readonly values: Readonly<Record<string, string>>; readonly onePasswordItemId: string; readonly singleInput: boolean; }): CredentialPayloadOrigin | null { if (args.inputs.length === 0) return { values: { token: "" } }; if (args.origin === "onepassword") { const id = args.onePasswordItemId.trim(); if (!args.singleInput || id.length === 0) return null; return { from: { provider: ONEPASSWORD_PROVIDER, id: ProviderItemId.make(id) }, }; } const values = Object.fromEntries( args.inputs.map((input) => [input.variable, (args.values[input.variable] ?? "").trim()]), ); return Object.values(values).every((value) => value.length > 0) ? { values } : null; } const numberBadge = (n: number) => ( <span className="inline-grid size-[18px] shrink-0 place-items-center rounded-full border border-border bg-muted text-[11px] text-muted-foreground"> {n} </span> ); function isOnePasswordRegistered( providers: AsyncResult.AsyncResult<readonly ProviderKey[], unknown>, ) { return AsyncResult.match(providers, { onInitial: () => false, onFailure: () => false, onSuccess: ({ value }) => value.some((provider: ProviderKey) => String(provider) === String(ONEPASSWORD_PROVIDER)), }); } function PasteCredentialInputs(props: { readonly inputs: readonly CredentialInput[]; readonly singleInput: boolean; readonly values: Readonly<Record<string, string>>; readonly onChange: (values: Record<string, string>) => void; }) { return ( <div className="space-y-2"> {props.inputs.map((input) => ( <div key={input.variable} className="space-y-1"> {!props.singleInput && ( <Label className="text-xs text-muted-foreground">{input.label}</Label> )} <Input type="password" autoComplete="new-password" placeholder={props.singleInput ? "paste the value / token" : `paste ${input.label}`} value={props.values[input.variable] ?? ""} onChange={(e: React.ChangeEvent<HTMLInputElement>) => props.onChange({ ...props.values, [input.variable]: e.target.value, }) } className="font-mono" data-ph-block /> </div> ))} </div> ); } function OnePasswordItemSelect(props: { readonly value: string; readonly onChange: (value: string) => void; }) { const itemsResult = useAtomValue(providerItemsAtom(ONEPASSWORD_PROVIDER)); const state = AsyncResult.matchWithError( itemsResult as AsyncResult.AsyncResult< readonly { readonly id: ProviderItemId; readonly name: string }[], Error >, { onInitial: () => ({ items: [] as readonly { readonly id: ProviderItemId; readonly name: string; }[], loading: true, error: null as string | null, }), onError: () => ({ items: [] as readonly { readonly id: ProviderItemId; readonly name: string; }[], loading: false, error: "Failed to load 1Password items", }), onDefect: () => ({ items: [] as readonly { readonly id: ProviderItemId; readonly name: string; }[], loading: false, error: "Failed to load 1Password items", }), onSuccess: ({ value }) => ({ items: value, loading: false, error: null }), }, ); if (state.loading) { return <p className="text-xs text-muted-foreground">Loading 1Password items…</p>; } if (state.error) { return <p className="text-xs text-destructive">{state.error}</p>; } if (state.items.length === 0) { return <p className="text-xs text-muted-foreground">No 1Password items found.</p>; } return ( <div className="space-y-1" data-ph-block> <Select value={props.value} onValueChange={props.onChange}> <SelectTrigger> <SelectValue placeholder="Select secret" /> </SelectTrigger> <SelectContent> {state.items.map((item) => ( <SelectItem key={String(item.id)} value={String(item.id)}> {item.name} </SelectItem> ))} </SelectContent> </Select> </div> ); } function CredentialValueFields(props: { readonly inputs: readonly CredentialInput[]; readonly singleInput: boolean; readonly values: Readonly<Record<string, string>>; readonly onValuesChange: (values: Record<string, string>) => void; readonly origin: CredentialOrigin; readonly onOriginChange: (origin: CredentialOrigin) => void; readonly onePasswordItemId: string; readonly onOnePasswordItemIdChange: (value: string) => void; }) { const providers = useAtomValue(providersAtom); const onePasswordAvailable = props.singleInput && isOnePasswordRegistered(providers); return ( <div className="space-y-3"> {onePasswordAvailable ? ( <RadioGroup value={props.origin} onValueChange={(value) => props.onOriginChange(value as CredentialOrigin)} className="grid grid-cols-2 gap-2" > <Label htmlFor="credential-origin-paste" className="flex cursor-pointer items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm font-normal has-[:checked]:border-ring has-[:checked]:bg-accent/40" > <RadioGroupItem id="credential-origin-paste" value="paste" /> Paste value </Label> <Label htmlFor="credential-origin-onepassword" className="flex cursor-pointer items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-sm font-normal has-[:checked]:border-ring has-[:checked]:bg-accent/40" > <RadioGroupItem id="credential-origin-onepassword" value="onepassword" /> 1Password </Label> </RadioGroup> ) : null} {onePasswordAvailable && props.origin === "onepassword" ? ( <OnePasswordItemSelect value={props.onePasswordItemId} onChange={props.onOnePasswordItemIdChange} /> ) : ( <PasteCredentialInputs inputs={props.inputs} singleInput={props.singleInput} values={props.values} onChange={props.onValuesChange} /> )} </div> ); } // --------------------------------------------------------------------------- // Step header — the label introducing each section of the form. Four style // variants are kept for design review; flip STEP_HEADER_VARIANT to preview each // one in isolation. The numbered-circle treatment implied a sequential wizard // the form isn't (every section shows at once), so the alternatives drop the // numbers. // - "numbered": numbered circle + label + inline hint (current). // - "eyebrow": uppercase micro-caps, hint on its own line. Matches the app's // existing section headers (e.g. AccountsSection). // - "sentence": plain form label in foreground weight, hint inline. // - "accent": a short leading rule for rhythm without implied sequence. // --------------------------------------------------------------------------- type StepHeaderVariant = "numbered" | "eyebrow" | "sentence" | "accent"; /** Swap this to preview each step-header style. */ const STEP_HEADER_VARIANT: StepHeaderVariant = "eyebrow"; function StepHeader(props: { readonly index: number; readonly label: string; readonly hint?: string; readonly htmlFor?: string; }) { const { index, label, hint, htmlFor } = props; const variants: Record<StepHeaderVariant, React.ReactElement> = { numbered: ( <Label htmlFor={htmlFor} className="text-xs text-muted-foreground"> {numberBadge(index)} {label} {hint ? <span className="font-normal text-muted-foreground/70">{hint}</span> : null} </Label> ), eyebrow: ( <div className="flex flex-col gap-1"> <Label htmlFor={htmlFor} className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground" > {label} </Label> {hint ? <span className="text-xs text-muted-foreground/70">{hint}</span> : null} </div> ), sentence: ( <Label htmlFor={htmlFor} className="text-sm font-medium text-foreground"> {label} {hint ? <span className="text-xs font-normal text-muted-foreground">{hint}</span> : null} </Label> ), accent: ( <Label htmlFor={htmlFor} className="gap-2.5 text-xs text-muted-foreground"> <span className="h-3.5 w-0.5 shrink-0 rounded-full bg-primary/70" aria-hidden /> <span className="text-sm font-medium text-foreground">{label}</span> {hint ? <span className="font-normal text-muted-foreground/70">{hint}</span> : null} </Label> ), }; return variants[STEP_HEADER_VARIANT]; } /** Derive the connection's display label from the user's free-text name (or a * default of "<owner> <integration>"). With an empty `label` this yields the * derived name shown as the name input's placeholder, so the optional-but- * prefilled intent is visible. */ export const connectionLabel = (label: string, owner: Owner, integrationName: string): string => label.trim() || `${ownerLabel(owner)} ${integrationName}`; export const connectionLabelForHost = ( label: string, owner: Owner, integrationName: string, organizationId: string | null, ): string => label.trim() || `${ownerLabelForHost(owner, organizationId)} ${integrationName}`; /** The default owner a new connection is saved under when the user makes no * explicit choice. Personal: a connection is most often a personal credential. */ export const DEFAULT_CONNECTION_OWNER: Owner = "user"; const authMethodKey = (method: AuthMethod): string => method.source === "custom" ? `custom:${String(method.template)}` : `declared:${method.id}`; /** The selectable methods: the declared catalog methods plus any custom method * created in this session, deduped by stable method identity (custom appended * last). A just-created method shows + can be selected before the catalog * refresh lands. */ export const mergeCustomMethods = ( declared: readonly AuthMethod[], created: readonly AuthMethod[], ): readonly AuthMethod[] => { const keys = new Set(declared.map(authMethodKey)); return [...declared, ...created.filter((method: AuthMethod) => !keys.has(authMethodKey(method)))]; }; /** Derive a stable JS-identifier-safe callable connection name from the label. */ export const connectionNameFrom = ( label: string, owner: Owner, integrationName: string, organizationId: string | null, ): ConnectionName => connectionIdentifier(connectionLabelForHost(label, owner, integrationName, organizationId)); // --------------------------------------------------------------------------- // Transparent DCR (RFC 7591) connect orchestration. // // For DCR-capable methods (MCP OAuth) the user clicks one "Connect" button and // we do everything: probe the integration's discovery URL → register a client // against the advertised registration endpoint → start // the OAuth flow with the minted client. No app picker, no pasted client id. // // This is extracted as a pure-ish orchestrator (injected `probe`/`register`/ // `start`) so the SEQUENCE is unit-testable without React/atoms. The caller // supplies thin adapters over the `probeOAuth` / `registerDynamicOAuthClient` / // popup-start atoms. // --------------------------------------------------------------------------- /** Discovery result from the probe step (subset of the `probeOAuth` response). */ type DcrProbeResult = { readonly authorizationUrl: string; readonly tokenUrl: string; readonly resource?: string | null; readonly scopesSupported?: readonly string[]; readonly registrationEndpoint?: string | null; readonly tokenEndpointAuthMethodsSupported?: readonly string[]; }; type DcrRegisterArgs = { readonly owner: Owner; readonly slug: OAuthClientSlug; readonly registrationEndpoint: string; readonly authorizationUrl: string; readonly tokenUrl: string; readonly resource?: string | null; readonly scopes: readonly string[]; readonly tokenEndpointAuthMethodsSupported?: readonly string[]; readonly clientName: string; readonly redirectUri?: string; readonly originIntegration?: IntegrationSlug; }; type DcrStartArgs = { readonly client: OAuthClientSlug; readonly owner: Owner; }; /** Outcome of the DCR orchestration. `"started"` means the OAuth flow handed * off (the popup/inline start ran); `"fallback"` means we could not auto-set-up * (probe failed, or no registration endpoint) and the caller should fall back * to the bring-your-own-app picker. */ type DcrOutcome = | { readonly kind: "started" } | { readonly kind: "fallback"; readonly reason: "probe-failed" | "no-registration-endpoint"; }; type RunDcrConnectDeps = { /** Probe the discovery URL → resolved endpoints + (maybe) a registration * endpoint. Resolves to null when the probe fails. */ readonly probe: (url: string) => Promise<DcrProbeResult | null>; /** Register a DCR client → the minted client slug, or null on failure. */ readonly register: (args: DcrRegisterArgs) => Promise<OAuthClientSlug | null>; /** Start the OAuth flow with the minted client (popup / inline). */ readonly start: (args: DcrStartArgs) => void; }; type RunDcrConnectInput = { readonly discoveryUrl: string; readonly owner: Owner; readonly integrationName: string; /** The owner's existing client slugs, so the minted slug stays unique. */ readonly existingSlugs: readonly string[]; /** Scopes declared by the integration's method (override the probed ones). */ readonly declaredScopes?: readonly string[]; /** Browser-facing callback URL registered with DCR when available. */ readonly redirectUri?: string; /** Integration that requested this DCR client. */ readonly integration: IntegrationSlug; }; /** * Run the transparent DCR connect sequence: probe → register → start. * * - Probe failure → `{ kind: "fallback", reason: "probe-failed" }` (caller shows BYO). * - No registration endpoint → `{ kind: "fallback", reason: "no-registration-endpoint" }`. * - Register failure → throws via the injected `register` rejecting; the caller * treats a thrown/rejected register as fallback (kept out of the happy path). * - Success → registers, calls `start`, returns `{ kind: "started" }`. */ export async function runDcrConnect( deps: RunDcrConnectDeps, input: RunDcrConnectInput, ): Promise<DcrOutcome> { const probe = await deps.probe(input.discoveryUrl); if (probe === null) return { kind: "fallback", reason: "probe-failed" }; const registrationEndpoint = probe.registrationEndpoint; if (!registrationEndpoint) return { kind: "fallback", reason: "no-registration-endpoint" };