"use client"
import { dmSans125ClassName } from "@/lib/fonts"
import { formatRelativeTime } from "@/components/settings/sync-utils"
import { cn } from "@lib/utils"
import { useAuth } from "@lib/auth-context"
import { authClient } from "@lib/auth"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@ui/components/alert-dialog"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@ui/components/dialog"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ui/components/select"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { PillButton } from "../integrations/install-steps"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
Check,
Copy,
KeyRound,
Loader2,
Plus,
Trash2,
XIcon,
} from "lucide-react"
import { useCallback, useId, useState } from "react"
import { toast } from "sonner"
type ListedApiKey = {
id: string
name: string | null
start: string | null
key?: string | null
createdAt: string
expiresAt: string | null
lastRequest: string | null
enabled: boolean
isScoped: boolean
containerTags: string[] | null
smType: string | null
smClient: string | null
}
const EXPIRY_OPTIONS = [
{ label: "1 year", value: "365" },
{ label: "6 months", value: "180" },
{ label: "30 days", value: "30" },
{ label: "7 days", value: "7" },
{ label: "Never", value: "0" },
] as const
const API_URL =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const MODAL_SHADOW =
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset"
const pillInputClass =
"h-9 w-full rounded-full border border-[#1E293B] bg-[#0D121A] px-3.5 text-[13px] font-medium text-[#FAFAFA] outline-none placeholder:text-[#5F6673] focus:border-[#334155]"
function ModalClose() {
return (
Close
)
}
function SettingsCard({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
function formatKeyPreview(start: string | null | undefined): string {
if (!start) return "sm_••••••"
return `${start}••••••`
}
function isExpired(expiresAt: string | null): boolean {
if (!expiresAt) return false
return new Date(expiresAt).getTime() <= Date.now()
}
function formatExpiresLabel(expiresAt: string | null): string {
if (!expiresAt) return "Never"
const date = new Date(expiresAt)
if (Number.isNaN(date.getTime())) return "—"
if (date.getTime() <= Date.now()) return "Expired"
return date.toLocaleDateString()
}
function extractCreatedKey(result: unknown): string {
if (!result || typeof result !== "object") {
throw new Error("API key missing from response")
}
const r = result as {
key?: string
data?: { key?: string }
error?: { message?: string }
}
if (r.error?.message) throw new Error(r.error.message)
const key = r.key ?? r.data?.key
if (!key) throw new Error("API key missing from response")
return key
}
export default function ApiKeys({
dialogPortalContainer,
}: {
dialogPortalContainer?: HTMLElement | null
}) {
const { org } = useAuth()
const queryClient = useQueryClient()
const nameId = useId()
const [createOpen, setCreateOpen] = useState(false)
const [keyName, setKeyName] = useState("")
const [expiryDays, setExpiryDays] = useState("365")
const [createdKey, setCreatedKey] = useState(null)
const [copied, setCopied] = useState(false)
const [revokeTarget, setRevokeTarget] = useState(null)
const {
data: keys = [],
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["api-keys", org?.id, "manage"],
queryFn: async () => {
if (!org?.id) return []
const res = await fetch(`${API_URL}/v3/auth/keys?type=keys`, {
credentials: "include",
})
if (!res.ok) {
throw new Error("Failed to load API keys")
}
const data = (await res.json()) as { keys?: ListedApiKey[] }
return data.keys ?? []
},
enabled: !!org?.id,
staleTime: 30 * 1000,
})
const invalidateKeys = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] })
queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id, "manage"] })
}, [org?.id, queryClient])
const createKeyMutation = useMutation({
mutationFn: async () => {
if (!org?.id) throw new Error("Organization is required")
const days = Number(expiryDays)
const expiresIn = days > 0 ? days * 24 * 60 * 60 : undefined
const name =
keyName.trim() || `key-${new Date().toISOString().slice(0, 10)}`
const res = await authClient.apiKey.create({
name,
expiresIn,
metadata: { organizationId: org.id },
prefix: `sm_${org.id}_`,
})
return extractCreatedKey(res)
},
onSuccess: (key) => {
setCreatedKey(key)
setKeyName("")
setExpiryDays("365")
setCopied(false)
invalidateKeys()
toast.success("API key created")
},
onError: (error) => {
toast.error("Failed to create API key", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
})
const revokeKeyMutation = useMutation({
mutationFn: async (keyId: string) => {
const res = await authClient.apiKey.delete({ keyId })
if (res && typeof res === "object" && "error" in res && res.error) {
const err = res.error as { message?: string }
throw new Error(err.message ?? "Failed to revoke API key")
}
},
onSuccess: () => {
setRevokeTarget(null)
invalidateKeys()
toast.success("API key revoked")
},
onError: (error) => {
toast.error("Failed to revoke API key", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
})
const handleCopy = async (value: string) => {
try {
await navigator.clipboard.writeText(value)
setCopied(true)
toast.success("API key copied to clipboard")
setTimeout(() => setCopied(false), 2000)
} catch {
toast.error("Failed to copy API key")
}
}
const resetCreateState = () => {
setCreateOpen(false)
setCreatedKey(null)
setKeyName("")
setExpiryDays("365")
setCopied(false)
}
const handleCreateOpenChange = (open: boolean) => {
if (!open) {
resetCreateState()
return
}
setCreateOpen(true)
}
return (
API Keys
Create keys for the Supermemory API, SDKs, and custom
integrations. Keys are shown once at creation.
{
setCreatedKey(null)
setCreateOpen(true)
}}
>
Create key
{isLoading ? (
Loading keys…
) : isError ? (
Couldn't load API keys.
refetch()}
className={cn(
dmSans125ClassName(),
"text-[13px] text-[#4BA0FA] hover:underline",
)}
>
Try again
) : keys.length === 0 ? (
No API keys yet
Create your first key to use the API programmatically or
connect custom tools.
{
setCreatedKey(null)
setCreateOpen(true)
}}
>
Create key
) : (
{keys.map((key) => {
const expired = isExpired(key.expiresAt)
const disabled = key.enabled === false || expired
return (
{key.name?.trim() || "Unnamed key"}
{key.isScoped && (
Scoped
)}
{disabled && (
{expired ? "Expired" : "Disabled"}
)}
{formatKeyPreview(key.start ?? key.key)}
·
Created {formatRelativeTime(key.createdAt)}
·
Last used{" "}
{key.lastRequest
? formatRelativeTime(key.lastRequest)
: "never"}
·
Expires {formatExpiresLabel(key.expiresAt)}
setRevokeTarget(key)}
title="Revoke key"
className="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-[#737373] transition-colors hover:bg-white/5 hover:text-[#C73B1B]"
>
Revoke key
)
})}
)}
Need docs?{" "}
API quickstart
{" · "}
Developer console
{/* Create / reveal dialog */}
{createdKey ? (
<>
API key created
Copy this key now. You won't be able to see it again.
{createdKey}
Store it somewhere safe. For security, the full key is only
shown once.
Done
handleCopy(createdKey)}>
{copied ? (
) : (
)}
{copied ? "Copied" : "Copy key"}
>
) : (
<>
Create API key
This key has full access to your organization's
Supermemory data via the API.
Cancel
createKeyMutation.mutate()}
disabled={createKeyMutation.isPending || !org?.id}
>
{createKeyMutation.isPending && (
)}
{createKeyMutation.isPending ? "Creating…" : "Create"}
>
)}
{/* Revoke confirmation */}
{
if (!open && !revokeKeyMutation.isPending) setRevokeTarget(null)
}}
>
Revoke API key?
{revokeTarget?.name?.trim()
? `"${revokeTarget.name}" will stop working immediately.`
: "This key will stop working immediately."}{" "}
Any apps or scripts still using it will fail. This cannot be
undone.
Cancel
{
e.preventDefault()
if (revokeTarget) revokeKeyMutation.mutate(revokeTarget.id)
}}
className="h-9 rounded-full bg-[#C73B1B] px-4 text-[13px] font-semibold text-white hover:bg-[#A83217]"
>
{revokeKeyMutation.isPending ? (
Revoking…
) : (
"Revoke key"
)}
)
}