"use client" import { useState } from "react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useUserProfile } from "@/lib/queries/use-user-profile" import { notify } from "@/lib/notify" interface ApiKey { keyIdentifier: string keyPrefix: string label: string | null createdAt: string lastUsedAt: string | null } interface WebhookConfiguration { webhookUrl: string | null webhookSecret: string | null webhookEnabled: boolean consecutiveFailures: number } function useApiKeys() { return useQuery({ queryKey: ["apiKeys"], queryFn: async () => { const response = await fetch("/api/v1/keys") if (!response.ok) throw new Error("failed to load api keys") const data = await response.json() return data.keys as ApiKey[] }, }) } function useCreateApiKey() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (label: string | null) => { const response = await fetch("/api/v1/keys", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ label }), }) if (!response.ok) { const data = await response.json() throw new Error(data.error || "failed to create api key") } return response.json() as Promise<{ fullKey: string keyPrefix: string keyIdentifier: string }> }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["apiKeys"] }) }, }) } function useRevokeApiKey() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (keyIdentifier: string) => { const response = await fetch(`/api/v1/keys/${keyIdentifier}`, { method: "DELETE", }) if (!response.ok) { const data = await response.json() throw new Error(data.error || "failed to revoke api key") } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["apiKeys"] }) }, }) } function useWebhookConfig() { return useQuery({ queryKey: ["webhookConfig"], queryFn: async () => { const response = await fetch("/api/webhook-config") if (!response.ok) throw new Error("failed to load webhook config") return response.json() as Promise }, }) } function useUpdateWebhookConfig() { const queryClient = useQueryClient() return useMutation({ mutationFn: async ( updates: Partial<{ webhookUrl: string webhookSecret: string webhookEnabled: boolean }> ) => { const response = await fetch("/api/webhook-config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }) if (!response.ok) { const data = await response.json() throw new Error(data.error || "failed to update webhook config") } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["webhookConfig"] }) }, }) } function useTestWebhook() { return useMutation({ mutationFn: async () => { const response = await fetch("/api/webhook-config/test", { method: "POST", }) if (!response.ok) { const data = await response.json() throw new Error(data.error || "failed to send test webhook") } return response.json() as Promise<{ delivered: boolean statusCode?: number error?: string }> }, }) } function ApiKeysSection() { const { data: apiKeys, isLoading } = useApiKeys() const createApiKey = useCreateApiKey() const revokeApiKey = useRevokeApiKey() const [newKeyLabel, setNewKeyLabel] = useState("") const [revealedKey, setRevealedKey] = useState(null) const [confirmRevokeIdentifier, setConfirmRevokeIdentifier] = useState< string | null >(null) function handleCreateKey() { createApiKey.mutate(newKeyLabel.trim() || null, { onSuccess: (data) => { setRevealedKey(data.fullKey) setNewKeyLabel("") notify("api key created") }, onError: (error: Error) => { notify(error.message) }, }) } function handleCopyKey() { if (revealedKey) { navigator.clipboard.writeText(revealedKey) notify("api key copied to clipboard") } } function handleRevokeKey(keyIdentifier: string) { revokeApiKey.mutate(keyIdentifier, { onSuccess: () => { notify("api key revoked") setConfirmRevokeIdentifier(null) }, onError: (error: Error) => { notify(error.message) }, }) } return (

api keys

use api keys to authenticate requests to the rest api

{revealedKey && (

copy this key now — it will not be shown again

{revealedKey}
)} {isLoading ? (

loading keys ...

) : ( <> {apiKeys && apiKeys.length > 0 && (
{apiKeys.map((apiKey) => (
{apiKey.keyPrefix}... {apiKey.label && ( {apiKey.label} )}
created{" "} {new Date(apiKey.createdAt).toLocaleDateString()} {apiKey.lastUsedAt && ` · last used ${new Date(apiKey.lastUsedAt).toLocaleDateString()}`}
{confirmRevokeIdentifier === apiKey.keyIdentifier ? (
revoke?
) : ( )}
))}
)} {(!apiKeys || apiKeys.length < 5) && (
setNewKeyLabel(event.target.value)} placeholder="key label (optional)" className="min-w-0 flex-1 border border-border bg-background-primary px-3 py-1.5 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" onKeyDown={(event) => { if (event.key === "Enter") handleCreateKey() }} />
)} )}
) } function WebhookSectionLoading() { const { data: webhookConfig, isLoading } = useWebhookConfig() if (isLoading) { return

loading webhook configuration ...

} if (!webhookConfig) { return

failed to load webhook configuration

} return } function WebhookSection({ initialConfiguration, }: { initialConfiguration: WebhookConfiguration }) { const { data: webhookConfig } = useWebhookConfig() const updateWebhookConfig = useUpdateWebhookConfig() const testWebhook = useTestWebhook() const [webhookUrl, setWebhookUrl] = useState( initialConfiguration.webhookUrl ?? "" ) const [webhookSecret, setWebhookSecret] = useState( initialConfiguration.webhookSecret ?? "" ) const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) function handleSaveWebhookConfig() { updateWebhookConfig.mutate( { webhookUrl: webhookUrl.trim(), webhookSecret: webhookSecret.trim(), }, { onSuccess: () => { notify("webhook configuration saved") setHasUnsavedChanges(false) }, onError: (error: Error) => { notify(error.message) }, } ) } function handleToggleEnabled() { if (!webhookConfig) return updateWebhookConfig.mutate( { webhookEnabled: !webhookConfig.webhookEnabled }, { onSuccess: () => { notify( webhookConfig.webhookEnabled ? "webhooks disabled" : "webhooks enabled" ) }, onError: (error: Error) => { notify(error.message) }, } ) } function handleTestWebhook() { testWebhook.mutate(undefined, { onSuccess: (data) => { if (data.delivered) { notify(`test webhook delivered (status ${data.statusCode})`) } else { notify(`test webhook failed: ${data.error}`) } }, onError: (error: Error) => { notify(error.message) }, }) } function handleGenerateSecret() { const array = new Uint8Array(32) crypto.getRandomValues(array) const generatedSecret = Array.from(array) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") setWebhookSecret(generatedSecret) setHasUnsavedChanges(true) } return (

webhooks

receive http post notifications when new entries arrive in your subscribed feeds

{ setWebhookUrl(event.target.value) setHasUnsavedChanges(true) }} placeholder="https://example.com/webhook" className="w-full border border-border bg-background-primary px-3 py-1.5 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" />
{ setWebhookSecret(event.target.value) setHasUnsavedChanges(true) }} placeholder="optional hmac-sha256 signing secret" className="min-w-0 flex-1 border border-border bg-background-primary px-3 py-1.5 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" />
{webhookConfig && (
status:{" "} {webhookConfig.webhookEnabled ? "enabled" : "disabled"} {webhookConfig.consecutiveFailures > 0 && ( {webhookConfig.consecutiveFailures} consecutive failure {webhookConfig.consecutiveFailures !== 1 && "s"} )}
)}
) } export function ApiSettings() { const { data: userProfile, isLoading } = useUserProfile() if (isLoading) { return

loading api settings ...

} if (!userProfile) { return (

failed to load api settings

) } if (userProfile.tier !== "developer") { return (

developer api

the developer plan includes a read-only rest api and webhook push notifications. upgrade to developer to access these features.

) } return (

api documentation

authenticate requests with an api key in the authorization header:

Authorization: Bearer asa_your_key_here

get /api/v1/profile — your account info and limits

get /api/v1/feeds — your subscribed feeds

get /api/v1/folders — your folders

get /api/v1/entries — entries with ?cursor, ?limit, ?feedIdentifier, ?readStatus, ?savedStatus filters

get /api/v1/entries/:id — single entry with full content

) }