ai-agents-client.tsx145 lines · main
1'use client';
2
3import { useCallback, useEffect, useState } from 'react';
4
5type AgentRow = {
6 id: string;
7 agentName: string;
8 scopes: string[];
9 hint: string;
10 expiresAt: string | null;
11 revokedAt: string | null;
12};
13
14export function AuthAiAgentsClient({ projectId }: { projectId: string }) {
15 const [items, setItems] = useState<AgentRow[]>([]);
16 const [name, setName] = useState('support-bot');
17 const [plaintext, setPlaintext] = useState<string | null>(null);
18 const [err, setErr] = useState<string | null>(null);
19 const [pending, setPending] = useState(false);
20
21 const load = useCallback(async () => {
22 if (!projectId) return;
23 setErr(null);
24 const res = await fetch(
25 `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/ai/agents`,
26 { credentials: 'include', cache: 'no-store' },
27 );
28 if (!res.ok) {
29 setErr(res.status === 401 ? 'sign in required' : `load failed (${res.status})`);
30 return;
31 }
32 const body = (await res.json()) as { agents?: AgentRow[] };
33 setItems(body.agents ?? []);
34 }, [projectId]);
35
36 useEffect(() => {
37 void load();
38 }, [load]);
39
40 async function create(): Promise<void> {
41 setPending(true);
42 setErr(null);
43 setPlaintext(null);
44 try {
45 const res = await fetch(
46 `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/ai/agents`,
47 {
48 method: 'POST',
49 credentials: 'include',
50 headers: { 'content-type': 'application/json' },
51 body: JSON.stringify({ agentName: name || 'agent', ttlHours: 24 }),
52 },
53 );
54 const body = (await res.json().catch(() => ({}))) as {
55 plaintext?: string;
56 message?: string;
57 };
58 if (!res.ok) throw new Error(body.message ?? `http ${res.status}`);
59 setPlaintext(body.plaintext ?? null);
60 await load();
61 } catch (e) {
62 setErr(e instanceof Error ? e.message : 'create failed');
63 } finally {
64 setPending(false);
65 }
66 }
67
68 async function revoke(id: string): Promise<void> {
69 setPending(true);
70 try {
71 await fetch(
72 `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/ai/agents/${encodeURIComponent(id)}`,
73 { method: 'DELETE', credentials: 'include' },
74 );
75 await load();
76 } finally {
77 setPending(false);
78 }
79 }
80
81 return (
82 <div className="flex max-w-xl flex-col gap-4">
83 <p className="font-mono text-xs text-[var(--color-text-muted)] leading-relaxed">
84 Tokens for AI agents and tools (not humans). Call{' '}
85 <code className="text-[var(--color-text)]">GET /v1/auth-core/ai/me</code> with{' '}
86 <code className="text-[var(--color-text)]">Authorization: Bearer brai_…</code>.
87 </p>
88 <div className="flex flex-wrap items-end gap-2">
89 <label className="flex flex-col gap-1 font-mono text-xs">
90 <span className="text-[var(--color-text-muted)]">agent name</span>
91 <input
92 value={name}
93 onChange={(e) => setName(e.target.value)}
94 className="rounded-md border bg-[var(--color-surface)] px-3 py-2"
95 style={{ borderColor: 'var(--auth-accent-border)' }}
96 />
97 </label>
98 <button
99 type="button"
100 disabled={pending}
101 onClick={() => void create()}
102 className="rounded-md px-3 py-2 font-mono text-xs font-medium text-black disabled:opacity-50"
103 style={{ background: '#FFFD74' }}
104 >
105 {pending ? 'creating…' : 'create agent token'}
106 </button>
107 </div>
108 {plaintext ? (
109 <div
110 className="rounded-md border p-3 font-mono text-xs"
111 style={{ borderColor: 'var(--auth-accent-border)' }}
112 >
113 <p className="text-[var(--color-text-muted)]">copy once:</p>
114 <code className="mt-1 block break-all text-[var(--color-text)]">
115 {plaintext}
116 </code>
117 </div>
118 ) : null}
119 <ul className="flex flex-col gap-2">
120 {items.map((a) => (
121 <li
122 key={a.id}
123 className="flex justify-between gap-2 rounded-md border px-3 py-2 font-mono text-xs"
124 style={{ borderColor: 'var(--auth-accent-border)' }}
125 >
126 <span className="text-[var(--color-text)]">
127 {a.agentName} · {a.hint}
128 {a.revokedAt ? ' · revoked' : ''}
129 </span>
130 {!a.revokedAt ? (
131 <button
132 type="button"
133 className="underline text-[var(--color-text-muted)]"
134 onClick={() => void revoke(a.id)}
135 >
136 revoke
137 </button>
138 ) : null}
139 </li>
140 ))}
141 </ul>
142 {err ? <p className="font-mono text-xs text-red-400">{err}</p> : null}
143 </div>
144 );
145}