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.
Unified · Split
| @@ -7,12 +7,33 @@ import type { · 6 unmodified lines | ||
| 7 | 7 | IntegrationQuickAddInput, |
| 8 | 8 | IntegrationQuickAddResult, |
| 9 | 9 | } from "@executor-js/sdk/client"; |
| 10 | +import { AuthTemplateSlug } from "@executor-js/sdk/shared"; | |
| 10 | 11 | import { slugifyNamespace } from "@executor-js/react/plugins/integration-identity"; |
| 12 | +import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements"; | |
| 11 | 13 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; |
| 12 | −import { openApiPresets } from "../sdk/presets"; | |
| 14 | +import { openApiPresets, type OpenApiPreset } from "../sdk/presets"; | |
| 13 | 15 | import { addOpenApiSpec } from "./atoms"; |
| 16 | +import { openApiWireAuthInput, templateFromPlacements } from "./auth-method-config"; | |
| 14 | 17 | import { decodeOpenApiSpecOverrides } from "../sdk/spec-overrides"; |
| 15 | 18 | |
| 19 | +const 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. */ | |
| 30 | +const 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 | 37 | /** One-click add for a registry OpenAPI row. The spec itself is the |
| 17 | 38 | * configuration: omitting `authenticationTemplate` and `baseUrl` tells the |
| 18 | 39 | * server to derive both from the document, exactly what the add page's |
| @@ -34,12 +55,46 @@ function useOpenApiQuickAdd(): ( · 15 unmodified lines | ||
| 34 | 55 | if (input.specOverrides && input.specOverrides.length > 0 && specOverrides === undefined) { |
| 35 | 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 | 86 | const exit = await doAdd({ |
| 38 | 87 | payload: { |
| 39 | 88 | spec: { kind: "url" as const, url: input.url }, |
| 40 | 89 | slug, |
| 41 | 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 | 99 | reactivityKeys: integrationWriteKeys, |
| 45 | 100 | }); |
| @@ -40,6 +40,13 @@ export interface AnalyticsEvents { · 39 unmodified lines | ||
| 40 | 40 | detected_kind?: string; |
| 41 | 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 | 50 | integration_add_started: { |
| 44 | 51 | plugin_key: string; |
| 45 | 52 | via: "detect" | "manual" | "preset" | "command_palette" | "catalog"; |
| @@ -1,8 +1,10 @@ | ||
| 1 | −import { useMemo } from "react"; | |
| 2 | −import { ArrowUpRightIcon } from "lucide-react"; | |
| 1 | +import { useMemo, useState } from "react"; | |
| 2 | +import { ArrowUpRightIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react"; | |
| 3 | 3 | import { getDomain } from "tldts"; |
| 4 | 4 | |
| 5 | +import { trackEvent } from "../api/analytics"; | |
| 5 | 6 | import { useCredentialGuidance } from "../lib/integrations-sh-catalog"; |
| 7 | +import { cn } from "../lib/utils"; | |
| 6 | 8 | |
| 7 | 9 | // --------------------------------------------------------------------------- |
| 8 | 10 | // What this key is, and where to get one. |
| @@ -105,18 +107,62 @@ export function CredentialGuidancePanel(props: { · 96 unmodified lines | ||
| 105 | 107 | {relevant.setup ? <SetupText text={relevant.setup} /> : null} |
| 106 | 108 | {/* Provenance, stated plainly: this text is machine-written registry |
| 107 | 109 | data, not the provider's own docs — the reader should weight it |
| 108 | − accordingly (menus move, prefixes change). */} | |
| 109 | − <p className="pt-0.5 font-mono text-[10px] uppercase tracking-wide text-muted-foreground/60"> | |
| 110 | − AI-generated guidance via{" "} | |
| 111 | − <a | |
| 112 | − href={`https://integrations.sh/${domain ?? ""}`} | |
| 113 | − target="_blank" | |
| 114 | − rel="noreferrer" | |
| 115 | − className="underline underline-offset-2 hover:text-muted-foreground" | |
| 116 | − > | |
| 117 | − integrations.sh | |
| 118 | − </a> | |
| 119 | − </p> | |
| 110 | + accordingly (menus move, prefixes change). The thumbs are the | |
| 111 | + accuracy loop: votes land in analytics keyed by domain, so wrong | |
| 112 | + guidance is findable instead of silently misleading. */} | |
| 113 | + <div className="flex items-center justify-between gap-3 pt-0.5"> | |
| 114 | + <p className="font-mono text-[10px] uppercase tracking-wide text-muted-foreground/60"> | |
| 115 | + AI-generated guidance via{" "} | |
| 116 | + <a | |
| 117 | + href={`https://integrations.sh/${domain ?? ""}`} | |
| 118 | + target="_blank" | |
| 119 | + rel="noreferrer" | |
| 120 | + className="underline underline-offset-2 hover:text-muted-foreground" | |
| 121 | + > | |
| 122 | + integrations.sh | |
| 123 | + </a> | |
| 124 | + </p> | |
| 125 | + <GuidanceVote domain={domain ?? ""} label={relevant.label} /> | |
| 126 | + </div> | |
| 120 | 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. */ | |
| 133 | +function 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 | +} | |