// --------------------------------------------------------------------------- // Custom auth-method actions — the shared create/remove skeleton behind every // accounts panel's "+ Custom method" flow. The shape is identical across // plugins (openapi, graphql, mcp): convert the generic placements into the // plugin's wire templates, merge-append via the plugin's configureAuth // endpoint, then diff the response against the prior slugs to find the created // method. Remove filters the set by slug and replaces. // // The plugin contributes only a small CODEC (wire-template ↔ generic // `AuthMethod`) and its configure mutation; everything else lives here. // --------------------------------------------------------------------------- import { useCallback } from "react"; import * as Exit from "effect/Exit"; import type { AuthMethod, Placement } from "./auth-placements"; import type { CreateCustomMethod } from "../components/add-custom-method-modal"; /** The plugin-specific edges of the custom-method flow. `T` is the plugin's * wire auth-template type (an object carrying a `slug`). */ export interface AuthMethodsCodec { /** Project wire templates into the generic `AuthMethod[]` the hub renders. */ readonly toAuthMethods: (templates: readonly T[]) => readonly AuthMethod[]; /** Build the wire templates for a custom method from its generic placements. * Empty result ⇒ the placements declare nothing persistable. Slugs are * omitted/blank — the backend backfills `custom_`. */ readonly templatesFromPlacements: (placements: readonly Placement[]) => readonly T[]; readonly slugOf: (template: T) => string; } /** The plugin's configureAuth mutation, normalized: merge-append by default, * `replace` swaps the whole declared set. Resolves to the resulting template * array via an Exit (the atom mutation's `promiseExit`). */ export type ConfigureAuthMethods = (input: { readonly authenticationTemplate: readonly T[]; readonly mode?: "merge" | "replace"; }) => Promise>; export interface CustomMethodActions { readonly createCustomMethod: CreateCustomMethod; readonly removeCustomMethod: (method: AuthMethod) => Promise; } export function useCustomMethodActions(options: { readonly existing: readonly T[]; readonly codec: AuthMethodsCodec; readonly configure: ConfigureAuthMethods; }): CustomMethodActions { const { existing, codec, configure } = options; const createCustomMethod = useCallback( async (input: { readonly label: string; readonly placements: readonly Placement[] }) => { const templates = codec.templatesFromPlacements(input.placements); if (templates.length === 0) return null; const exit = await configure({ authenticationTemplate: templates }); if (Exit.isFailure(exit)) return null; const before = new Set(existing.map((template: T) => codec.slugOf(template))); const created = codec .toAuthMethods(exit.value) .find((candidate: AuthMethod) => !before.has(String(candidate.template))); return created ?? null; }, [codec, configure, existing], ); const removeCustomMethod = useCallback( async (method: AuthMethod): Promise => { if (method.source !== "custom") return false; const next = existing.filter( (template: T) => codec.slugOf(template) !== String(method.template), ); const exit = await configure({ authenticationTemplate: next, mode: "replace" }); return Exit.isSuccess(exit); }, [codec, configure, existing], ); return { createCustomMethod, removeCustomMethod }; }