{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-auth",
  "title": "Agent Auth",
  "description": "Capability approval and grant revocation UI for Better Auth Agent Auth.",
  "dependencies": [
    "@better-auth-ui/core@latest",
    "@better-auth-ui/react@latest",
    "better-auth",
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "avatar",
    "badge",
    "button",
    "card",
    "checkbox",
    "skeleton",
    "spinner"
  ],
  "files": [
    {
      "path": "examples/start-shadcn-example/src/components/auth/agent-auth/agent-approval.tsx",
      "content": "\"use client\"\n\nimport type {\n  AgentApprovalRequest,\n  AgentAuthClient\n} from \"@better-auth-ui/core/plugins/agent-auth\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport {\n  useAgentApproval,\n  useApproveAgent,\n  useDenyAgent\n} from \"@better-auth-ui/react/plugins/agent-auth\"\nimport {\n  BotIcon,\n  CircleCheckIcon,\n  CircleXIcon,\n  FingerprintIcon,\n  EllipsisIcon\n} from \"lucide-react\"\nimport { useEffect, useMemo, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { agentAuthPlugin } from \"@/lib/auth/agent-auth-plugin\"\nimport { UserAvatar } from \"../user/user-avatar\"\nimport { cn } from \"cn\"\n\ntype ApprovalResult = \"approved\" | \"denied\"\n\nexport type AgentApprovalProps = { className?: string }\n\n/** Render the Agent Auth approval page configured by `deviceAuthorizationPage`. */\nexport function AgentApproval({ className }: AgentApprovalProps) {\n  const { authClient, basePaths, navigate, viewPaths } =\n    useAuth<AgentAuthClient>()\n  const plugin = useAuthPlugin(agentAuthPlugin)\n  const session = useSession(authClient)\n  const request = useMemo<AgentApprovalRequest | undefined>(() => {\n    if (typeof window === \"undefined\") return undefined\n    const query = new URLSearchParams(window.location.search)\n    const agentId = query.get(\"agent_id\")\n    if (!agentId) return undefined\n    return {\n      agentId,\n      approvalId: query.get(\"approval_id\") ?? undefined,\n      userCode: query.get(\"code\") ?? query.get(\"user_code\") ?? undefined\n    }\n  }, [])\n  const approval = useAgentApproval(authClient, plugin.adapter, request)\n  const approve = useApproveAgent(authClient, plugin.adapter)\n  const deny = useDenyAgent(authClient, plugin.adapter)\n  const [selection, setSelection] = useState<Set<string> | null>(null)\n  const [result, setResult] = useState<ApprovalResult>()\n\n  useEffect(() => {\n    if (session.isPending || session.data || typeof window === \"undefined\") {\n      return\n    }\n    const returnPath = `${window.location.pathname}${window.location.search}`\n    navigate({\n      to: `${basePaths.auth}/${viewPaths.auth.signIn}?redirectTo=${encodeURIComponent(returnPath)}`\n    })\n  }, [\n    basePaths.auth,\n    navigate,\n    session.data,\n    session.isPending,\n    viewPaths.auth.signIn\n  ])\n\n  const requested = approval.data?.requestedCapabilities ?? []\n  const selected =\n    selection ?? new Set(requested.map((grant) => grant.capability))\n  const updateSelection = (capability: string, isSelected: boolean) => {\n    const next = new Set(selected)\n    if (isSelected) next.add(capability)\n    else next.delete(capability)\n    setSelection(next)\n  }\n  const decision = {\n    ...request,\n    agentId: request?.agentId ?? \"\",\n    capabilities: [...selected]\n  }\n  const denyDecision = {\n    ...request,\n    agentId: request?.agentId ?? \"\"\n  }\n\n  if (!request) {\n    return (\n      <Card className={cn(\"w-full max-w-md\", className)}>\n        <CardContent className=\"text-sm text-destructive\">\n          {plugin.localization.invalidRequest}\n        </CardContent>\n      </Card>\n    )\n  }\n\n  if (result) {\n    const approved = result === \"approved\"\n    return (\n      <Card className={cn(\"w-full max-w-md\", className)}>\n        <CardContent className=\"flex flex-col items-center gap-4 py-10 text-center\">\n          {approved ? (\n            <CircleCheckIcon className=\"size-10 text-emerald-600\" />\n          ) : (\n            <CircleXIcon className=\"size-10 text-muted-foreground\" />\n          )}\n          <div className=\"flex flex-col gap-1\">\n            <h1 className=\"font-semibold\">\n              {approved\n                ? plugin.localization.approvedTitle\n                : plugin.localization.deniedTitle}\n            </h1>\n            <p className=\"text-sm text-muted-foreground\">\n              {approved\n                ? plugin.localization.approvedDescription\n                : plugin.localization.deniedDescription}\n            </p>\n          </div>\n        </CardContent>\n      </Card>\n    )\n  }\n\n  return (\n    <Card\n      className={cn(\n        \"max-h-[calc(100dvh-2rem)] w-full max-w-lg gap-0 overflow-hidden p-0\",\n        className\n      )}\n    >\n      <div\n        className=\"min-h-0 overflow-y-auto overscroll-contain focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n        role=\"region\"\n        aria-label={plugin.localization.approvalTitle}\n        tabIndex={0}\n      >\n        <CardHeader className=\"flex flex-col items-stretch gap-5 p-6\">\n          <div className=\"flex items-center justify-center gap-5\">\n            <div className=\"flex size-16 shrink-0 items-center justify-center rounded-full bg-muted/50\">\n              <BotIcon aria-hidden=\"true\" className=\"size-7\" />\n            </div>\n            <EllipsisIcon\n              aria-hidden=\"true\"\n              className=\"size-5 text-muted-foreground\"\n            />\n            <UserAvatar\n              className=\"size-16\"\n              user={session.data?.user}\n              isPending={session.isPending}\n            />\n          </div>\n          <div className=\"grid justify-items-center gap-1 text-center\">\n            <CardTitle className=\"max-w-full break-words text-xl font-semibold\">\n              {approval.data ? (\n                approval.data.name\n              ) : approval.isPending ? (\n                <Skeleton className=\"h-6 w-40\" />\n              ) : (\n                plugin.localization.approvalTitle\n              )}\n            </CardTitle>\n            <CardDescription>\n              {plugin.localization.approvalDescription}\n            </CardDescription>\n            {approval.data ? (\n              <p className=\"text-sm text-muted-foreground\">\n                {approval.data.hostName ?? approval.data.hostId} ·{\" \"}\n                {approval.data.mode === \"autonomous\"\n                  ? plugin.localization.autonomousAgent\n                  : plugin.localization.delegatedAgent}\n              </p>\n            ) : approval.isPending ? (\n              <Skeleton className=\"h-4 w-48\" />\n            ) : null}\n            <div className=\"mt-2 flex max-w-full flex-wrap justify-center gap-x-1 text-sm text-muted-foreground\">\n              <span>{plugin.localization.signedInAs}</span>\n              {session.data ? (\n                <span className=\"break-all font-medium text-foreground\">\n                  {session.data.user.name || session.data.user.email}\n                </span>\n              ) : session.isPending ? (\n                <Skeleton className=\"h-4 w-32\" />\n              ) : null}\n            </div>\n          </div>\n        </CardHeader>\n        <CardContent className=\"px-4 pb-6 sm:px-6\">\n          <div className=\"grid gap-4 rounded-lg bg-muted/50 p-5\">\n            <h2 className=\"text-sm font-medium\">\n              {plugin.localization.requestedCapabilities}\n            </h2>\n            {approval.isError ? (\n              <p className=\"text-sm text-destructive\">\n                {plugin.localization.approvalError}\n              </p>\n            ) : approval.isPending || session.isPending ? (\n              <div className=\"flex gap-3\">\n                <Skeleton className=\"size-4 shrink-0\" />\n                <div className=\"grid flex-1 gap-2\">\n                  <Skeleton className=\"h-4 w-32\" />\n                  <Skeleton className=\"h-4 w-full\" />\n                </div>\n              </div>\n            ) : requested.length ? (\n              <div className=\"grid gap-5\">\n                {requested.map((grant) => (\n                  <div\n                    key={grant.capability}\n                    className=\"flex items-start gap-3\"\n                  >\n                    <Checkbox\n                      id={`agent-capability-${grant.capability}`}\n                      className=\"mt-1\"\n                      checked={selected.has(grant.capability)}\n                      disabled={approve.isPending || deny.isPending}\n                      aria-label={grant.capability}\n                      onCheckedChange={(checked) =>\n                        updateSelection(grant.capability, checked === true)\n                      }\n                    />\n                    <div className=\"grid min-w-0 flex-1 gap-1\">\n                      <label\n                        htmlFor={`agent-capability-${grant.capability}`}\n                        className=\"cursor-pointer text-base leading-6 break-words\"\n                      >\n                        {grant.capability}\n                      </label>\n                      {grant.description ? (\n                        <p className=\"text-sm leading-5 text-muted-foreground\">\n                          {grant.description}\n                        </p>\n                      ) : null}\n                      {grant.reason ? (\n                        <p className=\"text-sm leading-5 text-muted-foreground\">\n                          {plugin.localization.requestReason}: {grant.reason}\n                        </p>\n                      ) : null}\n                      {grant.approvalStrength !== \"none\" ? (\n                        <p className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n                          {grant.approvalStrength === \"webauthn\" ? (\n                            <>\n                              <FingerprintIcon\n                                aria-hidden=\"true\"\n                                className=\"size-4\"\n                              />\n                              {plugin.localization.approvalWebauthn}\n                            </>\n                          ) : (\n                            plugin.localization.approvalSession\n                          )}\n                        </p>\n                      ) : null}\n                      {grant.constraints ? (\n                        <details className=\"mt-1 text-sm text-muted-foreground\">\n                          <summary className=\"w-fit cursor-pointer rounded-sm focus-visible:outline-2 focus-visible:outline-ring\">\n                            {plugin.localization.constraints}\n                          </summary>\n                          <pre className=\"mt-2 max-w-full overflow-x-auto rounded-md bg-background/50 p-3 text-xs\">\n                            {JSON.stringify(grant.constraints, null, 2)}\n                          </pre>\n                        </details>\n                      ) : null}\n                    </div>\n                  </div>\n                ))}\n              </div>\n            ) : (\n              <p className=\"text-sm text-muted-foreground\">\n                {plugin.localization.noCapabilities}\n              </p>\n            )}\n          </div>\n        </CardContent>\n      </div>\n      <CardFooter className=\"grid shrink-0 grid-cols-2 gap-3 p-4 sm:p-6\">\n        <Button\n          className=\"h-11 w-full\"\n          disabled={\n            approve.isPending ||\n            deny.isPending ||\n            !approval.data ||\n            !session.data ||\n            approval.isFetching ||\n            approval.isError\n          }\n          variant=\"secondary\"\n          onClick={() =>\n            deny.mutate(denyDecision, { onSuccess: () => setResult(\"denied\") })\n          }\n        >\n          {deny.isPending && <Spinner />}\n          {plugin.localization.deny}\n        </Button>\n        <Button\n          className=\"h-11 w-full\"\n          disabled={\n            approve.isPending ||\n            deny.isPending ||\n            !selected.size ||\n            !approval.data ||\n            !session.data ||\n            approval.isFetching ||\n            approval.isError\n          }\n          onClick={() =>\n            approve.mutate(decision, { onSuccess: () => setResult(\"approved\") })\n          }\n        >\n          {approve.isPending && <Spinner />}\n          {plugin.localization.allow}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/agent-auth/agent-approval.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/agent-auth/agent-authorizations.tsx",
      "content": "\"use client\"\n\nimport type {\n  AgentAuthClient,\n  AgentAuthorization,\n  AgentCapabilityGrant\n} from \"@better-auth-ui/core/plugins/agent-auth\"\nimport { useAuth, useAuthPlugin } from \"@better-auth-ui/react\"\nimport {\n  useAgentAuthorizations,\n  useRevokeAgentCapability\n} from \"@better-auth-ui/react/plugins/agent-auth\"\nimport { BotIcon, ShieldAlertIcon, XIcon } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { agentAuthPlugin } from \"@/lib/auth/agent-auth-plugin\"\nimport { cn } from \"cn\"\n\ntype RevokeTarget = {\n  agent: AgentAuthorization\n  grant: AgentCapabilityGrant\n}\n\nexport type AgentAuthorizationsProps = { className?: string }\n\n/** List user-owned agents and revoke individual active capability grants. */\nexport function AgentAuthorizations({ className }: AgentAuthorizationsProps) {\n  const { authClient, localization } = useAuth<AgentAuthClient>()\n  const plugin = useAuthPlugin(agentAuthPlugin)\n  const agents = useAgentAuthorizations(authClient, plugin.adapter)\n  const revoke = useRevokeAgentCapability(authClient, plugin.adapter)\n  const [target, setTarget] = useState<RevokeTarget>()\n\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)}>\n      <div className=\"flex flex-col gap-1\">\n        <h2 className=\"text-sm font-semibold\">{plugin.localization.agents}</h2>\n        <p className=\"text-xs text-muted-foreground\">\n          {plugin.localization.agentsDescription}\n        </p>\n      </div>\n      <Card className=\"p-0\">\n        <CardContent className=\"flex flex-col gap-5 p-4\">\n          {agents.isPending ? (\n            <>\n              <Skeleton className=\"h-24 rounded-xl\" />\n              <Skeleton className=\"h-24 rounded-xl\" />\n            </>\n          ) : agents.data?.length ? (\n            agents.data.map((agent) => (\n              <div key={agent.id} className=\"flex flex-col gap-3\">\n                <div className=\"flex items-center gap-3\">\n                  <div className=\"flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted\">\n                    <BotIcon className=\"size-4\" />\n                  </div>\n                  <div className=\"flex min-w-0 flex-1 flex-col\">\n                    <span className=\"truncate text-sm font-semibold\">\n                      {agent.name}\n                    </span>\n                    <span className=\"truncate text-xs text-muted-foreground\">\n                      {agent.hostName ?? agent.hostId}\n                    </span>\n                  </div>\n                  <Badge variant=\"secondary\">\n                    {agent.mode === \"autonomous\"\n                      ? plugin.localization.autonomousAgent\n                      : plugin.localization.delegatedAgent}\n                  </Badge>\n                </div>\n                <div className=\"flex flex-col gap-2 pl-13\">\n                  {agent.grants.map((grant) => (\n                    <div\n                      key={grant.capability}\n                      className=\"flex items-center gap-2\"\n                    >\n                      <div className=\"flex min-w-0 flex-1 flex-col\">\n                        <span className=\"truncate text-sm\">\n                          {grant.capability}\n                        </span>\n                        {grant.description && (\n                          <span className=\"truncate text-xs text-muted-foreground\">\n                            {grant.description}\n                          </span>\n                        )}\n                      </div>\n                      <Badge variant=\"secondary\">\n                        {plugin.localization[grant.status]}\n                      </Badge>\n                      {grant.status === \"active\" && (\n                        <Button\n                          type=\"button\"\n                          size=\"icon-sm\"\n                          variant=\"ghost\"\n                          aria-label={`${plugin.localization.revoke} ${grant.capability}`}\n                          onClick={() => setTarget({ agent, grant })}\n                        >\n                          <XIcon />\n                        </Button>\n                      )}\n                    </div>\n                  ))}\n                </div>\n              </div>\n            ))\n          ) : (\n            <div className=\"flex items-center gap-3 text-muted-foreground\">\n              <BotIcon className=\"size-5\" />\n              <p className=\"text-sm\">{plugin.localization.noAgents}</p>\n            </div>\n          )}\n        </CardContent>\n      </Card>\n\n      <AlertDialog\n        open={Boolean(target)}\n        onOpenChange={(open) => {\n          if (!open) setTarget(undefined)\n        }}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogMedia>\n              <ShieldAlertIcon />\n            </AlertDialogMedia>\n            <AlertDialogTitle>\n              {plugin.localization.revokeTitle}\n            </AlertDialogTitle>\n            <AlertDialogDescription>\n              {plugin.localization.revokeDescription}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <code className=\"rounded-lg bg-muted p-3 text-xs\">\n            {target?.grant.capability}\n          </code>\n          <AlertDialogFooter>\n            <AlertDialogCancel disabled={revoke.isPending}>\n              {localization.settings.cancel}\n            </AlertDialogCancel>\n            <Button\n              type=\"button\"\n              variant=\"destructive\"\n              disabled={revoke.isPending}\n              onClick={() => {\n                if (!target) return\n                revoke.mutate(\n                  {\n                    agentId: target.agent.id,\n                    capability: target.grant.capability\n                  },\n                  { onSuccess: () => setTarget(undefined) }\n                )\n              }}\n            >\n              {revoke.isPending && <Spinner />}\n              {plugin.localization.confirmRevoke}\n            </Button>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/agent-auth/agent-authorizations.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/components/auth/user/user-avatar.tsx",
      "content": "\"use client\"\n\nimport type { UsernameAuthClient } from \"@better-auth-ui/core/plugins/username\"\nimport { useAuth, useSession } from \"@better-auth-ui/react\"\nimport type { User } from \"better-auth\"\nimport { User2 } from \"lucide-react\"\nimport type { ReactNode } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { cn } from \"cn\"\n\nexport type UserAvatarProps = {\n  className?: string\n  fallback?: ReactNode\n  isPending?: boolean\n  /** @remarks `User` */\n  user?: User & { username?: string | null; displayUsername?: string | null }\n}\n\n/**\n * Display a user's avatar using session information or an explicit user prop.\n *\n * Renders a circular avatar that shows the user's image when available, a fallback node if provided, or the user's first two initials; while the session is loading (or when `isPending` is true) and no `user` prop is supplied, renders a skeleton placeholder.\n *\n * @param className - Additional CSS classes applied to the avatar container\n * @param user - Optional user object to display instead of the session user\n * @param isPending - When true, treat the component as loading and show the skeleton if no `user` is provided\n * @param fallback - Node to render inside the avatar fallback area before initials or the default icon\n * @returns The avatar element to render (JSX)\n */\nexport function UserAvatar({\n  className,\n  user,\n  isPending,\n  fallback\n}: UserAvatarProps) {\n  const { authClient } = useAuth<UsernameAuthClient>()\n  const { data: session, isPending: sessionPending } = useSession(authClient, {\n    enabled: !user && !isPending\n  })\n\n  if ((isPending || sessionPending) && !user) {\n    return <Skeleton className={cn(\"size-8 rounded-full\", className)} />\n  }\n\n  const resolvedUser = user ?? session?.user\n\n  const initials = (\n    resolvedUser?.username ||\n    resolvedUser?.name ||\n    resolvedUser?.email\n  )\n    ?.slice(0, 2)\n    .toUpperCase()\n\n  return (\n    <Avatar\n      className={cn(\n        \"size-8 bg-muted text-foreground text-sm rounded-full\",\n        className\n      )}\n    >\n      <AvatarImage\n        src={resolvedUser?.image ?? undefined}\n        alt={\n          resolvedUser?.displayUsername ||\n          resolvedUser?.name ||\n          resolvedUser?.email\n        }\n      />\n\n      <AvatarFallback className=\"text-muted-foreground!\">\n        {fallback || initials || <User2 className=\"size-4\" />}\n      </AvatarFallback>\n    </Avatar>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/user/user-avatar.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/agent-auth-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  type AgentAuthPluginOptions,\n  agentAuthPlugin as coreAgentAuthPlugin\n} from \"@better-auth-ui/core/plugins/agent-auth\"\n\nimport { AgentApproval } from \"@/components/auth/agent-auth/agent-approval\"\nimport { AgentAuthorizations } from \"@/components/auth/agent-auth/agent-authorizations\"\n\nexport const agentAuthPlugin = createAuthPlugin(\n  coreAgentAuthPlugin.id,\n  (options: AgentAuthPluginOptions) => {\n    const core = coreAgentAuthPlugin(options)\n    return {\n      ...core,\n      views: { auth: { agentApproval: AgentApproval } },\n      securityCards: core.grants ? [AgentAuthorizations] : []\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/agent-auth-plugin.ts"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/auth-plugin.ts",
      "content": "import type {\n  AuthPluginComponents,\n  AuthPlugin as AuthPluginPrimitive\n} from \"@better-auth-ui/react\"\n\ndeclare module \"@better-auth-ui/core\" {\n  /** Widens `useAuth().plugins` to the shadcn-typed `AuthPlugin`. */\n  interface AuthPluginRegister {\n    shadcn: AuthPlugin\n  }\n}\n\n/** Props the shadcn `<Auth>` router spreads onto plugin-contributed auth views. */\nexport type AuthViewProps = {\n  className?: string\n  socialLayout?: \"auto\" | \"horizontal\" | \"vertical\" | \"grid\"\n  socialPosition?: \"top\" | \"bottom\"\n}\n\n/** Props the shadcn `<Settings>` router spreads onto plugin-contributed settings views. */\nexport type SettingsViewProps = {\n  className?: string\n}\n\n/** Shadcn plugin type. Plugin authors import this from `@/lib/auth/auth-plugin`. */\nexport type AuthPlugin = AuthPluginPrimitive<\n  AuthPluginComponents,\n  AuthViewProps,\n  SettingsViewProps\n>\n",
      "type": "registry:lib",
      "target": "@lib/auth/auth-plugin.ts"
    }
  ],
  "type": "registry:component"
}