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
209209 * still declare the right method — an authless MCP server is a fact the
210210 * registry already knows. */
211211 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;
212216 }>;
213217 /** Legacy full-page edit surface. No host renders this anymore — plugin
214218 * 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
173173 initialNamespace?: string;
174174 initialAuthHeader?: string;
175175 initialAuthNote?: string;
176+ initialSpecOverrides?: string;
176177 }) {
177178 const integrationPlugins = useIntegrationPlugins();
178179 const openApiPlugin = integrationPlugins.find((plugin) => plugin.key === "openapi");
@@ -184,7 +185,17 @@ export default function AddOpenApiIntegration(props: { · 5 unmodified lines
184185
185186 const activePreset = resolveOpenApiPreset(openApiPresets, props.initialPreset, specUrl);
186187 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);
188199
189200 // After analysis
190201 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
5454 readonly header?: string;
5555 readonly note?: string;
5656 };
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[];
5761 }
5862
5963 export interface CatalogSearchEntry {
@@ -123,6 +127,7 @@ const SearchResponse = Schema.Struct({ · 63 unmodified lines
123127 note: Schema.optional(Schema.String),
124128 }),
125129 ),
130+ specOverrides: Schema.optional(Schema.Array(Schema.Unknown)),
126131 }),
127132 ),
128133 ),
@@ -145,6 +150,9 @@ export const parseCatalogSearch = (payload: unknown): readonly CatalogSearchEntr · 16 unmodified lines
145150 slug: surface.slug,
146151 ...(surface.url ? { url: surface.url } : {}),
147152 ...(surface.auth ? { auth: surface.auth } : {}),
153+ ...(surface.specOverrides && surface.specOverrides.length > 0
154+ ? { specOverrides: surface.specOverrides }
155+ : {}),
148156 },
149157 ]
150158 : [],
packages/react/src/pages/integration-add.tsx+4−1
@@ -18,9 +18,11 @@ export function AddIntegrationPage(props: { · 17 unmodified lines
1818 authHeader?: string;
1919 authNote?: string;
2020 authKind?: string;
21+ specOverrides?: string;
2122 }) {
2223 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;
2426 const navigate = useNavigate();
2527 const integrationPlugins = useIntegrationPlugins();
2628 const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom);
@@ -63,6 +65,7 @@ export function AddIntegrationPage(props: { · 36 unmodified lines
6365 initialAuthHeader={authHeader}
6466 initialAuthNote={authNote}
6567 initialAuthKind={authKind}
68+ initialSpecOverrides={specOverrides}
6669 onComplete={(slug?: string) => {
6770 trackEvent("integration_added", {
6871 plugin_key: pluginKey,
packages/react/src/pages/integration-browse.tsx+18−3
@@ -420,6 +420,7 @@ export function IntegrationBrowsePage() { · 419 unmodified lines
420420 readonly slug?: string;
421421 readonly domain: string;
422422 readonly auth?: CatalogSurface["auth"];
423+ readonly specOverrides?: CatalogSurface["specOverrides"];
423424 }) => {
424425 trackEvent("integration_add_started", {
425426 plugin_key: input.kind,
@@ -435,6 +436,7 @@ export function IntegrationBrowsePage() { · 9 unmodified lines
435436 ...(input.auth?.header ? { authHeader: input.auth.header } : {}),
436437 ...(input.auth?.note ? { authNote: input.auth.note } : {}),
437438 ...(input.auth?.kind ? { authKind: input.auth.kind } : {}),
439+ ...(input.specOverrides ? { specOverrides: JSON.stringify(input.specOverrides) } : {}),
438440 },
439441 });
440442 },
@@ -453,6 +455,7 @@ export function IntegrationBrowsePage() { · 12 unmodified lines
453455 domain: entry.domain,
454456 ...(surface ? { slug: surface.slug } : {}),
455457 ...(surface?.auth ? { auth: surface.auth } : {}),
458+ ...(surface?.specOverrides ? { specOverrides: surface.specOverrides } : {}),
456459 });
457460 return;
458461 }
@@ -477,7 +480,11 @@ export function IntegrationBrowsePage() { · 18 unmodified lines
477480 );
478481
479482 const pickPreset = useCallback(
480− (entry: PresetEntry, auth?: CatalogSurface["auth"]) => {
483+ (
484+ entry: PresetEntry,
485+ auth?: CatalogSurface["auth"],
486+ specOverrides?: CatalogSurface["specOverrides"],
487+ ) => {
481488 trackEvent("integration_add_started", {
482489 plugin_key: entry.pluginKey,
483490 via: "preset",
@@ -491,6 +498,7 @@ export function IntegrationBrowsePage() { · 7 unmodified lines
491498 if (auth?.header) search.authHeader = auth.header;
492499 if (auth?.note) search.authNote = auth.note;
493500 if (auth?.kind) search.authKind = auth.kind;
501+ if (specOverrides) search.specOverrides = JSON.stringify(specOverrides);
494502 void navigate({
495503 to: "/{-$orgSlug}/integrations/add/$pluginKey",
496504 params: { pluginKey: entry.pluginKey },
@@ -510,7 +518,11 @@ export function IntegrationBrowsePage() { · 13 unmodified lines
510518 const catalogIdentityByKey = useMemo(() => {
511519 const map = new Map<
512520 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+ }
514526 >();
515527 for (const entry of catalogEntries) {
516528 const pretty = entry.name ?? domainDisplayName(entry.domain);
@@ -524,6 +536,9 @@ export function IntegrationBrowsePage() { · 7 unmodified lines
524536 map.set(key, {
525537 domain: entry.domain,
526538 ...("auth" in surface && surface.auth ? { auth: surface.auth } : {}),
539+ ...("specOverrides" in surface && surface.specOverrides
540+ ? { specOverrides: surface.specOverrides }
541+ : {}),
527542 });
528543 }
529544 }
@@ -552,7 +567,7 @@ export function IntegrationBrowsePage() { · 22 unmodified lines
552567 ...(identity ? { domain: identity.domain } : {}),
553568 ...(entry.preset.summary ? { description: entry.preset.summary } : {}),
554569 ...(entry.preset.icon ? { iconUrl: entry.preset.icon } : {}),
555− onSelect: () => pickPreset(entry, identity?.auth),
570+ onSelect: () => pickPreset(entry, identity?.auth, identity?.specOverrides),
556571 added: isAdded(entry.pluginKey, entry.preset.defaultSlug, entry.preset.name),
557572 busy: false,
558573 });
packages/react/src/routes/integrations.add.$pluginKey.tsx+5−1
@@ -14,6 +14,8 @@ const SearchParams = Schema.toStandardSchemaV1( · 13 unmodified lines
1414 authHeader: Schema.optional(Schema.String),
1515 authNote: Schema.optional(Schema.String),
1616 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),
1719 }),
1820 );
1921
@@ -21,7 +23,8 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 1 unmodified line
2123 validateSearch: SearchParams,
2224 component: () => {
2325 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();
2528 return (
2629 <AddIntegrationPage
2730 pluginKey={pluginKey}
@@ -31,6 +34,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 3 unmodified lines
3134 authHeader={authHeader}
3235 authNote={authNote}
3336 authKind={authKind}
37+ specOverrides={specOverrides}
3438 />
3539 );
3640 },

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