var KEYS = []; var currentWorkspaceId = null; var selectedScopes = new Set(['read']); var search = ''; var SCOPES = ['read', 'write', 'deploy']; var SCOPE_DESC = { read: 'Read operations, logs, and usage', write: 'Create and update operations', deploy: 'Publish agents and operations', }; function mapKeyRecord(record) { var apiKey = record.api_key || {}; return { id: apiKey.id, name: apiKey.name, prefix: apiKey.prefix || '', scopes: apiKey.scopes || [], created: apiKey.created_at ? apiKey.created_at.slice(0, 10) : '—', lastUsed: apiKey.last_used_at ? apiKey.last_used_at.slice(0, 10) : 'Never', status: apiKey.status || 'active', }; } function currentWorkspace() { return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null; } async function loadKeys() { var workspace = currentWorkspace(); currentWorkspaceId = workspace ? workspace.id : null; setTableLoading(true); if (!currentWorkspaceId || !window.CrankApi) { KEYS = []; setTableLoading(false); renderTable('Workspace or API is unavailable'); return; } try { var response = await window.CrankApi.listPlatformApiKeys(currentWorkspaceId); KEYS = ((response && response.items) || []).map(mapKeyRecord); setTableLoading(false); renderTable(); } catch (error) { KEYS = []; setTableLoading(false); renderTable(error.message || 'Failed to load API keys'); } } 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); } async function revokeKey(id) { if (!confirm('Revoke this key? All requests using it will fail immediately.')) return; try { await window.CrankApi.revokePlatformApiKey(currentWorkspaceId, id); await loadKeys(); } catch (error) { alert(error.message || 'Failed to revoke key'); } } async function deleteKey(id) { if (!confirm('Delete this key? This cannot be undone.')) return; try { await window.CrankApi.deletePlatformApiKey(currentWorkspaceId, id); await loadKeys(); } catch (error) { alert(error.message || 'Failed to delete key'); } } async function createKey(name, scopes) { var created = await window.CrankApi.createPlatformApiKey(currentWorkspaceId, { name: name, scopes: scopes, }); return { rawKey: created.secret, record: mapKeyRecord(created.api_key), }; } function renderScopes() { var el = document.getElementById('scope-checkboxes'); var tmpl = document.getElementById('tmpl-scope-checkbox'); el.innerHTML = ''; SCOPES.forEach(function(scope) { var node = tmpl.content.cloneNode(true); var input = node.querySelector('input'); input.dataset.scope = scope; if (selectedScopes.has(scope)) input.checked = true; node.querySelector('.scope-checkbox-name').textContent = scope; node.querySelector('.scope-checkbox-desc').textContent = SCOPE_DESC[scope]; input.addEventListener('change', function() { if (this.checked) selectedScopes.add(this.dataset.scope); else selectedScopes.delete(this.dataset.scope); }); el.appendChild(node); }); } function renderTable(errorMessage) { var q = search.toLowerCase(); var tbody = document.getElementById('keys-tbody'); var rows = KEYS.filter(function(key) { return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q); }); var subtitle = document.querySelector('.section-card-subtitle'); tbody.innerHTML = ''; if (subtitle) { var active = KEYS.filter(function(key) { return key.status === 'active'; }).length; var revoked = KEYS.filter(function(key) { return key.status === 'revoked'; }).length; subtitle.textContent = active + ' active · ' + revoked + ' revoked'; } if (errorMessage) { var errorRow = document.createElement('tr'); var errorCell = document.createElement('td'); errorCell.colSpan = 7; errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);'; errorCell.textContent = errorMessage; errorRow.appendChild(errorCell); tbody.appendChild(errorRow); return; } 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(key) { var node = tmpl.content.cloneNode(true); node.querySelector('.col-name').textContent = key.name; node.querySelector('.col-mono').textContent = key.prefix; node.querySelector('.col-created').textContent = key.created; node.querySelector('.col-last-used').textContent = key.lastUsed; var scopesEl = node.querySelector('.col-scopes'); key.scopes.forEach(function(scope) { var span = document.createElement('span'); span.className = 'badge badge-scope'; span.textContent = scope; scopesEl.appendChild(span); }); var badge = document.createElement('span'); badge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked'; badge.textContent = key.status === 'active' ? 'Active' : 'Revoked'; node.querySelector('.col-status').appendChild(badge); var actionsActive = node.querySelector('.actions-active'); var actionsRevoked = node.querySelector('.actions-revoked'); if (key.status === 'active') { actionsActive.querySelector('[title=\"Copy key prefix\"]').addEventListener('click', function() { copyPrefix(key.prefix); }); actionsActive.querySelector('[title=\"Revoke key\"]').addEventListener('click', function() { revokeKey(key.id); }); } else { actionsActive.style.display = 'none'; actionsRevoked.style.display = ''; actionsRevoked.querySelector('[title=\"Delete\"]').addEventListener('click', function() { deleteKey(key.id); }); } tbody.appendChild(node); }); } 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(event) { if (event.target === modal) closeModal(); }); document.getElementById('modal-confirm-btn').addEventListener('click', async 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; } try { var created = await createKey(name, Array.from(selectedScopes)); KEYS.unshift(created.record); document.getElementById('reveal-key-value').textContent = created.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(); } catch (error) { alert(error.message || 'Failed to create key'); } }); document.getElementById('modal-done-btn').addEventListener('click', closeModal); document.getElementById('copy-key-btn').addEventListener('click', function() { var value = document.getElementById('reveal-key-value').textContent; if (navigator.clipboard) { navigator.clipboard.writeText(value).catch(function() {}); } this.innerHTML = ''; }); document.getElementById('key-search').addEventListener('input', function() { search = this.value; renderTable(); }); function copyPrefix(prefix) { if (navigator.clipboard) { navigator.clipboard.writeText(prefix).catch(function() {}); } } document.addEventListener('DOMContentLoaded', async function() { await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve()); await loadKeys(); window.addEventListener('crank:workspacechange', function() { loadKeys(); }); });