History

Fix the review findings on the quick-add group

Selection carries the CLICKED surface instead of re-finding by kind, and the kinds-only fallback disambiguates same-kind products by the row's product name against surface slugs — two same-kind products on one domain no longer merge one card's URL with the other's slug, auth, and overrides. The OpenAPI quick add replicates the full add page's method policy: preset OAuth wins outright; otherwise every spec-detected method is preserved (one preview call, only when the registry declared a header) and the registry key is appended only when the spec has no key method. The MCP remote add carries versionNegotiation through the payload schema and handler map, with a round-trip test — the legacy pin previously died at the HTTP boundary. URL presets hide only when registry-listed (built-ins and the provider catalogs declare it), so a deployment's custom preset keeps its card, and quick add matches the plugin's complete preset list. Quick-add bridges render only for plugins with the hook and call it unconditionally; duplicate plugin keys get one bridge.

Rhys Sullivan committed 2 months ago

Commit 82313271 parent 647612113 files · +278 −108Browse files

Showing the changes to packages/plugins/openapi/src (3 of 13 files). Show all

Unified · Split

3 files
  1. packages/plugins/openapi/src/providers/google/presets.ts +1 −0
  2. packages/plugins/openapi/src/providers/microsoft/presets.ts +1 −0
  3. packages/plugins/openapi/src/react/integration-plugin.ts +130 −87
packages/plugins/openapi/src/providers/google/presets.ts+1−0
@@ -346,6 +346,7 @@ export const googleCatalog: readonly IntegrationPreset[] = googleOpenApiPresets · 345 unmodified lines
346346 ...(preset.featured ? { featured: preset.featured } : {}),
347347 family: "google",
348348 specFormat: "google-discovery",
349+ registryListed: true,
349350 defaultSlug: googleServiceSlug(preset.id),
350351 authTemplate: googleCatalogAuthTemplate(preset.id),
351352 ...(GOOGLE_HEALTH_CHECKS[preset.id] ? { healthCheck: GOOGLE_HEALTH_CHECKS[preset.id] } : {}),
packages/plugins/openapi/src/providers/microsoft/presets.ts+1−0
@@ -569,6 +569,7 @@ export const microsoftCatalog: readonly IntegrationPreset[] = microsoftGraphScop · 568 unmodified lines
569569 ...(preset.featured ? { featured: preset.featured } : {}),
570570 family: "microsoft",
571571 specFormat: "microsoft-graph",
572+ registryListed: true,
572573 defaultSlug: microsoftServiceSlug(preset.id),
573574 authTemplate: microsoftGraphCatalogAuthTemplate(preset),
574575 ...(preset.id === "profile" ? { healthCheck: { operation: "me.GetUser" } } : {}),
packages/plugins/openapi/src/react/integration-plugin.ts+130−87
@@ -11,8 +11,9 @@ import { AuthTemplateSlug } from "@executor-js/sdk/shared"; · 10 unmodified lines
1111 import { slugifyNamespace } from "@executor-js/react/plugins/integration-identity";
1212 import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements";
1313 import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
14−import { openApiPresets, type OpenApiPreset } from "../sdk/presets";
15−import { addOpenApiSpec } from "./atoms";
14+import { openApiPresets } from "../sdk/presets";
15+import { detectedAuthenticationTemplates } from "../sdk/derive-auth";
16+import { addOpenApiSpec, previewOpenApiSpec } from "./atoms";
1617 import { openApiWireAuthInput, templateFromPlacements } from "./auth-method-config";
1718 import { decodeOpenApiSpecOverrides } from "../sdk/spec-overrides";
1819
@@ -26,10 +27,15 @@ const normalizedSpecUrl = (url: string): string => { · 7 unmodified lines
2627
2728 /** The preset table still knows things no spec can say — GitHub's OAuth
2829 * endpoints against a spec that declares NO security at all. A registry row
29− * whose URL is a preset's URL gets that knowledge pulled across. */
30−const presetForSpecUrl = (url: string): OpenApiPreset | undefined => {
30+ * whose URL is a preset's URL gets that knowledge pulled across. Matches the
31+ * plugin's COMPLETE preset list, so a deployment's custom presets
32+ * contribute too, not just the built-ins. */
33+const presetForSpecUrl = (
34+ presets: readonly IntegrationPreset[],
35+ url: string,
36+): IntegrationPreset | undefined => {
3137 const target = normalizedSpecUrl(url);
32− return openApiPresets.find(
38+ return presets.find(
3339 (preset) => preset.url !== undefined && normalizedSpecUrl(preset.url) === target,
3440 );
3541 };
@@ -38,72 +44,103 @@ const presetForSpecUrl = (url: string): OpenApiPreset | undefined => { · 2 unmodified lines
3844 * configuration: omitting `authenticationTemplate` and `baseUrl` tells the
3945 * server to derive both from the document, exactly what the add page's
4046 * untouched defaults submit. Registry spec overrides ride along. */
41−function useOpenApiQuickAdd(): (
42− input: IntegrationQuickAddInput,
43−) => Promise<IntegrationQuickAddResult> {
44− const doAdd = useAtomSet(addOpenApiSpec, { mode: "promiseExit" });
45− return useCallback(
46− async (input) => {
47− const slug = slugifyNamespace(input.slug ?? input.name);
48− if (!slug) return { ok: false, reason: "no derivable slug" };
49− // Registry overrides arrive as untyped JSON; a malformed patch goes to
50− // the configuration screen (its editor renders the parse error) rather
51− // than being silently dropped from a "successful" quick add.
52− const specOverrides = input.specOverrides
53− ? decodeOpenApiSpecOverrides(input.specOverrides)
54− : undefined;
55− if (input.specOverrides && input.specOverrides.length > 0 && specOverrides === undefined) {
56− return { ok: false, reason: "unparseable spec overrides" };
57− }
58− const preset = presetForSpecUrl(input.url);
59− // Declared methods travel from BOTH knowledge sources: the preset's
60− // OAuth template (endpoints a spec cannot carry) and the registry's
61− // header pattern (GitHub's spec declares no security at all, but the
62− // registry knows a PAT goes in the Authorization header). When neither
63− // knows anything, omitting the field lets the spec's own schemes derive.
64− const presetMethods = (preset?.authTemplate ?? []).flatMap((template) =>
65− template.kind === "oauth2"
66− ? [
67− openApiWireAuthInput({
68− ...template,
69− slug: AuthTemplateSlug.make(template.slug),
70− resource: template.resource ?? undefined,
71− }),
72− ]
73− : [],
74− );
75− const registryPlacement = input.authHeader
76− ? placementFromHeaderPattern(input.authHeader)
77− : null;
78− const authenticationTemplate = [
79− ...presetMethods,
80− ...(registryPlacement
81− ? [openApiWireAuthInput(templateFromPlacements([registryPlacement]))]
82− : []),
83− ];
84− const presetOverrides =
85− specOverrides && specOverrides.length > 0 ? specOverrides : preset?.specOverrides;
86− const exit = await doAdd({
87− payload: {
88− spec: { kind: "url" as const, url: input.url },
89− slug,
90− name: input.name,
91− ...(input.domain ? { displayDomain: input.domain } : {}),
92− ...(preset?.specFormat ? { specFormat: preset.specFormat } : {}),
93− ...(preset?.family ? { family: preset.family } : {}),
94− ...(preset?.healthCheck ? { healthCheck: preset.healthCheck } : {}),
95− ...(presetOverrides && presetOverrides.length > 0
96− ? { specOverrides: presetOverrides }
97− : {}),
98− ...(authenticationTemplate.length > 0 ? { authenticationTemplate } : {}),
99− },
100− reactivityKeys: integrationWriteKeys,
101− });
102− if (Exit.isFailure(exit)) return { ok: false, reason: "add failed" };
103− return { ok: true, slug: String(exit.value.slug) };
104− },
105− [doAdd],
106− );
47+function makeUseQuickAdd(presets: readonly IntegrationPreset[]) {
48+ return function useOpenApiQuickAdd(): (
49+ input: IntegrationQuickAddInput,
50+ ) => Promise<IntegrationQuickAddResult> {
51+ const doAdd = useAtomSet(addOpenApiSpec, { mode: "promiseExit" });
52+ const doPreview = useAtomSet(previewOpenApiSpec, { mode: "promiseExit" });
53+ return useCallback(
54+ async (input) => {
55+ const slug = slugifyNamespace(input.slug ?? input.name);
56+ if (!slug) return { ok: false, reason: "no derivable slug" };
57+ // Registry overrides arrive as untyped JSON; a malformed patch goes to
58+ // the configuration screen (its editor renders the parse error) rather
59+ // than being silently dropped from a "successful" quick add.
60+ const specOverrides = input.specOverrides
61+ ? decodeOpenApiSpecOverrides(input.specOverrides)
62+ : undefined;
63+ if (input.specOverrides && input.specOverrides.length > 0 && specOverrides === undefined) {
64+ return { ok: false, reason: "unparseable spec overrides" };
65+ }
66+ const preset = presetForSpecUrl(presets, input.url);
67+ // The FULL add page's method policy, replicated exactly: a preset's
68+ // OAuth template wins outright; otherwise EVERY spec-detected method is
69+ // preserved (an explicit template suppresses server-side derivation, so
70+ // sending only the registry header would erase a spec's declared OAuth
71+ // and key schemes); the registry's header pattern is appended only when
72+ // the detected set has no API-key method (GitHub declares no security
73+ // at all, and a PAT header is how most calls actually authenticate).
74+ const registryPlacement = input.authHeader
75+ ? placementFromHeaderPattern(input.authHeader)
76+ : null;
77+ const presetMethods = (preset?.authTemplate ?? []).flatMap((template) =>
78+ template.kind === "oauth2"
79+ ? [
80+ openApiWireAuthInput({
81+ ...template,
82+ slug: AuthTemplateSlug.make(template.slug),
83+ resource: template.resource ?? undefined,
84+ }),
85+ ]
86+ : [],
87+ );
88+ let authenticationTemplate = presetMethods;
89+ if (presetMethods.length === 0 && registryPlacement) {
90+ // Composing with spec knowledge needs the spec: one preview call,
91+ // only on this path (a plain registry row with no auth facts still
92+ // adds with zero extra round trips).
93+ const previewExit = await doPreview({ payload: { spec: input.url } });
94+ if (Exit.isFailure(previewExit)) return { ok: false, reason: "preview failed" };
95+ const previewSummary = previewExit.value;
96+ const detected = detectedAuthenticationTemplates(
97+ previewSummary.headerPresets,
98+ previewSummary.oauth2Presets,
99+ previewSummary.servers[0]?.url ?? "",
100+ );
101+ const detectedHasApiKey = detected.some((template) => template.kind === "apikey");
102+ authenticationTemplate = [
103+ ...detected.map(openApiWireAuthInput),
104+ ...(detectedHasApiKey
105+ ? []
106+ : [openApiWireAuthInput(templateFromPlacements([registryPlacement]))]),
107+ ];
108+ } else if (presetMethods.length > 0 && registryPlacement) {
109+ authenticationTemplate = [
110+ ...presetMethods,
111+ openApiWireAuthInput(templateFromPlacements([registryPlacement])),
112+ ];
113+ }
114+ // Preset overrides arrive as the same untyped JSON shape the registry
115+ // sends; decode both through the one boundary.
116+ const presetOverrides =
117+ specOverrides && specOverrides.length > 0
118+ ? specOverrides
119+ : preset?.specOverrides
120+ ? decodeOpenApiSpecOverrides(preset.specOverrides)
121+ : undefined;
122+ const exit = await doAdd({
123+ payload: {
124+ spec: { kind: "url" as const, url: input.url },
125+ slug,
126+ name: input.name,
127+ ...(input.domain ? { displayDomain: input.domain } : {}),
128+ ...(preset?.specFormat ? { specFormat: preset.specFormat } : {}),
129+ ...(preset?.family ? { family: preset.family } : {}),
130+ ...(preset?.healthCheck ? { healthCheck: preset.healthCheck } : {}),
131+ ...(presetOverrides && presetOverrides.length > 0
132+ ? { specOverrides: presetOverrides }
133+ : {}),
134+ ...(authenticationTemplate.length > 0 ? { authenticationTemplate } : {}),
135+ },
136+ reactivityKeys: integrationWriteKeys,
137+ });
138+ if (Exit.isFailure(exit)) return { ok: false, reason: "add failed" };
139+ return { ok: true, slug: String(exit.value.slug) };
140+ },
141+ [doAdd, doPreview],
142+ );
143+ };
107144 }
108145
109146 const importAdd = () => import("./AddOpenApiIntegration");
@@ -114,21 +151,27 @@ export interface OpenApiClientConfig { · 4 unmodified lines
114151 readonly presets?: readonly IntegrationPreset[];
115152 }
116153
117−export const createOpenApiIntegrationPlugin = (
118− config?: OpenApiClientConfig,
119−): IntegrationPlugin => ({
120− key: "openapi",
121− label: "OpenAPI",
122− add: lazy(importAdd),
123− editSheet: lazy(importEditSheet),
124− accounts: lazy(importAccounts),
125− presets: [...openApiPresets, ...(config?.presets ?? [])],
126− preload: () => {
127− void importAdd();
128− void importEditSheet();
129− void importAccounts();
130− },
131− useQuickAdd: useOpenApiQuickAdd,
132−});
154+export const createOpenApiIntegrationPlugin = (config?: OpenApiClientConfig): IntegrationPlugin => {
155+ // Built-ins are registry-listed (the picker shows the registry's card);
156+ // a deployment's custom presets are not, and keep their own cards.
157+ const presets: readonly IntegrationPreset[] = [
158+ ...openApiPresets.map((preset) => ({ ...preset, registryListed: true })),
159+ ...(config?.presets ?? []),
160+ ];
161+ return {
162+ key: "openapi",
163+ label: "OpenAPI",
164+ add: lazy(importAdd),
165+ editSheet: lazy(importEditSheet),
166+ accounts: lazy(importAccounts),
167+ presets,
168+ preload: () => {
169+ void importAdd();
170+ void importEditSheet();
171+ void importAccounts();
172+ },
173+ useQuickAdd: makeUseQuickAdd(presets),
174+ };
175+};
133176
134177 export const openApiIntegrationPlugin: IntegrationPlugin = createOpenApiIntegrationPlugin();

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t