History

Quick add pulls every knowledge source; rate the AI guidance

The GitHub registry row quick-added with no auth methods: its spec declares no security at all, the preset's OAuth endpoints only rode the preset path, and the registry's PAT header pattern was dropped. The OpenAPI quick add now pulls both across — a URL-matched preset contributes its OAuth template, spec overrides, spec format, family, and health check; the registry's header pattern becomes a declared API-key method — while spec-derived defaults still apply when neither source knows anything. The AI-generated guidance line also gains thumbs: votes land in analytics keyed by domain and credential label, so wrong registry guidance is findable instead of silently misleading.

Rhys Sullivan committed 2 months ago

Commit 6e3cd061 parent 797c4863 files · +124 −16Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/react/integration-plugin.ts +57 −2
  2. packages/react/src/api/analytics.tsx +7 −0
  3. packages/react/src/components/credential-guidance.tsx +60 −14
packages/plugins/openapi/src/react/integration-plugin.ts+57−2
@@ -7,12 +7,33 @@ import type { · 6 unmodified lines
7 IntegrationQuickAddInput,7 IntegrationQuickAddInput,
8 IntegrationQuickAddResult,8 IntegrationQuickAddResult,
9} from "@executor-js/sdk/client";9} from "@executor-js/sdk/client";
10import { AuthTemplateSlug } from "@executor-js/sdk/shared";
10import { slugifyNamespace } from "@executor-js/react/plugins/integration-identity";11import { slugifyNamespace } from "@executor-js/react/plugins/integration-identity";
12import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements";
11import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";13import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
12import { openApiPresets } from "../sdk/presets";14import { openApiPresets, type OpenApiPreset } from "../sdk/presets";
13import { addOpenApiSpec } from "./atoms";15import { addOpenApiSpec } from "./atoms";
16import { openApiWireAuthInput, templateFromPlacements } from "./auth-method-config";
14import { decodeOpenApiSpecOverrides } from "../sdk/spec-overrides";17import { decodeOpenApiSpecOverrides } from "../sdk/spec-overrides";
15 18
19const normalizedSpecUrl = (url: string): string => {
20 if (!URL.canParse(url)) return url.trim().replace(/\/$/, "");
21 const parsed = new URL(url);
22 parsed.hash = "";
23 parsed.searchParams.sort();
24 return parsed.toString().replace(/\/$/, "");
25};
26
27/** The preset table still knows things no spec can say — GitHub's OAuth
28 * endpoints against a spec that declares NO security at all. A registry row
29 * whose URL is a preset's URL gets that knowledge pulled across. */
30const presetForSpecUrl = (url: string): OpenApiPreset | undefined => {
31 const target = normalizedSpecUrl(url);
32 return openApiPresets.find(
33 (preset) => preset.url !== undefined && normalizedSpecUrl(preset.url) === target,
34 );
35};
36
16/** One-click add for a registry OpenAPI row. The spec itself is the37/** One-click add for a registry OpenAPI row. The spec itself is the
17 * configuration: omitting `authenticationTemplate` and `baseUrl` tells the38 * configuration: omitting `authenticationTemplate` and `baseUrl` tells the
18 * server to derive both from the document, exactly what the add page's39 * server to derive both from the document, exactly what the add page's
@@ -34,12 +55,46 @@ function useOpenApiQuickAdd(): ( · 15 unmodified lines
34 if (input.specOverrides && input.specOverrides.length > 0 && specOverrides === undefined) {55 if (input.specOverrides && input.specOverrides.length > 0 && specOverrides === undefined) {
35 return { ok: false, reason: "unparseable spec overrides" };56 return { ok: false, reason: "unparseable spec overrides" };
36 }57 }
58 const preset = presetForSpecUrl(input.url);
59 // Declared methods travel from BOTH knowledge sources: the preset's
60 // OAuth template (endpoints a spec cannot carry) and the registry's
61 // header pattern (GitHub's spec declares no security at all, but the
62 // registry knows a PAT goes in the Authorization header). When neither
63 // knows anything, omitting the field lets the spec's own schemes derive.
64 const presetMethods = (preset?.authTemplate ?? []).flatMap((template) =>
65 template.kind === "oauth2"
66 ? [
67 openApiWireAuthInput({
68 ...template,
69 slug: AuthTemplateSlug.make(template.slug),
70 resource: template.resource ?? undefined,
71 }),
72 ]
73 : [],
74 );
75 const registryPlacement = input.authHeader
76 ? placementFromHeaderPattern(input.authHeader)
77 : null;
78 const authenticationTemplate = [
79 ...presetMethods,
80 ...(registryPlacement
81 ? [openApiWireAuthInput(templateFromPlacements([registryPlacement]))]
82 : []),
83 ];
84 const presetOverrides =
85 specOverrides && specOverrides.length > 0 ? specOverrides : preset?.specOverrides;
37 const exit = await doAdd({86 const exit = await doAdd({
38 payload: {87 payload: {
39 spec: { kind: "url" as const, url: input.url },88 spec: { kind: "url" as const, url: input.url },
40 slug,89 slug,
41 name: input.name,90 name: input.name,
42 ...(specOverrides && specOverrides.length > 0 ? { specOverrides } : {}),91 ...(preset?.specFormat ? { specFormat: preset.specFormat } : {}),
92 ...(preset?.family ? { family: preset.family } : {}),
93 ...(preset?.healthCheck ? { healthCheck: preset.healthCheck } : {}),
94 ...(presetOverrides && presetOverrides.length > 0
95 ? { specOverrides: presetOverrides }
96 : {}),
97 ...(authenticationTemplate.length > 0 ? { authenticationTemplate } : {}),
43 },98 },
44 reactivityKeys: integrationWriteKeys,99 reactivityKeys: integrationWriteKeys,
45 });100 });
packages/react/src/api/analytics.tsx+7−0
@@ -40,6 +40,13 @@ export interface AnalyticsEvents { · 39 unmodified lines
40 detected_kind?: string;40 detected_kind?: string;
41 confidence?: string;41 confidence?: string;
42 };42 };
43 /** Thumbs on the AI-generated credential guidance panel — the accuracy
44 * signal for the registry's machine-written setup text. */
45 credential_guidance_rated: {
46 domain: string;
47 credential_label: string;
48 vote: "up" | "down";
49 };
43 integration_add_started: {50 integration_add_started: {
44 plugin_key: string;51 plugin_key: string;
45 via: "detect" | "manual" | "preset" | "command_palette" | "catalog";52 via: "detect" | "manual" | "preset" | "command_palette" | "catalog";
packages/react/src/components/credential-guidance.tsx+60−14
@@ -1,8 +1,10 @@
1import { useMemo } from "react";1import { useMemo, useState } from "react";
2import { ArrowUpRightIcon } from "lucide-react";2import { ArrowUpRightIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react";
3import { getDomain } from "tldts";3import { getDomain } from "tldts";
4 4
5import { trackEvent } from "../api/analytics";
5import { useCredentialGuidance } from "../lib/integrations-sh-catalog";6import { useCredentialGuidance } from "../lib/integrations-sh-catalog";
7import { cn } from "../lib/utils";
6 8
7// ---------------------------------------------------------------------------9// ---------------------------------------------------------------------------
8// What this key is, and where to get one.10// What this key is, and where to get one.
@@ -105,18 +107,62 @@ export function CredentialGuidancePanel(props: { · 96 unmodified lines
105 {relevant.setup ? <SetupText text={relevant.setup} /> : null}107 {relevant.setup ? <SetupText text={relevant.setup} /> : null}
106 {/* Provenance, stated plainly: this text is machine-written registry108 {/* Provenance, stated plainly: this text is machine-written registry
107 data, not the provider's own docs — the reader should weight it109 data, not the provider's own docs — the reader should weight it
108 accordingly (menus move, prefixes change). */}110 accordingly (menus move, prefixes change). The thumbs are the
109 <p className="pt-0.5 font-mono text-[10px] uppercase tracking-wide text-muted-foreground/60">111 accuracy loop: votes land in analytics keyed by domain, so wrong
110 AI-generated guidance via{" "}112 guidance is findable instead of silently misleading. */}
111 <a113 <div className="flex items-center justify-between gap-3 pt-0.5">
112 href={`https://integrations.sh/${domain ?? ""}`}114 <p className="font-mono text-[10px] uppercase tracking-wide text-muted-foreground/60">
113 target="_blank"115 AI-generated guidance via{" "}
114 rel="noreferrer"116 <a
115 className="underline underline-offset-2 hover:text-muted-foreground"117 href={`https://integrations.sh/${domain ?? ""}`}
116 >118 target="_blank"
117 integrations.sh119 rel="noreferrer"
118 </a>120 className="underline underline-offset-2 hover:text-muted-foreground"
119 </p>121 >
122 integrations.sh
123 </a>
124 </p>
125 <GuidanceVote domain={domain ?? ""} label={relevant.label} />
126 </div>
120 </div>127 </div>
121 );128 );
122}129}
130
131/** One vote per render of the panel; the pair collapses to the chosen thumb
132 * so a second opinion means reopening, not flip-flopping the metric. */
133function GuidanceVote(props: { readonly domain: string; readonly label: string }) {
134 const [voted, setVoted] = useState<"up" | "down" | null>(null);
135 const vote = (value: "up" | "down") => {
136 if (voted) return;
137 setVoted(value);
138 trackEvent("credential_guidance_rated", {
139 domain: props.domain,
140 credential_label: props.label,
141 vote: value,
142 });
143 };
144 return (
145 <span className="flex shrink-0 items-center gap-1">
146 {(["up", "down"] as const).map((value) => {
147 const Icon = value === "up" ? ThumbsUpIcon : ThumbsDownIcon;
148 if (voted && voted !== value) return null;
149 return (
150 // oxlint-disable-next-line react/forbid-elements -- an icon chip, not a Button variant
151 <button
152 key={value}
153 type="button"
154 aria-label={value === "up" ? "Guidance was accurate" : "Guidance was wrong"}
155 disabled={voted !== null}
156 onClick={() => vote(value)}
157 className={cn(
158 "rounded p-1 text-muted-foreground/50 transition-colors",
159 voted === value ? "text-foreground" : "hover:bg-muted hover:text-muted-foreground",
160 )}
161 >
162 <Icon className="size-3" aria-hidden />
163 </button>
164 );
165 })}
166 </span>
167 );
168}

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