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