import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { CompanySecret } from "@paperclipai/shared"; import { KeyRound, Pencil, RefreshCw, Trash2 } from "lucide-react"; import { ApiError } from "@/api/client"; import { secretsApi } from "@/api/secrets"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { useCompany } from "@/context/CompanyContext"; import { useToast } from "@/context/ToastContext"; import { cn, relativeTime } from "@/lib/utils"; type DialogMode = | { kind: "closed" } | { kind: "create" } | { kind: "rotate"; secret: CompanySecret } | { kind: "edit"; secret: CompanySecret } | { kind: "delete"; secret: CompanySecret }; const COMPANY_SECRETS_QUERY_KEY = "company-secrets"; export function CompanySecrets() { const { selectedCompany, selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const queryClient = useQueryClient(); const [dialog, setDialog] = useState({ kind: "closed" }); useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: "Settings", href: "/company/settings" }, { label: "Secrets" }, ]); }, [selectedCompany?.name, setBreadcrumbs]); const { data: secrets, isLoading } = useQuery({ queryKey: selectedCompanyId ? [COMPANY_SECRETS_QUERY_KEY, selectedCompanyId] : [COMPANY_SECRETS_QUERY_KEY, "none"], queryFn: () => secretsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); function invalidateList() { if (!selectedCompanyId) return; queryClient.invalidateQueries({ queryKey: [COMPANY_SECRETS_QUERY_KEY, selectedCompanyId], }); } function handleApiError(error: unknown, fallback: string) { const message = error instanceof Error ? error.message : fallback; pushToast({ tone: "error", title: fallback, body: message }); } return (

Company Secrets

Encrypted values that agents can reference from environment variables. Rotate to change a secret's value; delete to remove it from the company library.

{secrets?.length ?? 0} secret{secrets?.length === 1 ? "" : "s"}

{isLoading ? (
Loading…
) : !secrets || secrets.length === 0 ? (
No secrets yet. Use New secret to create one, or seal a plain env var from the agent configuration page.
) : (
    {secrets.map((secret) => (
  • {secret.name}
    {secret.description ? (

    {secret.description}

    ) : null}

    v{secret.latestVersion} · last rotated{" "} {relativeTime(secret.updatedAt)}

  • ))}
)}
{dialog.kind === "create" && selectedCompanyId ? ( setDialog({ kind: "closed" })} onCreated={(name) => { pushToast({ tone: "success", title: `Secret "${name}" created` }); invalidateList(); setDialog({ kind: "closed" }); }} onError={(error) => handleApiError(error, "Failed to create secret")} /> ) : null} {dialog.kind === "rotate" ? ( setDialog({ kind: "closed" })} onRotated={() => { pushToast({ tone: "success", title: `Secret "${dialog.secret.name}" rotated`, }); invalidateList(); setDialog({ kind: "closed" }); }} onError={(error) => handleApiError(error, "Failed to rotate secret")} /> ) : null} {dialog.kind === "edit" ? ( setDialog({ kind: "closed" })} onSaved={(name) => { pushToast({ tone: "success", title: `Secret "${name}" updated` }); invalidateList(); setDialog({ kind: "closed" }); }} onError={(error) => handleApiError(error, "Failed to update secret")} /> ) : null} {dialog.kind === "delete" ? ( setDialog({ kind: "closed" })} onDeleted={() => { pushToast({ tone: "success", title: `Secret "${dialog.secret.name}" deleted`, }); invalidateList(); setDialog({ kind: "closed" }); }} /> ) : null}
); } function CreateSecretDialog({ companyId, onClose, onCreated, onError, }: { companyId: string; onClose: () => void; onCreated: (name: string) => void; onError: (error: unknown) => void; }) { const [name, setName] = useState(""); const [value, setValue] = useState(""); const [description, setDescription] = useState(""); const create = useMutation({ mutationFn: () => secretsApi.create(companyId, { name: name.trim(), value, description: description.trim() || null, }), onSuccess: () => onCreated(name.trim()), onError, }); return ( !open && onClose()}> New secret Stored encrypted; visible to agents only when referenced from a secret-typed environment variable.
setName(event.target.value)} placeholder="API_TOKEN" autoFocus />