1931 lines · 81.8 KB · TypeScript
RawDownload
import { useEffect, useMemo, useRef, 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,  createOAuthClientOptimistic,  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,  oauthClientIdMetadataDocumentUrl,  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,  registrationScopes,  type OAuthClientFormPrefill,} 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;  /** Force the per-input label even for a single input (env vars name their   *  field rather than relying on a generic "value / token" placeholder). */  readonly showLabels?: boolean;  readonly values: Readonly<Record<string, string>>;  readonly onChange: (values: Record<string, string>) => void;}) {  if (!props.singleInput) {    return (      <div className="grid gap-3 sm:grid-cols-2">        {props.inputs.map((input) => (          <div key={input.variable} className="min-w-0 space-y-1.5">            <div className="flex min-w-0 items-center gap-2">              <Label                htmlFor={`credential-input-${input.variable}`}                className="min-w-0 truncate font-mono text-xs font-medium text-muted-foreground"              >                {input.label}              </Label>            </div>            <Input              id={`credential-input-${input.variable}`}              type="password"              autoComplete="new-password"              placeholder={`paste ${input.label}`}              value={props.values[input.variable] ?? ""}              onChange={(e: React.ChangeEvent<HTMLInputElement>) =>                props.onChange({                  ...props.values,                  [input.variable]: e.target.value,                })              }              className="h-9 font-mono text-sm"              data-ph-block            />          </div>        ))}      </div>    );  }   // Past the multi-input grid above, singleInput is always true, so labelled  // reduces to showLabels: env vars opt in to naming their single field.  const labelled = props.showLabels ?? false;  return (    <div className="space-y-2">      {props.inputs.map((input) => (        <div key={input.variable} className="space-y-1">          {labelled && (            <Label className="font-mono text-xs text-muted-foreground">{input.label}</Label>          )}          <Input            type="password"            autoComplete="new-password"            placeholder={labelled ? "secret value" : "paste the value / token"}            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 showLabels?: boolean;  /** Allow an external provider (1Password) origin. Off for env methods: the   *  wire's external `from` is keyed under the canonical `token` variable, but   *  an env credential must be keyed under its env var name, so a 1Password   *  origin would persist the secret under the wrong key and the subprocess   *  would launch without it. Paste stays correctly keyed per variable. */  readonly allowExternalProvider?: 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 &&    (props.allowExternalProvider ?? true) &&    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}          showLabels={props.showLabels}          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 CIMD connect orchestration.//// Client ID Metadata Document support is not Dynamic Client Registration:// there is no POST to a registration endpoint and no provider-side app to pick.// The OAuth client identity is this host's public metadata-document URL, stored// locally as a public PKCE client (`clientSecret: ""`) so the existing// `oauth.start` flow can run unchanged.// --------------------------------------------------------------------------- type CimdExistingClient = Pick<  OAuthClientSummary,  "owner" | "slug" | "grant" | "authorizationUrl" | "tokenUrl" | "resource" | "clientId">; type CimdCreateClientArgs = {  readonly owner: Owner;  readonly slug: OAuthClientSlug;  readonly authorizationUrl: string;  readonly tokenUrl: string;  readonly resource?: string | null;  readonly grant: "authorization_code";  readonly clientId: string;  readonly clientSecret: "";}; type RunCimdConnectDeps = {  readonly createClient: (args: CimdCreateClientArgs) => Promise<OAuthClientSlug | null>;  readonly start: (args: { readonly client: OAuthClientSlug; readonly owner: Owner }) => void;}; type RunCimdConnectInput = {  readonly owner: Owner;  readonly integrationName: string;  readonly authorizationUrl: string;

Lines 1–500 of 1931.