Registry surfaces can carry RFC 6902 JSON Patch for a spec; the picker passes it through and the OpenAPI add flow prefills its overrides editor with it, so the patch is visible and editable rather than applied invisibly. A preset's own overrides win, and a user's draft wins over both. First use: Neon's spec declares the Neon console's session cookies as security schemes — the registry's patch removes them, and the add flow now offers exactly the real Bearer method.
Unified · Split
| @@ -209,6 +209,10 @@ export interface IntegrationPlugin { · 208 unmodified lines | ||
| 209 | 209 | * still declare the right method — an authless MCP server is a fact the |
| 210 | 210 | * registry already knows. */ |
| 211 | 211 | readonly initialAuthKind?: string; |
| 212 | + /** JSON-encoded RFC 6902 patch the registry says to apply to the fetched | |
| 213 | + * spec — the registry's mechanism for improving a vendor's published | |
| 214 | + * document over time without hosting a fork. */ | |
| 215 | + readonly initialSpecOverrides?: string; | |
| 212 | 216 | }>; |
| 213 | 217 | /** Legacy full-page edit surface. No host renders this anymore — plugin |
| 214 | 218 | * configuration lives in the integration Edit sheet via `editSheet`. */ |
| @@ -173,6 +173,7 @@ export default function AddOpenApiIntegration(props: { · 172 unmodified lines | ||
| 173 | 173 | initialNamespace?: string; |
| 174 | 174 | initialAuthHeader?: string; |
| 175 | 175 | initialAuthNote?: string; |
| 176 | + initialSpecOverrides?: string; | |
| 176 | 177 | }) { |
| 177 | 178 | const integrationPlugins = useIntegrationPlugins(); |
| 178 | 179 | const openApiPlugin = integrationPlugins.find((plugin) => plugin.key === "openapi"); |
| @@ -184,7 +185,17 @@ export default function AddOpenApiIntegration(props: { · 5 unmodified lines | ||
| 184 | 185 | |
| 185 | 186 | const activePreset = resolveOpenApiPreset(openApiPresets, props.initialPreset, specUrl); |
| 186 | 187 | const presetSpecOverrides = decodeOpenApiSpecOverrides(activePreset?.specOverrides); |
| 187 | − const specOverridesText = specOverridesDraft ?? formatSpecOverridesText(presetSpecOverrides); | |
| 188 | + // Registry-declared overrides — how a vendor's published document gets | |
| 189 | + // improved without hosting a fork (Neon's console session cookies posing as | |
| 190 | + // security schemes). A preset's own overrides win; the user's draft wins | |
| 191 | + // over both, and the editor shows exactly what will apply. | |
| 192 | + const registrySpecOverrides = useMemo(() => { | |
| 193 | + if (!props.initialSpecOverrides) return undefined; | |
| 194 | + const parsed = parseSpecOverridesText(props.initialSpecOverrides); | |
| 195 | + return parsed.ok ? parsed.value : undefined; | |
| 196 | + }, [props.initialSpecOverrides]); | |
| 197 | + const specOverridesText = | |
| 198 | + specOverridesDraft ?? formatSpecOverridesText(presetSpecOverrides ?? registrySpecOverrides); | |
| 188 | 199 | |
| 189 | 200 | // After analysis |
| 190 | 201 | const [preview, setPreview] = useState<SpecPreviewSummary | null>(null); |
| @@ -54,6 +54,10 @@ export interface CatalogSurface { · 53 unmodified lines | ||
| 54 | 54 | readonly header?: string; |
| 55 | 55 | readonly note?: string; |
| 56 | 56 | }; |
| 57 | + /** RFC 6902 JSON Patch the registry says to apply to the fetched spec — | |
| 58 | + * how a vendor's published document gets improved without hosting a fork | |
| 59 | + * (e.g. Neon's console session cookies posing as security schemes). */ | |
| 60 | + readonly specOverrides?: readonly unknown[]; | |
| 57 | 61 | } |
| 58 | 62 | |
| 59 | 63 | export interface CatalogSearchEntry { |
| @@ -123,6 +127,7 @@ const SearchResponse = Schema.Struct({ · 63 unmodified lines | ||
| 123 | 127 | note: Schema.optional(Schema.String), |
| 124 | 128 | }), |
| 125 | 129 | ), |
| 130 | + specOverrides: Schema.optional(Schema.Array(Schema.Unknown)), | |
| 126 | 131 | }), |
| 127 | 132 | ), |
| 128 | 133 | ), |
| @@ -145,6 +150,9 @@ export const parseCatalogSearch = (payload: unknown): readonly CatalogSearchEntr · 16 unmodified lines | ||
| 145 | 150 | slug: surface.slug, |
| 146 | 151 | ...(surface.url ? { url: surface.url } : {}), |
| 147 | 152 | ...(surface.auth ? { auth: surface.auth } : {}), |
| 153 | + ...(surface.specOverrides && surface.specOverrides.length > 0 | |
| 154 | + ? { specOverrides: surface.specOverrides } | |
| 155 | + : {}), | |
| 148 | 156 | }, |
| 149 | 157 | ] |
| 150 | 158 | : [], |
| @@ -18,9 +18,11 @@ export function AddIntegrationPage(props: { · 17 unmodified lines | ||
| 18 | 18 | authHeader?: string; |
| 19 | 19 | authNote?: string; |
| 20 | 20 | authKind?: string; |
| 21 | + specOverrides?: string; | |
| 21 | 22 | }) { |
| 22 | 23 | useExecutorDocumentTitle("Add integration"); |
| 23 | − const { pluginKey, url, preset, namespace, authHeader, authNote, authKind } = props; | |
| 24 | + const { pluginKey, url, preset, namespace, authHeader, authNote, authKind, specOverrides } = | |
| 25 | + props; | |
| 24 | 26 | const navigate = useNavigate(); |
| 25 | 27 | const integrationPlugins = useIntegrationPlugins(); |
| 26 | 28 | const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom); |
| @@ -63,6 +65,7 @@ export function AddIntegrationPage(props: { · 36 unmodified lines | ||
| 63 | 65 | initialAuthHeader={authHeader} |
| 64 | 66 | initialAuthNote={authNote} |
| 65 | 67 | initialAuthKind={authKind} |
| 68 | + initialSpecOverrides={specOverrides} | |
| 66 | 69 | onComplete={(slug?: string) => { |
| 67 | 70 | trackEvent("integration_added", { |
| 68 | 71 | plugin_key: pluginKey, |
| @@ -420,6 +420,7 @@ export function IntegrationBrowsePage() { · 419 unmodified lines | ||
| 420 | 420 | readonly slug?: string; |
| 421 | 421 | readonly domain: string; |
| 422 | 422 | readonly auth?: CatalogSurface["auth"]; |
| 423 | + readonly specOverrides?: CatalogSurface["specOverrides"]; | |
| 423 | 424 | }) => { |
| 424 | 425 | trackEvent("integration_add_started", { |
| 425 | 426 | plugin_key: input.kind, |
| @@ -435,6 +436,7 @@ export function IntegrationBrowsePage() { · 9 unmodified lines | ||
| 435 | 436 | ...(input.auth?.header ? { authHeader: input.auth.header } : {}), |
| 436 | 437 | ...(input.auth?.note ? { authNote: input.auth.note } : {}), |
| 437 | 438 | ...(input.auth?.kind ? { authKind: input.auth.kind } : {}), |
| 439 | + ...(input.specOverrides ? { specOverrides: JSON.stringify(input.specOverrides) } : {}), | |
| 438 | 440 | }, |
| 439 | 441 | }); |
| 440 | 442 | }, |
| @@ -453,6 +455,7 @@ export function IntegrationBrowsePage() { · 12 unmodified lines | ||
| 453 | 455 | domain: entry.domain, |
| 454 | 456 | ...(surface ? { slug: surface.slug } : {}), |
| 455 | 457 | ...(surface?.auth ? { auth: surface.auth } : {}), |
| 458 | + ...(surface?.specOverrides ? { specOverrides: surface.specOverrides } : {}), | |
| 456 | 459 | }); |
| 457 | 460 | return; |
| 458 | 461 | } |
| @@ -477,7 +480,11 @@ export function IntegrationBrowsePage() { · 18 unmodified lines | ||
| 477 | 480 | ); |
| 478 | 481 | |
| 479 | 482 | const pickPreset = useCallback( |
| 480 | − (entry: PresetEntry, auth?: CatalogSurface["auth"]) => { | |
| 483 | + ( | |
| 484 | + entry: PresetEntry, | |
| 485 | + auth?: CatalogSurface["auth"], | |
| 486 | + specOverrides?: CatalogSurface["specOverrides"], | |
| 487 | + ) => { | |
| 481 | 488 | trackEvent("integration_add_started", { |
| 482 | 489 | plugin_key: entry.pluginKey, |
| 483 | 490 | via: "preset", |
| @@ -491,6 +498,7 @@ export function IntegrationBrowsePage() { · 7 unmodified lines | ||
| 491 | 498 | if (auth?.header) search.authHeader = auth.header; |
| 492 | 499 | if (auth?.note) search.authNote = auth.note; |
| 493 | 500 | if (auth?.kind) search.authKind = auth.kind; |
| 501 | + if (specOverrides) search.specOverrides = JSON.stringify(specOverrides); | |
| 494 | 502 | void navigate({ |
| 495 | 503 | to: "/{-$orgSlug}/integrations/add/$pluginKey", |
| 496 | 504 | params: { pluginKey: entry.pluginKey }, |
| @@ -510,7 +518,11 @@ export function IntegrationBrowsePage() { · 13 unmodified lines | ||
| 510 | 518 | const catalogIdentityByKey = useMemo(() => { |
| 511 | 519 | const map = new Map< |
| 512 | 520 | string, |
| 513 | − { readonly domain: string; readonly auth?: CatalogSurface["auth"] } | |
| 521 | + { | |
| 522 | + readonly domain: string; | |
| 523 | + readonly auth?: CatalogSurface["auth"]; | |
| 524 | + readonly specOverrides?: CatalogSurface["specOverrides"]; | |
| 525 | + } | |
| 514 | 526 | >(); |
| 515 | 527 | for (const entry of catalogEntries) { |
| 516 | 528 | const pretty = entry.name ?? domainDisplayName(entry.domain); |
| @@ -524,6 +536,9 @@ export function IntegrationBrowsePage() { · 7 unmodified lines | ||
| 524 | 536 | map.set(key, { |
| 525 | 537 | domain: entry.domain, |
| 526 | 538 | ...("auth" in surface && surface.auth ? { auth: surface.auth } : {}), |
| 539 | + ...("specOverrides" in surface && surface.specOverrides | |
| 540 | + ? { specOverrides: surface.specOverrides } | |
| 541 | + : {}), | |
| 527 | 542 | }); |
| 528 | 543 | } |
| 529 | 544 | } |
| @@ -552,7 +567,7 @@ export function IntegrationBrowsePage() { · 22 unmodified lines | ||
| 552 | 567 | ...(identity ? { domain: identity.domain } : {}), |
| 553 | 568 | ...(entry.preset.summary ? { description: entry.preset.summary } : {}), |
| 554 | 569 | ...(entry.preset.icon ? { iconUrl: entry.preset.icon } : {}), |
| 555 | − onSelect: () => pickPreset(entry, identity?.auth), | |
| 570 | + onSelect: () => pickPreset(entry, identity?.auth, identity?.specOverrides), | |
| 556 | 571 | added: isAdded(entry.pluginKey, entry.preset.defaultSlug, entry.preset.name), |
| 557 | 572 | busy: false, |
| 558 | 573 | }); |
| @@ -14,6 +14,8 @@ const SearchParams = Schema.toStandardSchemaV1( · 13 unmodified lines | ||
| 14 | 14 | authHeader: Schema.optional(Schema.String), |
| 15 | 15 | authNote: Schema.optional(Schema.String), |
| 16 | 16 | authKind: Schema.optional(Schema.String), |
| 17 | + // JSON-encoded RFC 6902 patch the registry says to apply to the spec. | |
| 18 | + specOverrides: Schema.optional(Schema.String), | |
| 17 | 19 | }), |
| 18 | 20 | ); |
| 19 | 21 | |
| @@ -21,7 +23,8 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 1 unmodified line | ||
| 21 | 23 | validateSearch: SearchParams, |
| 22 | 24 | component: () => { |
| 23 | 25 | const { pluginKey } = Route.useParams(); |
| 24 | − const { url, preset, namespace, authHeader, authNote, authKind } = Route.useSearch(); | |
| 26 | + const { url, preset, namespace, authHeader, authNote, authKind, specOverrides } = | |
| 27 | + Route.useSearch(); | |
| 25 | 28 | return ( |
| 26 | 29 | <AddIntegrationPage |
| 27 | 30 | pluginKey={pluginKey} |
| @@ -31,6 +34,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 3 unmodified lines | ||
| 31 | 34 | authHeader={authHeader} |
| 32 | 35 | authNote={authNote} |
| 33 | 36 | authKind={authKind} |
| 37 | + specOverrides={specOverrides} | |
| 34 | 38 | /> |
| 35 | 39 | ); |
| 36 | 40 | }, |