var KEYS = []; var AGENTS = []; var currentWorkspaceId = null; var currentAgentId = null; var selectedScopes = new Set(['read']); var search = ''; var SCOPES = ['read', 'write', 'deploy']; var SCOPE_DESC = { read: 'apikeys.scope.read', write: 'apikeys.scope.write', deploy: 'apikeys.scope.deploy', }; function tKey(key) { return window.t ? t(key) : key; } function tfKey(key, vars) { return window.tf ? tf(key, vars) : tKey(key); } function buildIconSvg(href, width, height) { var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', String(width)); svg.setAttribute('height', String(height)); var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttribute('href', href); svg.appendChild(use); return svg; } function mapKeyRecord(record) { var apiKey = record.api_key || {}; return { id: apiKey.id, agentId: apiKey.agent_id || null, 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) : tKey('apikeys.last_used.never'), status: apiKey.status || 'active', }; } function mapAgentRecord(record) { return { id: record.id, slug: record.slug, displayName: record.display_name || record.slug || record.id, mcpEndpoint: record.mcp_endpoint || '', }; } 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) { AGENTS = []; KEYS = []; currentAgentId = null; renderAgentPicker(); setCreateButtonState(); setTableLoading(false); renderTable(tKey('apikeys.error.api')); return; } try { var response = await window.CrankApi.listAgents(currentWorkspaceId); AGENTS = ((response && response.items) || []).map(mapAgentRecord); if (!AGENTS.length) { currentAgentId = null; KEYS = []; renderAgentPicker(); setCreateButtonState(); setTableLoading(false); renderTable(); return; } if (!currentAgentId || !AGENTS.some(function(agent) { return agent.id === currentAgentId; })) { currentAgentId = AGENTS[0].id; } renderAgentPicker(); var keysResponse = await window.CrankApi.listAgentPlatformApiKeys( currentWorkspaceId, currentAgentId ); KEYS = ((keysResponse && keysResponse.items) || []).map(mapKeyRecord); setCreateButtonState(); setTableLoading(false); renderTable(); } catch (error) { AGENTS = []; KEYS = []; currentAgentId = null; renderAgentPicker(); setCreateButtonState(); setTableLoading(false); renderTable(error.message || tKey('apikeys.error.load')); } } 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 = tKey('apikeys.loading'); tr.appendChild(td); tbody.appendChild(tr); } async function revokeKey(id) { if (!confirm(tKey('apikeys.confirm.revoke'))) return; if (!currentAgentId) return; try { var key = KEYS.find(function(item) { return item.id === id; }); await window.CrankApi.revokeAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id); await loadKeys(); if (window.CrankUi) { window.CrankUi.success( tfKey('apikeys.toast.revoke_message', { name: key ? key.name : '' }), tKey('apikeys.toast.revoke_title') ); } } catch (error) { if (window.CrankUi) { window.CrankUi.error( error.message || tKey('apikeys.toast.revoke_error_message'), tKey('apikeys.toast.revoke_error_title') ); } } } async function deleteKey(id) { if (!confirm(tKey('apikeys.confirm.delete'))) return; if (!currentAgentId) return; try { var key = KEYS.find(function(item) { return item.id === id; }); await window.CrankApi.deleteAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id); await loadKeys(); if (window.CrankUi) { window.CrankUi.success( tfKey('apikeys.toast.delete_message', { name: key ? key.name : '' }), tKey('apikeys.toast.delete_title') ); } } catch (error) { if (window.CrankUi) { window.CrankUi.error( error.message || tKey('apikeys.toast.delete_error_message'), tKey('apikeys.toast.delete_error_title') ); } } } async function createKey(name, scopes) { var created = await window.CrankApi.createAgentPlatformApiKey(currentWorkspaceId, currentAgentId, { name: name, scopes: scopes, }); return { rawKey: created.secret, record: mapKeyRecord(created.api_key), }; } function currentAgent() { return AGENTS.find(function(agent) { return agent.id === currentAgentId; }) || null; } function renderAgentPicker() { var select = document.getElementById('agent-select'); var hint = document.getElementById('agent-endpoint-hint'); if (!select || !hint) return; select.innerHTML = ''; if (!AGENTS.length) { var emptyOption = document.createElement('option'); emptyOption.value = ''; emptyOption.textContent = tKey('apikeys.agent.empty'); select.appendChild(emptyOption); select.disabled = true; hint.textContent = tKey('apikeys.agent.empty_hint'); return; } AGENTS.forEach(function(agent) { var option = document.createElement('option'); option.value = agent.id; option.textContent = agent.displayName; if (agent.id === currentAgentId) option.selected = true; select.appendChild(option); }); select.disabled = false; var agent = currentAgent(); hint.textContent = agent && agent.mcpEndpoint ? tfKey('apikeys.agent.endpoint', { endpoint: agent.mcpEndpoint }) : tKey('apikeys.agent.endpoint_missing'); } function setCreateButtonState() { var button = document.getElementById('btn-create-key'); if (!button) return; button.disabled = !currentAgentId; } 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 = tKey(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 cardList = document.getElementById('keys-card-list'); var rows = KEYS.filter(function(key) { return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q); }); var subtitle = document.getElementById('keys-summary-subtitle'); tbody.innerHTML = ''; if (cardList) { cardList.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 = currentAgentId ? tfKey('apikeys.active.subtitle', { active: active, revoked: revoked }) : tKey('apikeys.agent.empty_hint'); } 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); renderKeyCards([], errorMessage); 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 = currentAgentId ? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none')) : tKey('apikeys.agent.empty_hint'); empty.appendChild(td); tbody.appendChild(empty); renderKeyCards(rows); 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' ? tKey('apikeys.status.active') : tKey('apikeys.status.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.hidden = true; actionsRevoked.hidden = false; actionsRevoked.querySelector('[title=\"Delete\"]').addEventListener('click', function() { deleteKey(key.id); }); } tbody.appendChild(node); }); renderKeyCards(rows); } function renderKeyCards(rows, errorMessage) { var cardList = document.getElementById('keys-card-list'); if (!cardList) return; cardList.innerHTML = ''; if (errorMessage) { cardList.appendChild(buildKeyCardMessage(errorMessage, true)); return; } if (!rows.length) { cardList.appendChild( buildKeyCardMessage( currentAgentId ? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none')) : tKey('apikeys.agent.empty_hint'), false ) ); return; } rows.forEach(function(key) { var card = document.createElement('div'); card.className = 'resource-card'; var header = document.createElement('div'); header.className = 'resource-card-header'; var headerMain = document.createElement('div'); var title = document.createElement('div'); title.className = 'resource-card-title'; title.textContent = key.name; var subtitle = document.createElement('div'); subtitle.className = 'resource-card-subtitle'; subtitle.textContent = key.prefix; headerMain.appendChild(title); headerMain.appendChild(subtitle); var actions = document.createElement('div'); actions.className = 'resource-card-actions'; var statusBadge = document.createElement('span'); statusBadge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked'; statusBadge.textContent = key.status === 'active' ? tKey('apikeys.status.active') : tKey('apikeys.status.revoked'); actions.appendChild(statusBadge); header.appendChild(headerMain); header.appendChild(actions); card.appendChild(header); var metaGrid = document.createElement('div'); metaGrid.className = 'resource-meta-grid'; metaGrid.appendChild(buildMetaItem('apikeys.th.scopes', key.scopes.join(', ') || '—')); metaGrid.appendChild(buildMetaItem('apikeys.th.created', key.created)); metaGrid.appendChild(buildMetaItem('apikeys.th.last', key.lastUsed)); card.appendChild(metaGrid); var actionRow = document.createElement('div'); actionRow.className = 'resource-card-actions'; if (key.status === 'active') { actionRow.appendChild(buildCardAction('apikeys.action.copy_prefix', function() { copyPrefix(key.prefix); })); actionRow.appendChild(buildCardAction('apikeys.action.revoke', function() { revokeKey(key.id); }, true)); } else { actionRow.appendChild(buildCardAction('apikeys.action.delete', function() { deleteKey(key.id); }, true)); } card.appendChild(actionRow); cardList.appendChild(card); }); } function buildKeyCardMessage(text, isError) { var card = document.createElement('div'); card.className = 'resource-card'; var value = document.createElement('div'); value.className = 'resource-meta-value'; value.textContent = text; value.style.color = isError ? 'var(--danger,#f85149)' : 'var(--text-muted)'; card.appendChild(value); return card; } function buildMetaItem(labelKey, valueText) { var item = document.createElement('div'); item.className = 'resource-meta-item'; var label = document.createElement('div'); label.className = 'resource-meta-label'; label.textContent = tKey(labelKey); var value = document.createElement('div'); value.className = 'resource-meta-value'; value.textContent = valueText; item.appendChild(label); item.appendChild(value); return item; } function buildCardAction(labelKey, onClick, isDanger) { var button = document.createElement('button'); button.type = 'button'; button.className = isDanger ? 'btn-secondary' : 'btn-secondary'; button.textContent = tKey(labelKey); button.addEventListener('click', onClick); return button; } var modal = document.getElementById('modal-create'); function openModal() { if (!currentAgentId) return; document.getElementById('modal-form-body').hidden = false; document.getElementById('modal-reveal-body').hidden = true; document.getElementById('modal-footer-create').hidden = false; document.getElementById('modal-footer-done').hidden = true; 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 button = this; var name = document.getElementById('new-key-name').value.trim(); if (!name) { document.getElementById('new-key-name').focus(); return; } if (!selectedScopes.size) { if (window.CrankUi) { window.CrankUi.info( tKey('apikeys.toast.scope_missing_message'), tKey('apikeys.toast.scope_missing_title') ); } return; } try { button.disabled = true; button.textContent = tKey('apikeys.creating'); 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').hidden = true; document.getElementById('modal-reveal-body').hidden = false; document.getElementById('modal-footer-create').hidden = true; document.getElementById('modal-footer-done').hidden = false; renderTable(); if (window.CrankUi) { window.CrankUi.success( tKey('apikeys.toast.create_message'), tKey('apikeys.toast.create_title') ); } } catch (error) { if (window.CrankUi) { window.CrankUi.error( error.message || tKey('apikeys.toast.create_error_message'), tKey('apikeys.toast.create_error_title') ); } } finally { button.disabled = false; button.textContent = tKey('apikeys.modal.create'); } }); 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.replaceChildren( buildIconSvg( (window.APP_BASE || '') + 'icons/general/check.svg#icon', 13, 13 ) ); if (window.CrankUi) { window.CrankUi.info( tKey('apikeys.toast.copy_message'), tKey('apikeys.toast.copy_title') ); } }); document.getElementById('key-search').addEventListener('input', function() { search = this.value; renderTable(); }); document.getElementById('agent-select').addEventListener('change', async function() { currentAgentId = this.value || null; await loadKeys(); }); function copyPrefix(prefix) { if (navigator.clipboard) { navigator.clipboard.writeText(prefix).catch(function() {}); } if (window.CrankUi) { window.CrankUi.info(prefix, tKey('apikeys.toast.prefix_title')); } } document.addEventListener('DOMContentLoaded', async function() { await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve()); await loadKeys(); window.addEventListener('crank:workspacechange', function() { loadKeys(); }); });