agentgate 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +76 -23
- package/src/lib/db.js +43 -1
- package/src/lib/socketManager.js +73 -0
- package/src/routes/agents.js +73 -0
- package/src/routes/queue.js +78 -3
- package/src/routes/ui/auth.js +149 -0
- package/src/routes/ui/keys.js +302 -0
- package/src/routes/ui/messages.js +451 -0
- package/src/routes/ui/queue.js +420 -0
- package/src/routes/ui/settings.js +59 -0
- package/src/routes/ui/shared.js +139 -0
- package/src/routes/ui-new.js +196 -0
- package/src/routes/ui.js +260 -10
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// API Keys routes
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import { listApiKeys, createApiKey, deleteApiKey, updateAgentWebhook, getApiKeyById } from '../../lib/db.js';
|
|
4
|
+
import { escapeHtml, formatDate } from './shared.js';
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
// API Keys Management
|
|
9
|
+
router.get('/', (req, res) => {
|
|
10
|
+
const keys = listApiKeys();
|
|
11
|
+
res.send(renderKeysPage(keys));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
router.post('/create', async (req, res) => {
|
|
15
|
+
const { name } = req.body;
|
|
16
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
17
|
+
|
|
18
|
+
if (!name || !name.trim()) {
|
|
19
|
+
return wantsJson
|
|
20
|
+
? res.status(400).json({ error: 'Name is required' })
|
|
21
|
+
: res.send(renderKeysPage(listApiKeys(), 'Name is required'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const newKey = await createApiKey(name.trim());
|
|
25
|
+
const keys = listApiKeys();
|
|
26
|
+
|
|
27
|
+
if (wantsJson) {
|
|
28
|
+
return res.json({ success: true, key: newKey.key, keyPrefix: newKey.keyPrefix, name: newKey.name, keys });
|
|
29
|
+
}
|
|
30
|
+
res.send(renderKeysPage(keys, null, newKey));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
router.post('/:id/delete', (req, res) => {
|
|
34
|
+
const { id } = req.params;
|
|
35
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
36
|
+
|
|
37
|
+
deleteApiKey(id);
|
|
38
|
+
const keys = listApiKeys();
|
|
39
|
+
|
|
40
|
+
if (wantsJson) {
|
|
41
|
+
return res.json({ success: true, keys });
|
|
42
|
+
}
|
|
43
|
+
res.redirect('/ui/keys');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
router.post('/:id/webhook', (req, res) => {
|
|
47
|
+
const { id } = req.params;
|
|
48
|
+
const { webhook_url, webhook_token } = req.body;
|
|
49
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
50
|
+
|
|
51
|
+
const agent = getApiKeyById(id);
|
|
52
|
+
if (!agent) {
|
|
53
|
+
return wantsJson
|
|
54
|
+
? res.status(404).json({ error: 'Agent not found' })
|
|
55
|
+
: res.status(404).send('Agent not found');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
updateAgentWebhook(id, webhook_url, webhook_token);
|
|
59
|
+
const keys = listApiKeys();
|
|
60
|
+
|
|
61
|
+
if (wantsJson) {
|
|
62
|
+
return res.json({ success: true, keys });
|
|
63
|
+
}
|
|
64
|
+
res.redirect('/ui/keys');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
router.delete('/:id', (req, res) => {
|
|
68
|
+
const { id } = req.params;
|
|
69
|
+
const wantsJson = req.headers.accept?.includes('application/json');
|
|
70
|
+
|
|
71
|
+
deleteApiKey(id);
|
|
72
|
+
const keys = listApiKeys();
|
|
73
|
+
|
|
74
|
+
if (wantsJson) {
|
|
75
|
+
return res.json({ success: true, keys });
|
|
76
|
+
}
|
|
77
|
+
res.redirect('/ui/keys');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Render function
|
|
81
|
+
function renderKeysPage(keys, error = null, newKey = null) {
|
|
82
|
+
const renderKeyRow = (k) => `
|
|
83
|
+
<tr id="key-${k.id}">
|
|
84
|
+
<td><strong>${escapeHtml(k.name)}</strong></td>
|
|
85
|
+
<td><code class="key-value">${escapeHtml(k.key_prefix)}</code></td>
|
|
86
|
+
<td>
|
|
87
|
+
${k.webhook_url ? `
|
|
88
|
+
<span class="webhook-status webhook-configured" title="${escapeHtml(k.webhook_url)}">✓ Configured</span>
|
|
89
|
+
` : `
|
|
90
|
+
<span class="webhook-status webhook-none">Not set</span>
|
|
91
|
+
`}
|
|
92
|
+
<button type="button" class="btn-sm webhook-btn" data-id="${k.id}" data-name="${escapeHtml(k.name)}" data-url="${escapeHtml(k.webhook_url || '')}" data-token="${escapeHtml(k.webhook_token || '')}">Configure</button>
|
|
93
|
+
</td>
|
|
94
|
+
<td>${formatDate(k.created_at)}</td>
|
|
95
|
+
<td>
|
|
96
|
+
<button type="button" class="delete-btn" onclick="deleteKey('${k.id}')" title="Delete">×</button>
|
|
97
|
+
</td>
|
|
98
|
+
</tr>
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
return `<!DOCTYPE html>
|
|
102
|
+
<html>
|
|
103
|
+
<head>
|
|
104
|
+
<title>agentgate - API Keys</title>
|
|
105
|
+
<link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
|
|
106
|
+
<link rel="stylesheet" href="/public/style.css">
|
|
107
|
+
<style>
|
|
108
|
+
.keys-table { width: 100%; border-collapse: collapse; margin-top: 16px; }
|
|
109
|
+
.keys-table th, .keys-table td { padding: 12px; text-align: left; border-bottom: 1px solid #374151; }
|
|
110
|
+
.keys-table th { font-weight: 600; color: #9ca3af; font-size: 14px; }
|
|
111
|
+
.key-value { background: #1f2937; padding: 4px 8px; border-radius: 4px; font-size: 13px; color: #e5e7eb; }
|
|
112
|
+
.new-key-banner { background: #065f46; border: 1px solid #10b981; padding: 16px; border-radius: 8px; margin-bottom: 20px; color: #d1fae5; }
|
|
113
|
+
.new-key-banner code { background: #1f2937; color: #10b981; padding: 8px 12px; border-radius: 4px; display: block; margin-top: 8px; font-size: 14px; word-break: break-all; }
|
|
114
|
+
.delete-btn { background: none; border: none; color: #f87171; font-size: 20px; cursor: pointer; padding: 0 4px; line-height: 1; font-weight: bold; }
|
|
115
|
+
.delete-btn:hover { color: #dc2626; }
|
|
116
|
+
.back-link { color: #a78bfa; text-decoration: none; font-weight: 500; }
|
|
117
|
+
.back-link:hover { text-decoration: underline; }
|
|
118
|
+
.error-message { background: #7f1d1d; color: #fecaca; padding: 12px; border-radius: 8px; margin-bottom: 16px; }
|
|
119
|
+
.webhook-status { font-size: 12px; padding: 4px 8px; border-radius: 4px; margin-right: 8px; }
|
|
120
|
+
.webhook-configured { background: #065f46; color: #6ee7b7; }
|
|
121
|
+
.webhook-none { background: #374151; color: #9ca3af; }
|
|
122
|
+
.btn-sm { font-size: 12px; padding: 4px 8px; background: #4f46e5; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
|
123
|
+
.btn-sm:hover { background: #4338ca; }
|
|
124
|
+
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 1000; align-items: center; justify-content: center; }
|
|
125
|
+
.modal-overlay.active { display: flex; }
|
|
126
|
+
.modal { background: #1f2937; border-radius: 12px; padding: 24px; max-width: 500px; width: 90%; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
|
|
127
|
+
.modal h3 { margin: 0 0 16px 0; color: #f3f4f6; }
|
|
128
|
+
.modal label { display: block; margin-bottom: 4px; color: #d1d5db; font-size: 14px; }
|
|
129
|
+
.modal input { width: 100%; padding: 10px; border: 1px solid #374151; border-radius: 6px; background: #111827; color: #f3f4f6; margin-bottom: 12px; box-sizing: border-box; }
|
|
130
|
+
.modal input:focus { border-color: #6366f1; outline: none; }
|
|
131
|
+
.modal-buttons { display: flex; gap: 12px; justify-content: flex-end; margin-top: 16px; }
|
|
132
|
+
.modal .help-text { font-size: 12px; color: #9ca3af; margin-top: -8px; margin-bottom: 12px; }
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
137
|
+
<h1>API Keys</h1>
|
|
138
|
+
<a href="/ui" class="back-link">← Back to Dashboard</a>
|
|
139
|
+
</div>
|
|
140
|
+
<p>Manage API keys for your agents. Keys are hashed and can only be viewed once at creation.</p>
|
|
141
|
+
|
|
142
|
+
${error ? `<div class="error-message">${escapeHtml(error)}</div>` : ''}
|
|
143
|
+
|
|
144
|
+
${newKey ? `
|
|
145
|
+
<div class="new-key-banner">
|
|
146
|
+
<strong>New API key created!</strong> Copy it now - you won't be able to see it again.
|
|
147
|
+
<code>${newKey.key}</code>
|
|
148
|
+
<button type="button" class="btn-sm btn-primary" onclick="copyKey('${newKey.key}', this)" style="margin-top: 8px;">Copy to Clipboard</button>
|
|
149
|
+
</div>
|
|
150
|
+
` : ''}
|
|
151
|
+
|
|
152
|
+
<div class="card">
|
|
153
|
+
<h3>Create New Key</h3>
|
|
154
|
+
<form method="POST" action="/ui/keys/create" style="display: flex; gap: 12px; align-items: flex-end;">
|
|
155
|
+
<div style="flex: 1;">
|
|
156
|
+
<label>Key Name</label>
|
|
157
|
+
<input type="text" name="name" placeholder="e.g., clawdbot, moltbot, dev-agent" required>
|
|
158
|
+
</div>
|
|
159
|
+
<button type="submit" class="btn-primary">Create Key</button>
|
|
160
|
+
</form>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="card">
|
|
164
|
+
<h3>Existing Keys (${keys.length})</h3>
|
|
165
|
+
${keys.length === 0 ? `
|
|
166
|
+
<p style="color: var(--gray-500); text-align: center; padding: 20px;">No API keys yet. Create one above.</p>
|
|
167
|
+
` : `
|
|
168
|
+
<table class="keys-table">
|
|
169
|
+
<thead>
|
|
170
|
+
<tr>
|
|
171
|
+
<th>Name</th>
|
|
172
|
+
<th>Key Prefix</th>
|
|
173
|
+
<th>Webhook</th>
|
|
174
|
+
<th>Created</th>
|
|
175
|
+
<th></th>
|
|
176
|
+
</tr>
|
|
177
|
+
</thead>
|
|
178
|
+
<tbody id="keys-tbody">
|
|
179
|
+
${keys.map(renderKeyRow).join('')}
|
|
180
|
+
</tbody>
|
|
181
|
+
</table>
|
|
182
|
+
`}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Webhook Modal -->
|
|
186
|
+
<div id="webhook-modal" class="modal-overlay">
|
|
187
|
+
<div class="modal">
|
|
188
|
+
<h3>Configure Webhook for <span id="modal-agent-name"></span></h3>
|
|
189
|
+
<p style="color: #9ca3af; font-size: 14px; margin-bottom: 16px;">
|
|
190
|
+
When messages or queue updates are ready, agentgate will POST to this URL.
|
|
191
|
+
</p>
|
|
192
|
+
<form id="webhook-form">
|
|
193
|
+
<input type="hidden" id="webhook-agent-id" name="id">
|
|
194
|
+
<label for="webhook-url">Webhook URL</label>
|
|
195
|
+
<input type="url" id="webhook-url" name="webhook_url" placeholder="https://your-agent-gateway.com/webhook">
|
|
196
|
+
<p class="help-text">The endpoint that will receive POST notifications</p>
|
|
197
|
+
|
|
198
|
+
<label for="webhook-token">Authorization Token (optional)</label>
|
|
199
|
+
<input type="text" id="webhook-token" name="webhook_token" placeholder="secret-token">
|
|
200
|
+
<p class="help-text">Sent as Bearer token in Authorization header</p>
|
|
201
|
+
|
|
202
|
+
<div class="modal-buttons">
|
|
203
|
+
<button type="button" class="btn-secondary" onclick="closeWebhookModal()">Cancel</button>
|
|
204
|
+
<button type="submit" class="btn-primary">Save Webhook</button>
|
|
205
|
+
</div>
|
|
206
|
+
</form>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<script>
|
|
211
|
+
function copyKey(key, btn) {
|
|
212
|
+
navigator.clipboard.writeText(key).then(() => {
|
|
213
|
+
const orig = btn.textContent;
|
|
214
|
+
btn.textContent = 'Copied!';
|
|
215
|
+
setTimeout(() => btn.textContent = orig, 1500);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function showWebhookModal(btn) {
|
|
220
|
+
document.getElementById('webhook-agent-id').value = btn.dataset.id;
|
|
221
|
+
document.getElementById('modal-agent-name').textContent = btn.dataset.name;
|
|
222
|
+
document.getElementById('webhook-url').value = btn.dataset.url;
|
|
223
|
+
document.getElementById('webhook-token').value = btn.dataset.token;
|
|
224
|
+
document.getElementById('webhook-modal').classList.add('active');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function closeWebhookModal() {
|
|
228
|
+
document.getElementById('webhook-modal').classList.remove('active');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
document.querySelectorAll('.webhook-btn').forEach(btn => {
|
|
232
|
+
btn.addEventListener('click', () => showWebhookModal(btn));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
document.getElementById('webhook-form').addEventListener('submit', async (e) => {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
const id = document.getElementById('webhook-agent-id').value;
|
|
238
|
+
const webhookUrl = document.getElementById('webhook-url').value;
|
|
239
|
+
const webhookToken = document.getElementById('webhook-token').value;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch('/ui/keys/' + id + '/webhook', {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
245
|
+
body: JSON.stringify({ webhook_url: webhookUrl, webhook_token: webhookToken })
|
|
246
|
+
});
|
|
247
|
+
const data = await res.json();
|
|
248
|
+
|
|
249
|
+
if (data.success) {
|
|
250
|
+
closeWebhookModal();
|
|
251
|
+
window.location.reload();
|
|
252
|
+
} else {
|
|
253
|
+
alert(data.error || 'Failed to save webhook');
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
alert('Error: ' + err.message);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
document.getElementById('webhook-modal').addEventListener('click', (e) => {
|
|
261
|
+
if (e.target.classList.contains('modal-overlay')) {
|
|
262
|
+
closeWebhookModal();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
async function deleteKey(id) {
|
|
267
|
+
if (!confirm('Delete this API key? Any agents using it will lose access.')) return;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch('/ui/keys/' + id, {
|
|
271
|
+
method: 'DELETE',
|
|
272
|
+
headers: { 'Accept': 'application/json' }
|
|
273
|
+
});
|
|
274
|
+
const data = await res.json();
|
|
275
|
+
|
|
276
|
+
if (data.success) {
|
|
277
|
+
const row = document.getElementById('key-' + id);
|
|
278
|
+
if (row) row.remove();
|
|
279
|
+
|
|
280
|
+
const tbody = document.getElementById('keys-tbody');
|
|
281
|
+
const count = tbody ? tbody.querySelectorAll('tr').length : 0;
|
|
282
|
+
document.querySelector('.card:last-of-type h3').textContent = 'Existing Keys (' + count + ')';
|
|
283
|
+
|
|
284
|
+
if (count === 0) {
|
|
285
|
+
const table = document.querySelector('.keys-table');
|
|
286
|
+
if (table) {
|
|
287
|
+
table.outerHTML = '<p style="color: #9ca3af; text-align: center; padding: 20px;">No API keys yet. Create one above.</p>';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
alert(data.error || 'Failed to delete');
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
alert('Error: ' + err.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
</script>
|
|
298
|
+
</body>
|
|
299
|
+
</html>`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export default router;
|