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 | * still declare the right method — an authless MCP server is a fact the | 209 | * still declare the right method — an authless MCP server is a fact the |
| 210 | * registry already knows. */ | 210 | * registry already knows. */ |
| 211 | readonly initialAuthKind?: string; | 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 | /** Legacy full-page edit surface. No host renders this anymore — plugin | 217 | /** Legacy full-page edit surface. No host renders this anymore — plugin |
| 214 | * configuration lives in the integration Edit sheet via `editSheet`. */ | 218 | * configuration lives in the integration Edit sheet via `editSheet`. */ |
| @@ -173,6 +173,7 @@ export default function AddOpenApiIntegration(props: { · 172 unmodified lines | |||
| 173 | initialNamespace?: string; | 173 | initialNamespace?: string; |
| 174 | initialAuthHeader?: string; | 174 | initialAuthHeader?: string; |
| 175 | initialAuthNote?: string; | 175 | initialAuthNote?: string; |
| 176 | initialSpecOverrides?: string; | ||
| 176 | }) { | 177 | }) { |
| 177 | const integrationPlugins = useIntegrationPlugins(); | 178 | const integrationPlugins = useIntegrationPlugins(); |
| 178 | const openApiPlugin = integrationPlugins.find((plugin) => plugin.key === "openapi"); | 179 | const openApiPlugin = integrationPlugins.find((plugin) => plugin.key === "openapi"); |
| @@ -184,7 +185,17 @@ export default function AddOpenApiIntegration(props: { · 5 unmodified lines | |||
| 184 | 185 | ||
| 185 | const activePreset = resolveOpenApiPreset(openApiPresets, props.initialPreset, specUrl); | 186 | const activePreset = resolveOpenApiPreset(openApiPresets, props.initialPreset, specUrl); |
| 186 | const presetSpecOverrides = decodeOpenApiSpecOverrides(activePreset?.specOverrides); | 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 | // After analysis | 200 | // After analysis |
| 190 | const [preview, setPreview] = useState<SpecPreviewSummary | null>(null); | 201 | const [preview, setPreview] = useState<SpecPreviewSummary | null>(null); |
| @@ -54,6 +54,10 @@ export interface CatalogSurface { · 53 unmodified lines | |||
| 54 | readonly header?: string; | 54 | readonly header?: string; |
| 55 | readonly note?: string; | 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 | export interface CatalogSearchEntry { | 63 | export interface CatalogSearchEntry { |
| @@ -123,6 +127,7 @@ const SearchResponse = Schema.Struct({ · 63 unmodified lines | |||
| 123 | note: Schema.optional(Schema.String), | 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 | slug: surface.slug, | 150 | slug: surface.slug, |
| 146 | ...(surface.url ? { url: surface.url } : {}), | 151 | ...(surface.url ? { url: surface.url } : {}), |
| 147 | ...(surface.auth ? { auth: surface.auth } : {}), | 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 | authHeader?: string; | 18 | authHeader?: string; |
| 19 | authNote?: string; | 19 | authNote?: string; |
| 20 | authKind?: string; | 20 | authKind?: string; |
| 21 | specOverrides?: string; | ||
| 21 | }) { | 22 | }) { |
| 22 | useExecutorDocumentTitle("Add integration"); | 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 | const navigate = useNavigate(); | 26 | const navigate = useNavigate(); |
| 25 | const integrationPlugins = useIntegrationPlugins(); | 27 | const integrationPlugins = useIntegrationPlugins(); |
| 26 | const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom); | 28 | const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom); |
| @@ -63,6 +65,7 @@ export function AddIntegrationPage(props: { · 36 unmodified lines | |||
| 63 | initialAuthHeader={authHeader} | 65 | initialAuthHeader={authHeader} |
| 64 | initialAuthNote={authNote} | 66 | initialAuthNote={authNote} |
| 65 | initialAuthKind={authKind} | 67 | initialAuthKind={authKind} |
| 68 | initialSpecOverrides={specOverrides} | ||
| 66 | onComplete={(slug?: string) => { | 69 | onComplete={(slug?: string) => { |
| 67 | trackEvent("integration_added", { | 70 | trackEvent("integration_added", { |
| 68 | plugin_key: pluginKey, | 71 | plugin_key: pluginKey, |
| @@ -420,6 +420,7 @@ export function IntegrationBrowsePage() { · 419 unmodified lines | |||
| 420 | readonly slug?: string; | 420 | readonly slug?: string; |
| 421 | readonly domain: string; | 421 | readonly domain: string; |
| 422 | readonly auth?: CatalogSurface["auth"]; | 422 | readonly auth?: CatalogSurface["auth"]; |
| 423 | readonly specOverrides?: CatalogSurface["specOverrides"]; | ||
| 423 | }) => { | 424 | }) => { |
| 424 | trackEvent("integration_add_started", { | 425 | trackEvent("integration_add_started", { |
| 425 | plugin_key: input.kind, | 426 | plugin_key: input.kind, |
| @@ -435,6 +436,7 @@ export function IntegrationBrowsePage() { · 9 unmodified lines | |||
| 435 | ...(input.auth?.header ? { authHeader: input.auth.header } : {}), | 436 | ...(input.auth?.header ? { authHeader: input.auth.header } : {}), |
| 436 | ...(input.auth?.note ? { authNote: input.auth.note } : {}), | 437 | ...(input.auth?.note ? { authNote: input.auth.note } : {}), |
| 437 | ...(input.auth?.kind ? { authKind: input.auth.kind } : {}), | 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 | domain: entry.domain, | 455 | domain: entry.domain, |
| 454 | ...(surface ? { slug: surface.slug } : {}), | 456 | ...(surface ? { slug: surface.slug } : {}), |
| 455 | ...(surface?.auth ? { auth: surface.auth } : {}), | 457 | ...(surface?.auth ? { auth: surface.auth } : {}), |
| 458 | ...(surface?.specOverrides ? { specOverrides: surface.specOverrides } : {}), | ||
| 456 | }); | 459 | }); |
| 457 | return; | 460 | return; |
| 458 | } | 461 | } |
| @@ -477,7 +480,11 @@ export function IntegrationBrowsePage() { · 18 unmodified lines | |||
| 477 | ); | 480 | ); |
| 478 | 481 | ||
| 479 | const pickPreset = useCallback( | 482 | const pickPreset = useCallback( |
| 480 | (entry: PresetEntry, auth?: CatalogSurface["auth"]) => { | 483 | ( |
| 484 | entry: PresetEntry, | ||
| 485 | auth?: CatalogSurface["auth"], | ||
| 486 | specOverrides?: CatalogSurface["specOverrides"], | ||
| 487 | ) => { | ||
| 481 | trackEvent("integration_add_started", { | 488 | trackEvent("integration_add_started", { |
| 482 | plugin_key: entry.pluginKey, | 489 | plugin_key: entry.pluginKey, |
| 483 | via: "preset", | 490 | via: "preset", |
| @@ -491,6 +498,7 @@ export function IntegrationBrowsePage() { · 7 unmodified lines | |||
| 491 | if (auth?.header) search.authHeader = auth.header; | 498 | if (auth?.header) search.authHeader = auth.header; |
| 492 | if (auth?.note) search.authNote = auth.note; | 499 | if (auth?.note) search.authNote = auth.note; |
| 493 | if (auth?.kind) search.authKind = auth.kind; | 500 | if (auth?.kind) search.authKind = auth.kind; |
| 501 | if (specOverrides) search.specOverrides = JSON.stringify(specOverrides); | ||
| 494 | void navigate({ | 502 | void navigate({ |
| 495 | to: "/{-$orgSlug}/integrations/add/$pluginKey", | 503 | to: "/{-$orgSlug}/integrations/add/$pluginKey", |
| 496 | params: { pluginKey: entry.pluginKey }, | 504 | params: { pluginKey: entry.pluginKey }, |
| @@ -510,7 +518,11 @@ export function IntegrationBrowsePage() { · 13 unmodified lines | |||
| 510 | const catalogIdentityByKey = useMemo(() => { | 518 | const catalogIdentityByKey = useMemo(() => { |
| 511 | const map = new Map< | 519 | const map = new Map< |
| 512 | string, | 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 | for (const entry of catalogEntries) { | 527 | for (const entry of catalogEntries) { |
| 516 | const pretty = entry.name ?? domainDisplayName(entry.domain); | 528 | const pretty = entry.name ?? domainDisplayName(entry.domain); |
| @@ -524,6 +536,9 @@ export function IntegrationBrowsePage() { · 7 unmodified lines | |||
| 524 | map.set(key, { | 536 | map.set(key, { |
| 525 | domain: entry.domain, | 537 | domain: entry.domain, |
| 526 | ...("auth" in surface && surface.auth ? { auth: surface.auth } : {}), | 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 | ...(identity ? { domain: identity.domain } : {}), | 567 | ...(identity ? { domain: identity.domain } : {}), |
| 553 | ...(entry.preset.summary ? { description: entry.preset.summary } : {}), | 568 | ...(entry.preset.summary ? { description: entry.preset.summary } : {}), |
| 554 | ...(entry.preset.icon ? { iconUrl: entry.preset.icon } : {}), | 569 | ...(entry.preset.icon ? { iconUrl: entry.preset.icon } : {}), |
| 555 | onSelect: () => pickPreset(entry, identity?.auth), | 570 | onSelect: () => pickPreset(entry, identity?.auth, identity?.specOverrides), |
| 556 | added: isAdded(entry.pluginKey, entry.preset.defaultSlug, entry.preset.name), | 571 | added: isAdded(entry.pluginKey, entry.preset.defaultSlug, entry.preset.name), |
| 557 | busy: false, | 572 | busy: false, |
| 558 | }); | 573 | }); |
| @@ -14,6 +14,8 @@ const SearchParams = Schema.toStandardSchemaV1( · 13 unmodified lines | |||
| 14 | authHeader: Schema.optional(Schema.String), | 14 | authHeader: Schema.optional(Schema.String), |
| 15 | authNote: Schema.optional(Schema.String), | 15 | authNote: Schema.optional(Schema.String), |
| 16 | authKind: Schema.optional(Schema.String), | 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 | validateSearch: SearchParams, | 23 | validateSearch: SearchParams, |
| 22 | component: () => { | 24 | component: () => { |
| 23 | const { pluginKey } = Route.useParams(); | 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 | return ( | 28 | return ( |
| 26 | <AddIntegrationPage | 29 | <AddIntegrationPage |
| 27 | pluginKey={pluginKey} | 30 | pluginKey={pluginKey} |
| @@ -31,6 +34,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 3 unmodified lines | |||
| 31 | authHeader={authHeader} | 34 | authHeader={authHeader} |
| 32 | authNote={authNote} | 35 | authNote={authNote} |
| 33 | authKind={authKind} | 36 | authKind={authKind} |
| 37 | specOverrides={specOverrides} | ||
| 34 | /> | 38 | /> |
| 35 | ); | 39 | ); |
| 36 | }, | 40 | }, |