History

Registry auth for OpenAPI and preset add flows; product-key matching

GitHub's official OpenAPI description declares no securitySchemes, so the add flow offered OAuth (from the preset template) and nothing else, while a PAT bearer header is how most calls actually authenticate. The registry's declared placement now seeds an API key method alongside detected OAuth; a spec-declared key method still wins over the registry's version of the same fact. Preset-to-catalog matching moves from rendered titles to a product key (normalized name plus kind), so 'GitHub REST' and the registry's 'GitHub API' count as one product — titles and domains both proved unreliable joins. MCP surfaces carry their auth facts too.

Rhys Sullivan committed 2 months ago

Commit 97c4ef41 parent d3acfe32 files · +50 −16Browse files

Unified · Split

2 files
  1. packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx +21 −2
  2. packages/react/src/pages/integration-browse.tsx +29 −14
packages/plugins/openapi/src/react/AddOpenApiIntegration.tsx+21−2
@@ -25,6 +25,7 @@ import { · 24 unmodified lines
2525 type AuthMethodRow,
2626 type AuthMethodSeed,
2727 } from "@executor-js/react/components/auth-method-list-editor";
28+import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements";
2829 import { CardStack, CardStackContent } from "@executor-js/react/components/card-stack";
2930 import { FieldLabel } from "@executor-js/react/components/field";
3031 import { FloatActions } from "@executor-js/react/components/float-actions";
@@ -170,6 +171,8 @@ export default function AddOpenApiIntegration(props: { · 139 unmodified lines
170171 initialUrl?: string;
171172 initialPreset?: string;
172173 initialNamespace?: string;
174+ initialAuthHeader?: string;
175+ initialAuthNote?: string;
173176 }) {
174177 const integrationPlugins = useIntegrationPlugins();
175178 const openApiPlugin = integrationPlugins.find((plugin) => plugin.key === "openapi");
@@ -297,14 +300,26 @@ export default function AddOpenApiIntegration(props: { · 121 unmodified lines
297300 ...(preview?.headerPresets ?? []).map((preset) => preset.label),
298301 ...(preview?.oauth2Presets ?? []).map((preset) => preset.label),
299302 ];
300− return authenticationTemplate.map(
303+ const detected = authenticationTemplate.map(
301304 (template: Authentication, index: number): AuthMethodSeed => ({
302305 value: editorValueFromAuthentication(template),
303306 slug: String(template.slug),
304307 ...(labels[index] !== undefined ? { label: labels[index] } : {}),
305308 }),
306309 );
307− }, [preview, authenticationTemplate]);
310+ if (preview === null) return detected;
311+ // The registry's declared credential placement fills what the document
312+ // leaves out. GitHub's official description famously declares no
313+ // securitySchemes at all, and even with the OAuth preset template a PAT
314+ // bearer header is how most calls actually authenticate — so the key
315+ // method is offered alongside OAuth. A spec-declared key method wins over
316+ // the registry's version of the same fact.
317+ const placement = props.initialAuthHeader
318+ ? placementFromHeaderPattern(props.initialAuthHeader)
319+ : null;
320+ if (!placement || detected.some((seed) => seed.value.kind === "apikey")) return detected;
321+ return [...detected, { value: { kind: "apikey", placements: [placement] } }];
322+ }, [preview, authenticationTemplate, props.initialAuthHeader]);
308323 const authMethodList = useAuthMethodList(authMethodSeeds);
309324
310325 // The methods to register, mapped back to stored `Authentication[]`. Drops
@@ -601,6 +616,10 @@ export default function AddOpenApiIntegration(props: { · 290 unmodified lines
601616 />
602617 )}
603618
619+ {preview && props.initialAuthNote ? (
620+ <p className="text-xs leading-relaxed text-muted-foreground">{props.initialAuthNote}</p>
621+ ) : null}
622+
604623 {preview ? (
605624 <AddOpenApiHealthCheckSection
606625 presetHealthCheck={activePreset?.healthCheck}
packages/react/src/pages/integration-browse.tsx+29−14
@@ -98,6 +98,16 @@ const withSurface = (name: string, surface: string): string => { · 97 unmodified lines
9898 return already ? name : `${name} ${surface}`;
9999 };
100100
101+/** The key on which a preset and a catalog row count as the same product:
102+ * the name with surface words and punctuation stripped, plus the kind.
103+ * "GitHub REST" and "GitHub API" are one product; "Stripe API" and
104+ * "Stripe MCP" are not. */
105+const productKey = (name: string, kind: string): string =>
106+ `${name
107+ .toLowerCase()
108+ .replace(/\b(api|rest|graphql|mcp)\b/g, "")
109+ .replace(/[^a-z0-9]/g, "")}|${kind}`;
110+
101111 const detectionRank: Record<IntegrationDetectionResult["confidence"], number> = {
102112 high: 3,
103113 medium: 2,
@@ -151,6 +161,8 @@ interface Row { · 47 unmodified lines
151161 readonly key: string;
152162 readonly testId: string;
153163 readonly title: string;
164+ /** Surface kind for product matching (productKey); not rendered. */
165+ readonly kindKey: string;
154166 /** The domain, shown beside a prettified title so near-namesakes stay
155167 * distinguishable. Omitted when the title is already the domain. */
156168 readonly domain?: string;
@@ -492,8 +504,8 @@ export function IntegrationBrowsePage() { · 335 unmodified lines
492504 // catalog row for the same product must not read as two different kinds of
493505 // thing — the row shows the registry's identity (the domain) like every
494506 // other result, and the preset only supplies the better add flow. Keyed by
495− // the rendered title, the same match the dedupe below uses.
496− const catalogIdentityByTitle = useMemo(() => {
507+ // productKey, the same match the dedupe below uses.
508+ const catalogIdentityByKey = useMemo(() => {
497509 const map = new Map<
498510 string,
499511 { readonly domain: string; readonly auth?: CatalogSurface["auth"] }
@@ -505,10 +517,9 @@ export function IntegrationBrowsePage() { · 5 unmodified lines
505517 ? entry.surfaces
506518 : entry.kinds.map((kind) => ({ kind }));
507519 for (const surface of surfaces) {
508− const word = SURFACE_WORD[surface.kind] ?? CATALOG_KIND_LABEL[surface.kind];
509− const title = withSurface(pretty, word).toLowerCase();
510− if (!map.has(title)) {
511− map.set(title, {
520+ const key = productKey(pretty, surface.kind);
521+ if (!map.has(key)) {
522+ map.set(key, {
512523 domain: entry.domain,
513524 ...("auth" in surface && surface.auth ? { auth: surface.auth } : {}),
514525 });
@@ -529,11 +540,13 @@ export function IntegrationBrowsePage() { · 14 unmodified lines
529540 }
530541 const surface = SURFACE_WORD[entry.pluginKey] ?? entry.pluginLabel;
531542 const title = withSurface(entry.preset.name, surface);
532− const identity = catalogIdentityByTitle.get(title.toLowerCase());
543+ const kindKey = entry.pluginKey === "google" ? "openapi" : entry.pluginKey;
544+ const identity = catalogIdentityByKey.get(productKey(entry.preset.name, kindKey));
533545 rows.push({
534546 key: `preset-${entry.pluginKey}-${entry.preset.id}`,
535547 testId: `preset-${entry.preset.id}`,
536548 title,
549+ kindKey,
537550 ...(identity ? { domain: identity.domain } : {}),
538551 ...(entry.preset.summary ? { description: entry.preset.summary } : {}),
539552 ...(entry.preset.icon ? { iconUrl: entry.preset.icon } : {}),
@@ -543,7 +556,7 @@ export function IntegrationBrowsePage() { · 3 unmodified lines
543556 });
544557 }
545558 return rows;
546− }, [allPresets, kind, text, pickPreset, isAdded, catalogIdentityByTitle]);
559+ }, [allPresets, kind, text, pickPreset, isAdded, catalogIdentityByKey]);
547560
548561 // --- Catalog rows: one per (service, surface) -----------------------------
549562 const catalogRows = useMemo<readonly Row[]>(() => {
@@ -567,6 +580,7 @@ export function IntegrationBrowsePage() { · 17 unmodified lines
567580 key: `catalog-${entry.domain}-${surface.kind}-${known?.slug ?? pretty}`,
568581 testId: `catalog-${known?.slug ?? `${entry.domain}-${surface.kind}`}`,
569582 title: withSurface(pretty, word),
583+ kindKey: surface.kind,
570584 domain: entry.domain,
571585 ...(description ? { description } : {}),
572586 iconUrl: catalogLogoUrl(entry.domain, 10),
@@ -577,12 +591,13 @@ export function IntegrationBrowsePage() { · 4 unmodified lines
577591 });
578592 });
579593 // A registry row for something a preset already offers is a worse copy of
580− // it: same service, no auth template, no health check. Matched on the
581− // rendered title because a preset's domain is inferred from its icon and
582− // often does not match the registry's (the Gmail preset's icon is Google's,
583− // so a domain comparison let a second "Gmail API" through).
584− const presetTitles = new Set(presetRows.map((row) => row.title.toLowerCase()));
585− rows = rows.filter((row) => !presetTitles.has(row.title.toLowerCase()));
594+ // it: same service, no auth template, no health check. Matched on
595+ // productKey — normalized name plus kind — because neither titles nor
596+ // domains line up reliably: the Gmail preset's icon is Google's (so a
597+ // domain comparison let a second "Gmail API" through), and the GitHub
598+ // preset says "GitHub REST" where the registry row says "GitHub API".
599+ const presetKeys = new Set(presetRows.map((row) => productKey(row.title, row.kindKey)));
600+ rows = rows.filter((row) => !presetKeys.has(productKey(row.title, row.kindKey)));
586601 if (text.length === 0) return rows;
587602 // A name match beats a mention in the blurb: searching "gmail" should not
588603 // rank a CRM that merely describes itself as living inside Gmail above

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