/* ═══════════════════════════════════════════════════ Crank — API Keys page Data: GET /api/workspaces/{slug}/keys ═══════════════════════════════════════════════════ */ var KEYS = []; var currentWorkspaceSlug = ''; var SCOPES = ['read', 'write', 'deploy']; var SCOPE_DESC = { read: 'Read operations, logs, and usage', write: 'Create and update operations', deploy: 'Publish and rollback deployments', }; var selectedScopes = new Set(['read']); var search = ''; // ── API helpers ────────────────────────────────────────────────────────────── function apiBase() { return '/api/workspaces/' + currentWorkspaceSlug + '/keys'; } // In production: replace fetch(mockUrl()) with fetch(apiBase()) function mockUrl() { return (window.DATA_URL || 'data/') + 'keys.json'; } // ── Load keys for current workspace ───────────────────────────────────────── function loadKeys() { currentWorkspaceSlug = getCurrentWs().slug; setTableLoading(true); // Production: fetch(apiBase()) fetch(mockUrl()) .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function(data) { // Mock JSON is keyed by workspace slug; real API returns array directly KEYS = Array.isArray(data) ? data : (data[currentWorkspaceSlug] || []); setTableLoading(false); renderTable(); }) .catch(function() { KEYS = []; setTableLoading(false); renderTable(); }); } function setTableLoading(on) { var tbody = document.getElementById('keys-tbody'); if (!on) return; tbody.innerHTML = ''; var tr = document.createElement('tr'); var td = document.createElement('td'); td.colSpan = 7; td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);'; td.textContent = 'Loading…'; tr.appendChild(td); tbody.appendChild(tr); } // ── Mutations (in production: real fetch calls) ────────────────────────────── function revokeKey(id) { if (!confirm('Revoke this key? All requests using it will fail immediately.')) return; // Production: fetch(apiBase() + '/' + id, { method: 'PATCH', body: JSON.stringify({ status: 'revoked' }) }) var k = KEYS.find(function(k) { return k.id === id; }); if (k) { k.status = 'revoked'; renderTable(); } } function deleteKey(id) { // Production: fetch(apiBase() + '/' + id, { method: 'DELETE' }) KEYS.splice(KEYS.findIndex(function(k) { return k.id === id; }), 1); renderTable(); } function createKey(name, scopes) { // Production: fetch(apiBase(), { method: 'POST', body: JSON.stringify({ name, scopes }) }) // → response: { key: 'mcp_...', prefix: 'mcp_...', id: '...' } var rand = Array.from({length: 32}, function() { return Math.random().toString(36)[2]; }).join(''); var rawKey = 'mcp_' + name.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 4) + '_' + rand; var prefix = rawKey.slice(0, 12) + '…'; var newKey = { id: 'key_' + Date.now(), name: name, prefix: prefix, scopes: scopes, created: new Date().toISOString().slice(0, 10), lastUsed: 'Never', status: 'active', }; KEYS.unshift(newKey); return rawKey; } // ── Render ─────────────────────────────────────────────────────────────────── function renderScopes() { var el = document.getElementById('scope-checkboxes'); var tmpl = document.getElementById('tmpl-scope-checkbox'); el.innerHTML = ''; SCOPES.forEach(function(s) { var node = tmpl.content.cloneNode(true); var input = node.querySelector('input'); input.dataset.scope = s; if (selectedScopes.has(s)) input.checked = true; node.querySelector('.scope-checkbox-name').textContent = s; node.querySelector('.scope-checkbox-desc').textContent = SCOPE_DESC[s]; input.addEventListener('change', function() { if (this.checked) selectedScopes.add(this.dataset.scope); else selectedScopes.delete(this.dataset.scope); }); el.appendChild(node); }); } function renderTable() { var q = search.toLowerCase(); var tbody = document.getElementById('keys-tbody'); var rows = KEYS.filter(function(k) { return !q || k.name.toLowerCase().includes(q) || k.prefix.includes(q); }); tbody.innerHTML = ''; if (!rows.length) { var empty = document.createElement('tr'); var td = document.createElement('td'); td.colSpan = 7; td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);'; td.textContent = KEYS.length ? 'No keys match your search' : 'No API keys yet'; empty.appendChild(td); tbody.appendChild(empty); return; } var tmpl = document.getElementById('tmpl-key-row'); rows.forEach(function(k) { var node = tmpl.content.cloneNode(true); node.querySelector('.col-name').textContent = k.name; node.querySelector('.col-mono').textContent = k.prefix; node.querySelector('.col-created').textContent = k.created; node.querySelector('.col-last-used').textContent = k.lastUsed; var scopesEl = node.querySelector('.col-scopes'); k.scopes.forEach(function(s) { var span = document.createElement('span'); span.className = 'badge badge-scope'; span.textContent = s; scopesEl.appendChild(span); }); var badge = document.createElement('span'); badge.className = k.status === 'active' ? 'badge badge-active' : 'badge badge-revoked'; badge.textContent = k.status === 'active' ? 'Active' : 'Revoked'; node.querySelector('.col-status').appendChild(badge); var actionsActive = node.querySelector('.actions-active'); var actionsRevoked = node.querySelector('.actions-revoked'); if (k.status === 'active') { actionsActive.querySelector('[title="Copy key prefix"]') .addEventListener('click', function() { copyPrefix(k.prefix); }); actionsActive.querySelector('[title="Revoke key"]') .addEventListener('click', function() { revokeKey(k.id); }); } else { actionsActive.style.display = 'none'; actionsRevoked.style.display = ''; actionsRevoked.querySelector('[title="Delete"]') .addEventListener('click', function() { deleteKey(k.id); }); } tbody.appendChild(node); }); } // ── Modal ──────────────────────────────────────────────────────────────────── var modal = document.getElementById('modal-create'); function openModal() { document.getElementById('modal-form-body').style.display = ''; document.getElementById('modal-reveal-body').style.display = 'none'; document.getElementById('modal-footer-create').style.display = ''; document.getElementById('modal-footer-done').style.display = 'none'; document.getElementById('new-key-name').value = ''; selectedScopes = new Set(['read']); renderScopes(); modal.classList.add('open'); setTimeout(function() { document.getElementById('new-key-name').focus(); }, 50); } function closeModal() { modal.classList.remove('open'); } document.getElementById('btn-create-key').addEventListener('click', openModal); document.getElementById('modal-close-btn').addEventListener('click', closeModal); document.getElementById('modal-cancel-btn').addEventListener('click', closeModal); modal.addEventListener('click', function(e) { if (e.target === modal) closeModal(); }); document.getElementById('modal-confirm-btn').addEventListener('click', function() { var name = document.getElementById('new-key-name').value.trim(); if (!name) { document.getElementById('new-key-name').focus(); return; } if (!selectedScopes.size) { alert('Select at least one scope.'); return; } var rawKey = createKey(name, Array.from(selectedScopes)); document.getElementById('reveal-key-value').textContent = rawKey; document.getElementById('modal-form-body').style.display = 'none'; document.getElementById('modal-reveal-body').style.display = ''; document.getElementById('modal-footer-create').style.display = 'none'; document.getElementById('modal-footer-done').style.display = ''; renderTable(); }); document.getElementById('modal-done-btn').addEventListener('click', closeModal); document.getElementById('copy-key-btn').addEventListener('click', function() { var val = document.getElementById('reveal-key-value').textContent; navigator.clipboard && navigator.clipboard.writeText(val); this.innerHTML = ''; }); document.getElementById('key-search').addEventListener('input', function() { search = this.value; renderTable(); }); // ── Init ───────────────────────────────────────────────────────────────────── function copyPrefix(prefix) { navigator.clipboard && navigator.clipboard.writeText(prefix); } document.addEventListener('DOMContentLoaded', loadKeys);