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.
Showing the changes to packages/plugins/openapi/src (3 of 13 files). Show all
Unified · Split
| @@ -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] } : {}), |
| @@ -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" } } : {}), |
| @@ -11,8 +11,9 @@ import { AuthTemplateSlug } from "@executor-js/sdk/shared"; · 10 unmodified lines | |||
| 11 | import { slugifyNamespace } from "@executor-js/react/plugins/integration-identity"; | 11 | import { slugifyNamespace } from "@executor-js/react/plugins/integration-identity"; |
| 12 | import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements"; | 12 | import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements"; |
| 13 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; | 13 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; |
| 14 | import { openApiPresets, type OpenApiPreset } from "../sdk/presets"; | 14 | import { openApiPresets } from "../sdk/presets"; |
| 15 | import { addOpenApiSpec } from "./atoms"; | 15 | import { detectedAuthenticationTemplates } from "../sdk/derive-auth"; |
| 16 | import { addOpenApiSpec, previewOpenApiSpec } from "./atoms"; | ||
| 16 | import { openApiWireAuthInput, templateFromPlacements } from "./auth-method-config"; | 17 | import { openApiWireAuthInput, templateFromPlacements } from "./auth-method-config"; |
| 17 | import { decodeOpenApiSpecOverrides } from "../sdk/spec-overrides"; | 18 | import { 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 OAuth | 28 | /** 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 row | 29 | * 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 |
| 30 | const 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. */ | ||
| 33 | const 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 the | 44 | * configuration: omitting `authenticationTemplate` and `baseUrl` tells the |
| 39 | * server to derive both from the document, exactly what the add page's | 45 | * 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. */ |
| 41 | function useOpenApiQuickAdd(): ( | 47 | function 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 to | 55 | const slug = slugifyNamespace(input.slug ?? input.name); |
| 50 | // the configuration screen (its editor renders the parse error) rather | 56 | 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.specOverrides | 58 | // 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's | 65 | } |
| 60 | // OAuth template (endpoints a spec cannot carry) and the registry's | 66 | const preset = presetForSpecUrl(presets, input.url); |
| 61 | // header pattern (GitHub's spec declares no security at all, but the | 67 | // The FULL add page's method policy, replicated exactly: a preset's |
| 62 | // registry knows a PAT goes in the Authorization header). When neither | 68 | // 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.authHeader | 81 | ...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 | ...(registryPlacement | 86 | : [], |
| 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 > 0 | 101 | 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 | ||
| 109 | const importAdd = () => import("./AddOpenApiIntegration"); | 146 | const 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 | ||
| 117 | export const createOpenApiIntegrationPlugin = ( | 154 | export 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 | ||
| 134 | export const openApiIntegrationPlugin: IntegrationPlugin = createOpenApiIntegrationPlugin(); | 177 | export const openApiIntegrationPlugin: IntegrationPlugin = createOpenApiIntegrationPlugin(); |