344 lines
16 KiB
HTML
344 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<base href="../">
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Crank — Agent Keys</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="css/variables.css">
|
|
<link rel="stylesheet" href="css/layout.css">
|
|
<link rel="stylesheet" href="css/pages.css">
|
|
<style>
|
|
.scope-checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
padding: 9px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-canvas);
|
|
}
|
|
.scope-checkbox-name { font-size: 13px; font-weight: 500; color: var(--text-primary); }
|
|
.scope-checkbox-desc { font-size: 11.5px; color: var(--text-muted); }
|
|
.keys-card-list { display: none; }
|
|
.key-kind-tabs {
|
|
display: inline-flex;
|
|
gap: 4px;
|
|
padding: 4px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
background: var(--bg-overlay);
|
|
}
|
|
.key-kind-tab {
|
|
border: 0;
|
|
border-radius: var(--radius);
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
padding: 7px 12px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.key-kind-tab.active {
|
|
background: var(--accent);
|
|
color: #fff;
|
|
}
|
|
.key-kind-header-control {
|
|
display: grid;
|
|
grid-template-columns: auto auto;
|
|
align-items: center;
|
|
gap: 10px;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
}
|
|
.key-kind-header-hint {
|
|
grid-column: 1 / -1;
|
|
max-width: 520px;
|
|
margin: 0;
|
|
text-align: left;
|
|
}
|
|
.api-keys-page-header {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 14px;
|
|
}
|
|
.api-keys-page-header .page-header-text {
|
|
max-width: 680px;
|
|
}
|
|
.api-keys-page-header .page-header-actions {
|
|
width: 100%;
|
|
}
|
|
.approval-warning-callout {
|
|
display: flex;
|
|
gap: 10px;
|
|
padding: 12px 14px;
|
|
border: 1px solid var(--amber-border);
|
|
border-radius: var(--radius-lg);
|
|
background: var(--amber-bg);
|
|
color: var(--text-primary);
|
|
font-size: 13px;
|
|
line-height: 1.55;
|
|
}
|
|
.approval-warning-callout svg {
|
|
color: var(--amber);
|
|
flex: 0 0 auto;
|
|
margin-top: 2px;
|
|
}
|
|
@media (max-width: 720px) {
|
|
#keys-table-wrap { display: none; }
|
|
.keys-card-list { display: grid; }
|
|
.key-kind-header-control { grid-template-columns: 1fr; justify-content: stretch; width: 100%; }
|
|
.key-kind-tabs { width: 100%; }
|
|
.key-kind-tab { flex: 1; }
|
|
.key-kind-header-hint { text-align: left; }
|
|
}
|
|
</style>
|
|
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
|
|
<nav class="navbar">
|
|
<a href="/" class="nav-logo">
|
|
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
|
<span class="nav-logo-text">Crank</span>
|
|
</a>
|
|
|
|
<!-- Single workspace indicator -->
|
|
<div class="ws-switcher" id="ws-switcher">
|
|
<div class="ws-switcher-trigger">
|
|
<div class="ws-dot" id="ws-dot">A</div>
|
|
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-links">
|
|
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
|
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
|
<a href="/api-keys" class="nav-link active" data-i18n="nav.apikeys">API Keys</a>
|
|
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
|
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
|
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
|
</div>
|
|
|
|
<div class="nav-right">
|
|
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
|
|
|
<div class="nav-divider"></div>
|
|
|
|
<div class="user-menu">
|
|
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
|
<div class="user-dropdown">
|
|
<div class="user-dropdown-header">
|
|
<div class="user-dropdown-name">Crank</div>
|
|
<div class="user-dropdown-role">—</div>
|
|
</div>
|
|
<button class="user-dropdown-item" data-action="settings-link">
|
|
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
|
|
<span data-i18n="nav.settings">Settings</span>
|
|
</button>
|
|
<div class="dropdown-divider"></div>
|
|
<button class="user-dropdown-item danger" data-action="logout">
|
|
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
|
|
<span data-i18n="nav.logout">Log out</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="mobile-nav" hidden>
|
|
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
|
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
|
<a href="/api-keys" class="mobile-nav-link active" data-i18n="nav.apikeys">API Keys</a>
|
|
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
|
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
|
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
|
</div>
|
|
|
|
<!-- ═══════════════════ PAGE ═══════════════════ -->
|
|
<div class="page">
|
|
|
|
<div class="page-header api-keys-page-header">
|
|
<div class="page-header-text">
|
|
<h1 class="page-title" data-i18n="apikeys.title">Agent Keys</h1>
|
|
<p class="page-subtitle" data-i18n="apikeys.subtitle">These keys connect an MCP client to the MCP server and are issued for a specific agent.</p>
|
|
</div>
|
|
<div class="page-header-actions">
|
|
<div class="key-kind-header-control">
|
|
<div class="key-kind-tabs" role="tablist" aria-label="Key type">
|
|
<button class="key-kind-tab active" id="key-kind-mcp-client" type="button" data-key-kind="mcp_client" data-i18n="apikeys.kind.mcp">MCP clients</button>
|
|
<button class="key-kind-tab" id="key-kind-approval" type="button" data-key-kind="approval" data-i18n="apikeys.kind.approval">Approvals</button>
|
|
</div>
|
|
<button class="btn-primary" id="btn-create-key" type="button">
|
|
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
|
<span id="btn-create-key-label" data-i18n="apikeys.new">Create key</span>
|
|
</button>
|
|
<div class="field-hint key-kind-header-hint" id="key-kind-hint"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="callout info">
|
|
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="8" cy="8" r="6.5"/>
|
|
<path d="M8 11V8M8 5.5V5"/>
|
|
</svg>
|
|
<div>
|
|
<strong data-i18n="apikeys.callout.title">Keys are only shown once.</strong>
|
|
<span data-i18n="apikeys.callout.body">Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="section-card-header">
|
|
<div>
|
|
<div class="section-card-title" data-i18n="apikeys.agent.title">Agent selection</div>
|
|
<div class="section-card-subtitle" id="agent-access-subtitle" data-i18n="apikeys.agent.subtitle">Select the AI agent this key is issued for.</div>
|
|
</div>
|
|
</div>
|
|
<div class="section-card-body" style="display:grid;gap:12px;">
|
|
<div class="field-group" style="margin-bottom:0;">
|
|
<label class="field-label" for="agent-select" data-i18n="apikeys.agent.label">AI agent</label>
|
|
<select class="field-input" id="agent-select"></select>
|
|
<div class="field-hint" id="agent-endpoint-hint" data-testid="agent-key-endpoint-hint"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="section-card-header">
|
|
<div>
|
|
<div class="section-card-title" data-i18n="apikeys.active.title">Active keys</div>
|
|
<div class="section-card-subtitle" id="keys-summary-subtitle">3 active · 1 revoked</div>
|
|
</div>
|
|
<div class="filter-bar-search" style="max-width: 220px; margin: 0;">
|
|
<svg width="13" height="13" style="position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--text-muted);" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215zM11.5 7a4.499 4.499 0 11-8.997 0A4.499 4.499 0 0111.5 7z"/></svg>
|
|
<input type="text" id="key-search" data-i18n-ph="apikeys.search" placeholder="Filter keys…" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="keys-table-wrap">
|
|
<table class="data-table" id="keys-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="apikeys.th.name">Name</th>
|
|
<th data-i18n="apikeys.th.prefix">Key prefix</th>
|
|
<th data-i18n="apikeys.th.scopes">Access</th>
|
|
<th data-i18n="apikeys.th.created">Created</th>
|
|
<th data-i18n="apikeys.th.last">Last used</th>
|
|
<th data-i18n="apikeys.th.status">Status</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="keys-tbody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="resource-list keys-card-list" id="keys-card-list"></div>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="section-card-header">
|
|
<div class="section-card-title" data-i18n="apikeys.scope_ref">Access reference</div>
|
|
</div>
|
|
<div class="section-card-body" id="scope-reference-grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /page -->
|
|
|
|
<!-- ══ Create Key Modal ══ -->
|
|
<div class="modal-overlay" id="modal-create">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<span class="modal-title" id="modal-create-title" data-i18n="apikeys.modal.title">Create agent key</span>
|
|
<button class="modal-close" id="modal-close-btn" type="button">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
|
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body" id="modal-form-body">
|
|
<div class="field-group">
|
|
<label class="field-label" data-i18n="apikeys.modal.name">Key name</label>
|
|
<input class="field-input" id="new-key-name" type="text" data-i18n-ph="apikeys.modal.name_placeholder" placeholder="e.g. Production, CI pipeline" autocomplete="off">
|
|
<div class="field-hint" data-i18n="apikeys.modal.name_hint">A descriptive label to identify the key. Only visible to admins.</div>
|
|
</div>
|
|
<div class="approval-warning-callout" id="approval-key-warning" hidden>
|
|
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
|
</svg>
|
|
<div data-i18n="apikeys.approval.warning">Do not pass this key to an LLM or MCP client. It is only for an external interface where a human confirms an action.</div>
|
|
</div>
|
|
<div class="field-group" style="margin-bottom:0;">
|
|
<label class="field-label" data-i18n="apikeys.modal.scopes">Access</label>
|
|
<div style="display:flex;flex-direction:column;gap:8px;margin-top:2px;" id="scope-checkboxes">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body" id="modal-reveal-body" hidden>
|
|
<div class="callout warning" style="margin-bottom:16px;">
|
|
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
|
</svg>
|
|
<div><strong data-i18n="apikeys.modal.reveal_title">Copy this key now.</strong> <span data-i18n="apikeys.modal.reveal_body">It won't be shown again.</span></div>
|
|
</div>
|
|
<div class="key-reveal">
|
|
<span class="key-mask" id="reveal-key-value">mcp_prod_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span>
|
|
<button class="key-copy" id="copy-key-btn" type="button" data-i18n-title="apikeys.modal.copy" title="Copy to clipboard">
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="5" y="5" width="9" height="9" rx="1.5"/>
|
|
<path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer" id="modal-footer-create">
|
|
<button class="btn-secondary" id="modal-cancel-btn" type="button" data-i18n="apikeys.modal.cancel">Cancel</button>
|
|
<button class="btn-primary" id="modal-confirm-btn" type="button" data-i18n="apikeys.modal.create">Create key</button>
|
|
</div>
|
|
<div class="modal-footer" id="modal-footer-done" hidden>
|
|
<button class="btn-primary" id="modal-done-btn" type="button" data-i18n="apikeys.modal.done">Done — I've copied the key</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Template: scope checkbox row -->
|
|
<template id="tmpl-scope-checkbox">
|
|
<label class="scope-checkbox-label">
|
|
<input type="checkbox" data-scope="" style="width:14px;height:14px;accent-color:var(--accent);cursor:pointer;flex-shrink:0;">
|
|
<div>
|
|
<div class="scope-checkbox-name"></div>
|
|
<div class="scope-checkbox-desc"></div>
|
|
</div>
|
|
</label>
|
|
</template>
|
|
|
|
<!-- Template: API key table row -->
|
|
<template id="tmpl-key-row">
|
|
<tr>
|
|
<td class="col-name"></td>
|
|
<td class="col-mono"></td>
|
|
<td><div class="col-scopes" style="display:flex;gap:4px;flex-wrap:wrap;"></div></td>
|
|
<td class="col-created"></td>
|
|
<td class="col-last-used"></td>
|
|
<td class="col-status"></td>
|
|
<td class="col-actions">
|
|
<span class="actions-active">
|
|
<button class="btn-icon" data-i18n-title="apikeys.action.copy_prefix" title="Copy key prefix"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/></svg></button>
|
|
<button class="btn-icon danger" data-i18n-title="apikeys.action.revoke" title="Revoke key"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.5"/><path d="M5.5 10.5l5-5M10.5 10.5l-5-5"/></svg></button>
|
|
</span>
|
|
<span class="actions-revoked" hidden>
|
|
<button class="btn-icon" data-i18n-title="apikeys.action.delete" title="Delete"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2h4M2.5 4h11M5 4l.5 9h5l.5-9"/></svg></button>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
|
|
<script src="%CRANK_BUNDLE_API_KEYS%"></script>
|
|
|
|
</body>
|
|
</html>
|