639 lines · 25.6 KB · TypeScript
RawDownload
import {  ConnectionAddress,  PolicyId,  ProviderKey,  type ArtifactId,  type AuthTemplateSlug,  type Connection,  type ConnectionName,  type IntegrationSlug,  type OAuthClientSlug,  type OAuthClientSummary,  type OAuthGrant,  type Owner,  type ProviderItemId,  type TokenEndpointAuthMethod,  type ToolAddress,} from "@executor-js/sdk/shared";import * as Atom from "effect/unstable/reactivity/Atom";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import * as Reactivity from "effect/unstable/reactivity/Reactivity";import * as Effect from "effect/Effect"; import { ExecutorApiClient } from "./client";import { connectionWriteKeys, ReactivityKey } from "./reactivity-keys"; // ---------------------------------------------------------------------------// Query atoms — typed, cached, reactive. v2: owner-scoped (org | user) instead// of scope-stacked. The executor is bound to its `{ tenant, subject }` from the// request auth, so atoms carry only the UI's `owner` choice — never a scope id.// --------------------------------------------------------------------------- /** * Whole tool catalog across BOTH owners (`tools.list` with no `owner`). * * Omitting `owner` short-circuits the executor's owner WHERE clause to `true`, * so org (`subject=""`) and user (`subject=<user>`) rows return together. Each * wire row still carries its own `owner` + `connection`, so the view layer can * group/badge per account without re-querying. This replaces the old * owner-scoped `toolsAtom(owner)` view filter. */export const toolsAllAtom = ExecutorApiClient.query("tools", "list", {  query: {},  timeToLive: "30 seconds",  reactivityKeys: [ReactivityKey.tools],}); /** * Tools produced by one integration's connections across BOTH owners. Same * omit-owner merge as `toolsAllAtom`, narrowed to a single integration. Each row * retains its `owner` + `connection` for per-account grouping. */export const integrationToolsAllAtom = (integration: IntegrationSlug) =>  ExecutorApiClient.query("tools", "list", {    query: { integration },    timeToLive: "30 seconds",    reactivityKeys: [ReactivityKey.tools],  }); export const toolSchemaAtom = (address: ToolAddress) =>  ExecutorApiClient.query("tools", "schema", {    query: { address },    timeToLive: "1 minute",    reactivityKeys: [ReactivityKey.tools],  }); // ---------------------------------------------------------------------------// Integrations — the tenant-shared catalog (was `sources`).// --------------------------------------------------------------------------- export const integrationsAtom = ExecutorApiClient.query("integrations", "list", {  timeToLive: "30 seconds",  reactivityKeys: [ReactivityKey.integrations],}); /** Single integration by slug — derived from the catalog list. */export const integrationAtom = (slug: IntegrationSlug) =>  Atom.mapResult(    integrationsOptimisticAtom,    (integrations) => integrations.find((i) => i.slug === slug) ?? null,  ); /** The integration's currently declared health check (null when none is set). *  Backs the editor's "current selection" and the status surfaces deciding *  whether a connection can be probed at all. */export const integrationHealthCheckAtom = (slug: IntegrationSlug) =>  ExecutorApiClient.query("integrations", "healthCheckGet", {    params: { slug },    timeToLive: "30 seconds",    reactivityKeys: [ReactivityKey.healthChecks],  }); /** Operations the user can pick as the health check, server-ranked *  (non-destructive + fewest-required-args first). Backs the editor picker. */export const integrationHealthCheckCandidatesAtom = (slug: IntegrationSlug) =>  ExecutorApiClient.query("integrations", "healthCheckCandidates", {    params: { slug },    timeToLive: "5 minutes",    reactivityKeys: [ReactivityKey.integrations],  }); // ---------------------------------------------------------------------------// Connections — owner-scoped credentials (was `secrets` + `connections`).// --------------------------------------------------------------------------- export const connectionsAtom = (owner: Owner) =>  ExecutorApiClient.query("connections", "list", {    query: { owner },    timeToLive: "30 seconds",    reactivityKeys: [ReactivityKey.connections],  }); /** * All connections across BOTH owners (`connections.list` with no `owner`). Same * omit-owner merge as the tools atoms: org + user connections return together, * each carrying its own `owner`. This is the read/view surface; owner-scoped * optimistic write families (`connectionsOptimisticAtom(owner)` etc.) stay keyed * by owner because writes target a specific owner. */export const connectionsAllAtom = ExecutorApiClient.query("connections", "list", {  query: {},  timeToLive: "30 seconds",  reactivityKeys: [ReactivityKey.connections],}); // ---------------------------------------------------------------------------// Providers — credential-backend discovery (new in v2).// --------------------------------------------------------------------------- export const providersAtom = ExecutorApiClient.query("providers", "list", {  timeToLive: "5 minutes",  reactivityKeys: [ReactivityKey.providers],}); export const providerItemsAtom = (key: ProviderKey) =>  ExecutorApiClient.query("providers", "items", {    params: { key },    // Long retention on purpose: external-provider listings (1Password) are    // slow, so pickers render the last-known list instantly and revalidate in    // the background on mount instead of flashing a loading state each open.    timeToLive: "10 minutes",    reactivityKeys: [ReactivityKey.providers],  }); // ---------------------------------------------------------------------------// Policies — owner-scoped.// --------------------------------------------------------------------------- export const policiesAtom = ExecutorApiClient.query("policies", "list", {  timeToLive: "30 seconds",  reactivityKeys: [ReactivityKey.policies],}); export const pausedExecutionAtom = (executionId: string) =>  ExecutorApiClient.query("executions", "getPaused", {    params: { executionId },    timeToLive: "5 seconds",  }); export const artifactsAtom = ExecutorApiClient.query("artifacts", "list", {  timeToLive: "30 seconds",  reactivityKeys: [ReactivityKey.artifacts],}); /** * One artifact, with its `code`. `artifacts.list` deliberately omits the source * so a long list stays cheap, so the detail page must fetch the row itself — * it cannot derive from the list atom the way `integrationAtom` does. */export const artifactAtom = Atom.family((artifactId: ArtifactId) =>  ExecutorApiClient.query("artifacts", "get", {    params: { artifactId },    timeToLive: "30 seconds",    reactivityKeys: [ReactivityKey.artifacts],  }),); // ---------------------------------------------------------------------------// Mutation atoms — reactivityKeys must be passed at call site (effect-atom// does not accept them at definition time). See `reactivity-keys.tsx` for the// canonical key arrays.// --------------------------------------------------------------------------- export const createConnection = ExecutorApiClient.mutation("connections", "create"); export const removeConnection = ExecutorApiClient.mutation("connections", "remove"); /** Edit a connection's user-curated metadata (description, identityLabel). *  Pass `reactivityKeys: connectionWriteKeys` at the call site. */export const updateConnection = ExecutorApiClient.mutation("connections", "update"); export const refreshConnection = ExecutorApiClient.mutation("connections", "refresh"); /** Probe a SAVED connection's health on demand (the "Check now" button). The *  server persists the verdict on the connection row (`last_health`), so a check *  that changes the verdict must invalidate the connections cache or a later *  read within the atom TTL serves the pre-check state. Callers pass *  `reactivityKeys: connectionCheckKeys` (see `use-connection-health.ts` for the *  manual-vs-automatic split that keeps the automatic path from churning the *  cache on every load). */export const checkConnectionHealth = ExecutorApiClient.mutation("connections", "checkHealth"); export interface CheckConnectionHealthArgs {  readonly params: {    readonly owner: Owner;    readonly integration: IntegrationSlug;    readonly name: ConnectionName;  };  readonly query: { readonly ifStaleMs?: number };  readonly reactivityKeys?: ReadonlyArray<unknown>;} /** The AUTOMATIC health probe, one atom PER CONNECTION. * *  `checkConnectionHealth` above is one shared mutation atom. Awaiting it *  (`useAtomSet(..., { mode: "promiseExit" })`) resolves with the atom's next *  settled result, whichever call produced it, and a new call interrupts the *  one in flight. A surface that probes every row of a list in one pass *  therefore cancels all but the last probe and hands every row the LAST *  row's verdict. Each row then reads a foreign verdict as a change to its own *  connection, refreshes the connections cache, and re-probes: the probe storm *  the automatic path was built to avoid. Keying the atom by connection address *  gives every probe its own fiber and its own result. */export const checkConnectionHealthFor = Atom.family((address: ConnectionAddress) =>  ExecutorApiClient.runtime.fn<CheckConnectionHealthArgs>()((args) => {    const probe = Effect.gen(function* () {      const client = yield* ExecutorApiClient;      return yield* client.connections.checkHealth({ params: args.params, query: args.query });    }).pipe(Effect.withSpan("connection.health.probe", { attributes: { address } }));    return args.reactivityKeys ? Reactivity.mutation(probe, args.reactivityKeys) : probe;  }),); /** Validate an IN-FLIGHT credential without saving it (the key-first connect *  flow). Returns the probe result the UI derives a connection name from. */export const validateConnection = ExecutorApiClient.mutation("connections", "validate"); export const updateIntegration = ExecutorApiClient.mutation("integrations", "update"); export const removeIntegration = ExecutorApiClient.mutation("integrations", "remove"); export const detectIntegration = ExecutorApiClient.mutation("integrations", "detect"); /** Set or clear an integration's declared health check. *  Pass `reactivityKeys: healthCheckWriteKeys` at the call site. */export const setIntegrationHealthCheck = ExecutorApiClient.mutation(  "integrations",  "healthCheckSet",); // ---------------------------------------------------------------------------// OAuth — v2 flow. `start` runs a registered client to mint a connection for an// integration; `complete` exchanges the authorization code; `cancel` drops an// in-flight session; `probe` discovers an authorization-server's metadata.// --------------------------------------------------------------------------- export const createOAuthClient = ExecutorApiClient.mutation("oauth", "createClient"); /** RFC 7591 Dynamic Client Registration — mint a client against the server's *  registration endpoint, with NO pasted client id/secret. *  Pass `reactivityKeys: oauthClientWriteKeys` at the call site to refresh the *  clients list. */export const registerDynamicOAuthClient = ExecutorApiClient.mutation("oauth", "registerDynamic"); /** Registered OAuth clients (apps) visible to the caller — the org's shared *  clients + the caller's own user clients. Metadata-only summaries; the *  server never returns the client secret. */export const oauthClientsAtom = ExecutorApiClient.query("oauth", "listClients", {  timeToLive: "30 seconds",  reactivityKeys: [ReactivityKey.oauthClients],}); /** Permanently remove a registered OAuth app. Keyed by `(owner, slug)`: the *  slug is a path param, the owner a payload field (mirrors the connections / *  policies delete contract). Connections that referenced the app keep their *  stored slug and surface a reconnect prompt at next token refresh — this *  never cascades into connections. */export const removeOAuthClient = ExecutorApiClient.mutation("oauth", "removeClient"); export const probeOAuth = ExecutorApiClient.mutation("oauth", "probe"); export const startOAuth = ExecutorApiClient.mutation("oauth", "start"); export const completeOAuth = ExecutorApiClient.mutation("oauth", "complete"); export const cancelOAuth = ExecutorApiClient.mutation("oauth", "cancel"); /** Fire-and-forget reactivity bump after an OAuth flow mints/refreshes a *  connection out-of-band (the popup completes via the server callback). The *  body is a no-op; declaring `connectionWriteKeys` invalidates the connection *  and tool queries so the UI re-reads the freshly-minted connection. */export const oauthConnectionCompleted = ExecutorApiClient.runtime.fn<{  readonly reactivityKeys: typeof connectionWriteKeys;}>()(() => Effect.void, {  reactivityKeys: connectionWriteKeys,}); export const createPolicy = ExecutorApiClient.mutation("policies", "create"); export const updatePolicy = ExecutorApiClient.mutation("policies", "update"); export const removePolicy = ExecutorApiClient.mutation("policies", "remove"); export const renameArtifact = ExecutorApiClient.mutation("artifacts", "rename"); export const removeArtifact = ExecutorApiClient.mutation("artifacts", "remove"); /** * Upgrade an artifact's gallery preview to a snapshot of a settled render. * * Fired by the artifact page after the shell reports that its render has * settled with real data. Best-effort by design: a failure here means the card * keeps showing its layout preview, which is a perfectly good picture. */export const setArtifactPreview = ExecutorApiClient.mutation("artifacts", "setPreview"); export const resumeExecution = ExecutorApiClient.mutation("executions", "resume"); /** Run codemode source (`POST /executions`). Used by the per-tool Run/Test panel *  to invoke a single tool against a connection and verify credentials. */export const executeCode = ExecutorApiClient.mutation("executions", "execute"); // ---------------------------------------------------------------------------// Integrations — optimistic surface.// --------------------------------------------------------------------------- export const integrationsOptimisticAtom = Atom.optimistic(integrationsAtom); export const removeIntegrationOptimistic = integrationsOptimisticAtom.pipe(  Atom.optimisticFn({    reducer: (current, arg: { readonly params: { readonly slug: IntegrationSlug } }) =>      AsyncResult.map(current, (rows) =>        rows.filter((integration) => integration.slug !== arg.params.slug),      ),    fn: removeIntegration,  }),); // ---------------------------------------------------------------------------// Connections — optimistic removals. Owner-scoped: a connection is identified// by (owner, integration, name) so removals filter by all three.// --------------------------------------------------------------------------- export const connectionsOptimisticAtom = Atom.family((owner: Owner) =>  Atom.optimistic(connectionsAtom(owner)),); export const removeConnectionOptimistic = Atom.family((owner: Owner) =>  connectionsOptimisticAtom(owner).pipe(    Atom.optimisticFn({      reducer: (        current,        arg: {          readonly params: {            readonly owner: Owner;            readonly integration: IntegrationSlug;            readonly name: ConnectionName;          };        },      ) =>        AsyncResult.map(current, (rows) =>          rows.filter(            (connection) =>              connection.owner !== arg.params.owner ||              connection.integration !== arg.params.integration ||              connection.name !== arg.params.name,          ),        ),      fn: removeConnection,    }),  ),); /** This owner's connections, narrowed to a single integration. Derived from the *  optimistic surface so adds/removes flow through without an extra fetch. */export const connectionsForIntegrationAtom = Atom.family(  (key: { readonly integration: IntegrationSlug; readonly owner: Owner }) =>    Atom.mapResult(connectionsOptimisticAtom(key.owner), (rows: readonly Connection[]) =>      rows.filter((connection: Connection) => connection.integration === key.integration),    ),); // The connection-create payload mirrors `CreateConnectionPayload` from the core// API: the common fields plus exactly one value origin — a single pasted `value`// (the `token` input), a `values` map (one per named input, e.g. Datadog's two// keys), or an external `from` reference.type CreateConnectionArg = {  readonly payload: {    readonly owner: Owner;    readonly name: ConnectionName;    readonly integration: IntegrationSlug;    readonly template: AuthTemplateSlug;    readonly identityLabel?: string | null;    readonly description?: string | null;  } & (    | { readonly value: string }    | { readonly values: Record<string, string> }    | {        readonly from: {          readonly provider: ProviderKey;          readonly id: ProviderItemId;        };      }  );  readonly reactivityKeys: typeof connectionWriteKeys;}; /** Optimistic create — prepends a placeholder row built from the payload so the *  new account appears immediately, then reconciles when the server returns the *  canonical connection. `provider`/`address` are placeholders until the refresh *  lands. */export const addConnectionOptimistic = Atom.family((owner: Owner) =>  connectionsOptimisticAtom(owner).pipe(    Atom.optimisticFn({      reducer: (current, arg: CreateConnectionArg) =>        AsyncResult.map(current, (rows) => {          const { payload } = arg;          // Shape matches the wire `ConnectionResponse` (non-optional          // `identityLabel`/`expiresAt`), not the broader SDK `Connection`.          const optimistic = {            owner: payload.owner,            name: payload.name,            integration: payload.integration,            template: payload.template,            provider: ProviderKey.make("default"),            address: ConnectionAddress.make(              `tools.${payload.integration}.${payload.owner}.${payload.name}`,            ),            identityLabel: payload.identityLabel ?? null,            description: payload.description ?? null,            expiresAt: null,            // Optimistic placeholder predates the server resolving which app (if            // any) minted this connection; reconciled on refresh. Matches the            // wire `ConnectionResponse.oauthClient` (nullable).            oauthClient: null,            oauthClientOwner: null,            oauthScope: null,            missingOAuthScopes: [],            lastHealth: null,          };          return [optimistic, ...rows];        }),      fn: createConnection,    }),  ),); // ---------------------------------------------------------------------------// Policies — optimistic surface. Reads go through `policiesOptimisticAtom`// (which layers in-flight transitions on top of `policiesAtom`), and writes// go through the matching `*PolicyOptimistic` mutation atoms. Each mutation// declares a reducer that produces the next array of rows; effect-atom's// `Atom.optimisticFn` handles transition tracking, waiting state, and the// post-commit refresh — including racing calls (latest reducer wins).// --------------------------------------------------------------------------- export const policiesOptimisticAtom = Atom.optimistic(policiesAtom); export const createPolicyOptimistic = policiesOptimisticAtom.pipe(  Atom.optimisticFn({    reducer: (      current,      arg: {        readonly payload: {          readonly owner: Owner;          readonly pattern: string;          readonly action: "approve" | "require_approval" | "block";        };      },    ) =>      AsyncResult.map(current, (rows) => [        {          id: PolicyId.make(`pending-${Math.random().toString(36).slice(2)}`),          owner: arg.payload.owner,          pattern: arg.payload.pattern,          action: arg.payload.action,          // Empty string sorts before any fractional-indexing key, so the          // placeholder lands at the top until the server returns the          // canonical key.          position: "",          createdAt: Date.now(),          updatedAt: Date.now(),        },        ...rows,      ]),    fn: createPolicy,  }),); export const updatePolicyOptimistic = policiesOptimisticAtom.pipe(  Atom.optimisticFn({    reducer: (      current,      arg: {        readonly params: { readonly policyId: PolicyId };        readonly payload: {          readonly action?: "approve" | "require_approval" | "block";          readonly pattern?: string;          readonly position?: string;        };      },

Lines 1–500 of 639.