Files
kandev/apps/web/components/settings/plugins/plugin-repo-link.tsx
T
João Salavisa 91bdee3d3a feat(plugins): show source repo link on installed plugin list and settings (#1821)
* feat(plugins): show source repo link on installed plugin list and settings

Installed plugins carried no repository URL — a "Repo" link existed only
on marketplace catalog cards, whose repo_url is registry-derived from
plugins.yaml and never reaches an installed record. Let the manifest
declare an optional repo_url so sideloaded and directly-installed plugins
surface the same link.

- manifest: add optional RepoURL (repo_url); validate http(s)-only at
  registration so a javascript: scheme can't reach the operator's session.
- Record embeds the manifest, so repo_url flows through JSON to the
  frontend PluginRecord with no extra plumbing.
- frontend: extract the shared, guarded PluginRepoLink (+ isHttpUrl) from
  the marketplace row and render it on the installed plugin row and the
  plugin detail header.
- fixture + docs: exercise repo_url in the e2e fixture manifest and
  document the field in the public manifest reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): harden repo_url handling per review

- PluginRepoLink accepts string|undefined|null and isHttpUrl null-checks
  before trim, so it can't throw on an absent repo_url (CodeRabbit); drop
  the now-redundant truthiness guards at the call sites.
- validateRepoURL trims m.RepoURL in place so the stored/serialised value
  matches what was validated (Greptile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 14:03:52 +01:00

40 lines
1.3 KiB
TypeScript

import { IconExternalLink } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
/**
* Guard for rendering a plugin-supplied repo URL as an `<a href>`. A plugin's
* repo_url comes from its manifest (installed plugins) or an untrusted
* index.json (marketplace catalog), so only http(s) is allowed — a
* `javascript:` (or other) scheme would execute in the operator's session on
* click, and `rel`/`target` do NOT block it.
*/
export function isHttpUrl(url: string | undefined | null): boolean {
if (!url) return false;
return /^https?:\/\//i.test(url.trim());
}
/**
* A guarded external "Repo" link to a plugin's source repository. Renders
* nothing when the URL is empty or not http(s). Shared by the marketplace
* catalog card and the installed-plugin list/detail so the scheme guard and
* link markup live in one place.
*/
export function PluginRepoLink({ url, className }: { url?: string | null; className?: string }) {
if (!isHttpUrl(url)) return null;
return (
<a
href={url ?? undefined}
target="_blank"
rel="noreferrer"
data-testid="plugin-repo-link"
className={cn(
"inline-flex items-center gap-1 hover:text-foreground cursor-pointer",
className,
)}
>
<IconExternalLink className="h-3.5 w-3.5" />
Repo
</a>
);
}