1530 lines · 62.8 KB · TypeScript
RawDownload
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.