History

Prefill spec overrides from the registry

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.

Rhys Sullivan committed 2 months ago

Commit 12ae0911 parent 5d8aadd6 files · +51 −6Browse files

Unified · Split

6 files
  1. packages/core/sdk/src/client.ts +4 −0
  2. packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx +12 −1
  3. packages/react/src/lib/integrations-sh-catalog.ts +8 −0
  4. packages/react/src/pages/integration-add.tsx +4 −1
  5. packages/react/src/pages/integration-browse.tsx +18 −3
  6. packages/react/src/routes/integrations.add.$pluginKey.tsx +5 −1
packages/core/sdk/src/client.ts+4−0
@@ -209,6 +209,10 @@ export interface IntegrationPlugin { · 208 unmodified lines
209 * still declare the right method — an authless MCP server is a fact the209 * 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 — plugin217 /** 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`. */
packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx+12−1
@@ -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 analysis200 // After analysis
190 const [preview, setPreview] = useState<SpecPreviewSummary | null>(null);201 const [preview, setPreview] = useState<SpecPreviewSummary | null>(null);
packages/react/src/lib/integrations-sh-catalog.ts+8−0
@@ -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
59export interface CatalogSearchEntry {63export 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 : [],
packages/react/src/pages/integration-add.tsx+4−1
@@ -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,
packages/react/src/pages/integration-browse.tsx+18−3
@@ -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 });
packages/react/src/routes/integrations.add.$pluginKey.tsx+5−1
@@ -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 <AddIntegrationPage29 <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 },

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