Files
2026-06-24 11:39:39 +00:00

647 lines
20 KiB
JavaScript

var KEYS = [];
var AGENTS = [];
var currentWorkspaceId = null;
var currentAgentId = null;
var activeKeyKind = 'mcp_client';
var selectedScopes = new Set(['read']);
var search = '';
var SCOPES_BY_KIND = {
mcp_client: ['read', 'write', 'deploy'],
approval: ['read_pending', 'approve', 'deny'],
};
var SCOPE_DESC = {
read: 'apikeys.scope.read',
write: 'apikeys.scope.write',
deploy: 'apikeys.scope.deploy',
approve: 'apikeys.scope.approve',
deny: 'apikeys.scope.deny',
read_pending: 'apikeys.scope.read_pending',
};
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,
keyKind: apiKey.key_kind || 'mcp_client',
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,
key_kind: activeKeyKind,
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;
var label = document.getElementById('btn-create-key-label');
if (label) {
label.textContent = activeKeyKind === 'approval'
? tKey('apikeys.new_approval')
: tKey('apikeys.new_mcp');
}
}
function renderScopes() {
var el = document.getElementById('scope-checkboxes');
var tmpl = document.getElementById('tmpl-scope-checkbox');
el.innerHTML = '';
(SCOPES_BY_KIND[activeKeyKind] || []).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 renderKeyKindTabs() {
document.querySelectorAll('[data-key-kind]').forEach(function(button) {
var kind = button.dataset.keyKind;
button.classList.toggle('active', kind === activeKeyKind);
button.setAttribute('aria-selected', kind === activeKeyKind ? 'true' : 'false');
});
var hint = document.getElementById('key-kind-hint');
if (hint) {
hint.textContent = activeKeyKind === 'approval'
? tKey('apikeys.kind.approval_hint')
: tKey('apikeys.kind.mcp_hint');
}
renderScopeReference();
setCreateButtonState();
}
function renderScopeReference() {
var grid = document.getElementById('scope-reference-grid');
if (!grid) return;
grid.innerHTML = '';
(SCOPES_BY_KIND[activeKeyKind] || []).forEach(function(scope) {
var item = document.createElement('div');
var title = document.createElement('div');
title.style.cssText = 'font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:4px;';
var badge = document.createElement('span');
badge.className = 'badge badge-scope';
badge.textContent = scope;
title.appendChild(badge);
var description = document.createElement('div');
description.style.cssText = 'font-size:12px;color:var(--text-muted);line-height:1.55;';
description.textContent = tKey(SCOPE_DESC[scope]);
item.appendChild(title);
item.appendChild(description);
grid.appendChild(item);
});
}
function renderTable(errorMessage) {
var q = search.toLowerCase();
var tbody = document.getElementById('keys-tbody');
var cardList = document.getElementById('keys-card-list');
var visibleKeys = KEYS.filter(function(key) { return key.keyKind === activeKeyKind; });
var rows = visibleKeys.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 = visibleKeys.filter(function(key) { return key.status === 'active'; }).length;
var revoked = visibleKeys.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
? (visibleKeys.length ? tKey('apikeys.empty.search') : emptyTextForKind())
: 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
? (visibleKeys.length ? tKey('apikeys.empty.search') : emptyTextForKind())
: 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 emptyTextForKind() {
return activeKeyKind === 'approval'
? tKey('apikeys.empty.approval')
: tKey('apikeys.empty.none');
}
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([activeKeyKind === 'approval' ? 'approve' : 'read']);
document.getElementById('modal-create-title').textContent = activeKeyKind === 'approval'
? tKey('apikeys.modal.title_approval')
: tKey('apikeys.modal.title_mcp');
document.getElementById('approval-key-warning').hidden = activeKeyKind !== 'approval';
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();
});
document.querySelectorAll('[data-key-kind]').forEach(function(button) {
button.addEventListener('click', function() {
activeKeyKind = this.dataset.keyKind || 'mcp_client';
search = '';
document.getElementById('key-search').value = '';
renderKeyKindTabs();
renderTable();
});
});
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() {
renderKeyKindTabs();
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await loadKeys();
window.addEventListener('crank:workspacechange', function() {
loadKeys();
});
});