import { ConnectionAddress, PolicyId, ProviderKey, type AuthTemplateSlug, type Connection, type ConnectionName, type IntegrationSlug, type OAuthClientSlug, type OAuthClientSummary, type OAuthGrant, type Owner, type ProviderItemId, 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 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, ); // ---------------------------------------------------------------------------// 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 }, timeToLive: "30 seconds", 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", }); // ---------------------------------------------------------------------------// 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"); export const refreshConnection = ExecutorApiClient.mutation("connections", "refresh"); export const updateIntegration = ExecutorApiClient.mutation("integrations", "update"); export const removeIntegration = ExecutorApiClient.mutation("integrations", "remove"); export const detectIntegration = ExecutorApiClient.mutation("integrations", "detect"); // ---------------------------------------------------------------------------// 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 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 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, 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, }; 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; }; }, ) => AsyncResult.map(current, (rows) => rows.map((r) => r.id === arg.params.policyId ? { ...r, ...(arg.payload.action !== undefined ? { action: arg.payload.action } : {}), ...(arg.payload.pattern !== undefined ? { pattern: arg.payload.pattern } : {}), ...(arg.payload.position !== undefined ? { position: arg.payload.position } : {}), } : r, ), ), fn: updatePolicy, }),); export const removePolicyOptimistic = policiesOptimisticAtom.pipe( Atom.optimisticFn({ reducer: (current, arg: { readonly params: { readonly policyId: PolicyId } }) => AsyncResult.map(current, (rows) => rows.filter((r) => r.id !== arg.params.policyId)), fn: removePolicy, }),); // ---------------------------------------------------------------------------// OAuth clients (apps) — optimistic surface. The list reads through// `oauthClientsOptimisticAtom`; the remove mutation drops the matching// `(owner, slug)` row immediately, then `oauthClientWriteKeys` refreshes the// canonical list. Removals never touch `connections`: an orphaned connection// stays visible and surfaces a reconnect prompt at its next refresh.// --------------------------------------------------------------------------- export const oauthClientsOptimisticAtom = Atom.optimistic(oauthClientsAtom); export const removeOAuthClientOptimistic = oauthClientsOptimisticAtom.pipe( Atom.optimisticFn({ reducer: ( current, arg: { readonly params: { readonly slug: OAuthClientSlug }; readonly payload: { readonly owner: Owner }; }, ) => AsyncResult.map(current, (rows) => rows.filter( (client) => client.owner !== arg.payload.owner || client.slug !== arg.params.slug, ), ), fn: removeOAuthClient, }),); /** Register (or edit) an OAuth app and paint it into the list immediately. * `createClient` upserts by `(owner, slug)` server-side, so the reducer mirrors * that: an existing row is replaced (edit), a new slug is appended (register). * The client secret is never part of the summary, so it is dropped. The * post-commit refresh (`oauthClientWriteKeys`) reconciles with the server. */export const createOAuthClientOptimistic = oauthClientsOptimisticAtom.pipe( Atom.optimisticFn({ reducer: ( current, arg: { readonly payload: { readonly owner: Owner; readonly slug: OAuthClientSlug; readonly authorizationUrl: string; readonly tokenUrl: string; readonly grant: OAuthGrant; readonly clientId: string; readonly resource?: string | null; }; }, ) => AsyncResult.map(current, (rows) => { const summary: OAuthClientSummary = { owner: arg.payload.owner, slug: arg.payload.slug, grant: arg.payload.grant, authorizationUrl: arg.payload.authorizationUrl, tokenUrl: arg.payload.tokenUrl, resource: arg.payload.resource ?? null, clientId: arg.payload.clientId, origin: { kind: "manual" }, }; const index = rows.findIndex( (client) => client.owner === summary.owner && client.slug === summary.slug, ); if (index === -1) return [...rows, summary]; const next = rows.slice(); next[index] = summary; return next; }), fn: createOAuthClient, }),);
import {
ConnectionAddress,
PolicyId,
ProviderKey,
type AuthTemplateSlug,
type Connection,
type ConnectionName,
type IntegrationSlug,
type OAuthClientSlug,
type OAuthClientSummary,
type OAuthGrant,
type Owner,
type ProviderItemId,
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 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,
);
// ---------------------------------------------------------------------------
// 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 },
timeToLive: "30 seconds",
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",
});
// ---------------------------------------------------------------------------
// 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");
export const refreshConnection = ExecutorApiClient.mutation("connections", "refresh");
export const updateIntegration = ExecutorApiClient.mutation("integrations", "update");
export const removeIntegration = ExecutorApiClient.mutation("integrations", "remove");
export const detectIntegration = ExecutorApiClient.mutation("integrations", "detect");
// ---------------------------------------------------------------------------
// 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 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 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,
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,
};
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;
};
},
) =>
AsyncResult.map(current, (rows) =>
rows.map((r) =>
r.id === arg.params.policyId
? {
...r,
...(arg.payload.action !== undefined ? { action: arg.payload.action } : {}),
...(arg.payload.pattern !== undefined ? { pattern: arg.payload.pattern } : {}),
...(arg.payload.position !== undefined ? { position: arg.payload.position } : {}),
}
: r,
),
),
fn: updatePolicy,
}),
);
export const removePolicyOptimistic = policiesOptimisticAtom.pipe(
Atom.optimisticFn({
reducer: (current, arg: { readonly params: { readonly policyId: PolicyId } }) =>
AsyncResult.map(current, (rows) => rows.filter((r) => r.id !== arg.params.policyId)),
fn: removePolicy,
}),
);
// ---------------------------------------------------------------------------
// OAuth clients (apps) — optimistic surface. The list reads through
// `oauthClientsOptimisticAtom`; the remove mutation drops the matching
// `(owner, slug)` row immediately, then `oauthClientWriteKeys` refreshes the
// canonical list. Removals never touch `connections`: an orphaned connection
// stays visible and surfaces a reconnect prompt at its next refresh.
// ---------------------------------------------------------------------------
export const oauthClientsOptimisticAtom = Atom.optimistic(oauthClientsAtom);
export const removeOAuthClientOptimistic = oauthClientsOptimisticAtom.pipe(
Atom.optimisticFn({
reducer: (
current,
arg: {
readonly params: { readonly slug: OAuthClientSlug };
readonly payload: { readonly owner: Owner };
},
) =>
AsyncResult.map(current, (rows) =>
rows.filter(
(client) => client.owner !== arg.payload.owner || client.slug !== arg.params.slug,
),
),
fn: removeOAuthClient,
}),
);
/** Register (or edit) an OAuth app and paint it into the list immediately.
* `createClient` upserts by `(owner, slug)` server-side, so the reducer mirrors
* that: an existing row is replaced (edit), a new slug is appended (register).
* The client secret is never part of the summary, so it is dropped. The
* post-commit refresh (`oauthClientWriteKeys`) reconciles with the server. */
export const createOAuthClientOptimistic = oauthClientsOptimisticAtom.pipe(
Atom.optimisticFn({
reducer: (
current,
arg: {
readonly payload: {
readonly owner: Owner;
readonly slug: OAuthClientSlug;
readonly authorizationUrl: string;
readonly tokenUrl: string;
readonly grant: OAuthGrant;
readonly clientId: string;
readonly resource?: string | null;
};
},
) =>
AsyncResult.map(current, (rows) => {
const summary: OAuthClientSummary = {
owner: arg.payload.owner,
slug: arg.payload.slug,
grant: arg.payload.grant,
authorizationUrl: arg.payload.authorizationUrl,
tokenUrl: arg.payload.tokenUrl,
resource: arg.payload.resource ?? null,
clientId: arg.payload.clientId,
origin: { kind: "manual" },
};
const index = rows.findIndex(
(client) => client.owner === summary.owner && client.slug === summary.slug,
);
if (index === -1) return [...rows, summary];
const next = rows.slice();
next[index] = summary;
return next;
}),
fn: createOAuthClient,
}),
);