1/** 2 * Usage API Keys â table rendering, CRUD, permission summary. 3 */ 4 5import { getEffectiveApiKey, isAuthenticated, getClient, getUsageKeysStore, setUsageKeysStore, getGroupsStore, setStat, updateStatCards, maskApiKey, LIST_PAGE_SIZE } from './auth.js'; 6import { escapeHtml, showStatus, hideStatus, showActionProgress, closeActionProgress, openModal, closeModal, confirmDelete, copyToClipboard, formatError, logError, ICON_PENCIL, ICON_TRASH, ICON_COPY } from './ui-utils.js'; 7import { buildGroupMultiSelect, attachGroupMultiSelectLogic, updateMultiSelectSummary, getSelectedGroupIds } from './groups.js'; 8 9// ----- Helpers ----- 10 11export function normalizeUsageKeyItem(item) { 12 return { 13 id: item.id, 14 usage_api_key: item.usage_api_key ?? item.api_key, 15 name: item.name, 16 description: item.description, 17 expiration: item.expiration, 18 can_create_groups: item.can_create_groups ?? false, 19 can_delete_groups: item.can_delete_groups ?? false, 20 can_create_pkps: item.can_create_pkps ?? false, 21 can_manage_ipfs_ids_in_groups: item.can_manage_ipfs_ids_in_groups ?? [], 22 can_add_pkp_to_groups: item.can_add_pkp_to_groups ?? [], 23 can_remove_pkp_from_groups: item.can_remove_pkp_from_groups ?? [], 24 can_execute_in_groups: item.can_execute_in_groups ?? [], 25 }; 26} 27 28export function renderPermissionSummary(item) { 29 const parts = []; 30 if (item.can_create_groups) parts.push('create groups'); 31 if (item.can_delete_groups) parts.push('delete groups'); 32 if (item.can_create_pkps) parts.push('create PKPs'); 33 const fmtGroups = (ids) => (ids && ids.length > 0) ? (ids.includes(0) ? 'all' : ids.join(', ')) : null; 34 const exec = fmtGroups(item.can_execute_in_groups); 35 if (exec) parts.push('execute: ' + exec); 36 const manage = fmtGroups(item.can_manage_ipfs_ids_in_groups); 37 if (manage) parts.push('manage actions: ' + manage); 38 const addPkp = fmtGroups(item.can_add_pkp_to_groups); 39 if (addPkp) parts.push('add PKP: ' + addPkp); 40 const removePkp = fmtGroups(item.can_remove_pkp_from_groups); 41 if (removePkp) parts.push('remove PKP: ' + removePkp); 42 return parts.length > 0 ? parts.join('; ') : 'none'; 43} 44 45// Record a freshly-created usage key into the in-memory store + UI. Extracted 46// so both the success path and the "broadcast but unconfirmed" recovery path 47// (sovereign mode) surface the once-only cleartext key identically. 48function recordCreatedUsageKey(usageKey, name, description) { 49 if (!usageKey) return; 50 getUsageKeysStore().push({ 51 id: usageKey.slice(0, 12), 52 api_key: usageKey, 53 usage_api_key: usageKey, 54 name: name || '', 55 description: description || '', 56 expiration: '', 57 }); 58 setStat('usageKeys', getUsageKeysStore().length); 59 renderUsageKeysTable(); 60 updateStatCards(); 61} 62 63// ----- Table rendering ----- 64 65export function renderUsageKeysTable() { 66 const items = getUsageKeysStore(); 67 const tbody = document.getElementById('usage-keys-tbody'); 68 const empty = document.getElementById('usage-keys-empty'); 69 if (!tbody) return; 70 tbody.innerHTML = ''; 71 if (!items || items.length === 0) { 72 if (empty) empty.style.display = 'block'; 73 return; 74 } 75 if (empty) empty.style.display = 'none'; 76 items.forEach((item) => { 77 const expiration = (() => { 78 const ts = Number(item.expiration); 79 if (!ts) return 'â'; 80 return new Date(ts * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); 81 })(); 82 const tr = document.createElement('tr'); 83 tr.innerHTML = 84 '<td>' + escapeHtml(item.name || '') + '</td>' + 85 '<td class="mono">' + escapeHtml(item.description || '') + '</td>' + 86 '<td class="mono" style="font-size:0.82em;">' + escapeHtml(renderPermissionSummary(item)) + '</td>' + 87 '<td class="mono">' + escapeHtml(expiration) + '</td>' + 88 '<td class="cell-actions"></td>'; 89 const actionsCell = tr.querySelector('.cell-actions'); 90 const fullKey = item.api_key || (!item.api_key_hash ? item.usage_api_key : null); 91 const copyBtn = document.createElement('button'); 92 copyBtn.type = 'button'; 93 copyBtn.className = 'btn-icon'; 94 copyBtn.title = fullKey ? 'Copy usage API key' : 'Key only available when first created'; 95 copyBtn.innerHTML = ICON_COPY; 96 if (!fullKey) copyBtn.disabled = true; 97 copyBtn.addEventListener('click', () => { 98 if (!fullKey) return; 99 copyToClipboard(fullKey, copyBtn, 'title'); 100 }); 101 actionsCell.appendChild(copyBtn); 102 const canEdit = !!(item.usage_api_key ?? item.api_key); 103 const editBtn = document.createElement('button'); 104 editBtn.type = 'button'; 105 editBtn.className = 'btn-icon';
106 editBtn.title = canEdit ? 'Edit' : 'Edit requires key (add key in this session to edit later)'; 107 editBtn.innerHTML = ICON_PENCIL; 108 if (!canEdit) editBtn.disabled = true; 109 editBtn.addEventListener('click', () => canEdit && openUsageKeyModal(normalizeUsageKeyItem(item))); 110 actionsCell.appendChild(editBtn); 111 const delBtn = document.createElement('button'); 112 delBtn.type = 'button'; 113 delBtn.className = 'btn-icon btn-icon-danger'; 114 delBtn.title = (item.usage_api_key ?? item.api_key) ? 'Delete' : 'Delete requires key (add key in this session to remove later)'; 115 delBtn.innerHTML = ICON_TRASH; 116 const canRemove = !!(item.usage_api_key ?? item.api_key); 117 if (!canRemove) delBtn.disabled = true; 118 delBtn.addEventListener('click', () => canRemove && confirmAndRemoveUsageKey(normalizeUsageKeyItem(item))); 119 actionsCell.appendChild(delBtn); 120 tbody.appendChild(tr); 121 }); 122} 123 124// ----- Load ----- 125 126export async function loadUsageKeys() { 127 const apiKey = getEffectiveApiKey(); 128 if (!isAuthenticated()) return []; 129 hideStatus('overview-status-usage-keys'); 130 const btn = document.getElementById('btn-load-usage-keys'); 131 if (btn) btn.disabled = true; 132 try { 133 const client = await getClient(); 134 const items = await client.listApiKeys({ apiKey, pageNumber: '0', pageSize: LIST_PAGE_SIZE }); 135 const mapped = items.map((it) => ({ 136 id: it.id, 137 api_key_hash: it.api_key_hash, 138 usage_api_key: it.api_key_hash, 139 name: it.name ?? '', 140 description: it.description ?? '', 141 expiration: it.expiration, 142 can_create_groups: it.can_create_groups ?? false, 143 can_delete_groups: it.can_delete_groups ?? false, 144 can_create_pkps: it.can_create_pkps ?? false, 145 can_manage_ipfs_ids_in_groups: it.can_manage_ipfs_ids_in_groups ?? [], 146 can_add_pkp_to_groups: it.can_add_pkp_to_groups ?? [], 147 can_remove_pkp_from_groups: it.can_remove_pkp_from_groups ?? [], 148 can_execute_in_groups: it.can_execute_in_groups ?? [], 149 })); 150 setUsageKeysStore(mapped); 151 setStat('usageKeys', mapped.length); 152 renderUsageKeysTable(); 153 updateStatCards(); 154 return mapped; 155 } catch (e) { 156 logError('loadUsageKeys', e); 157 showStatus('overview-status-usage-keys', 'Error: ' + formatError(e), 'error'); 158 return []; 159 } finally { 160 if (btn) btn.disabled = false; 161 } 162} 163 164// ----- CRUD ----- 165 166function openUsageKeyModal(item = null) { 167 const isEdit = item != null; 168 const body = 169 '<div class="form-group"><label for="modal-usage-name">Name (optional)</label><input type="text" id="modal-usage-name" class="input" placeholder="Optional" value="' + escapeHtml(item?.name ?? '') + '"></div>' + 170 '<div class="form-group"><label for="modal-usage-desc">Description (optional)</label><input type="text" id="modal-usage-desc" class="input" placeholder="Optional" value="' + escapeHtml(item?.description ?? '') + '"></div>' + 171 '<div class="form-group">' + 172 '<label class="checkbox-label"><input type="checkbox" id="modal-usage-can-create-groups"' + (isEdit ? ' disabled' : '') + '> Can create groups</label>' + 173 '<label class="checkbox-label"><input type="checkbox" id="modal-usage-can-delete-groups"' + (isEdit ? ' disabled' : '') + '> Can delete groups</label>' + 174 '<label class="checkbox-label"><input type="checkbox" id="modal-usage-can-create-pkps"' + (isEdit ? ' disabled' : '') + '> Can create PKPs</label>' + 175 '</div>' + 176 '<div class="form-group"><label>Can execute in groups</label>' + buildGroupMultiSelect('modal-usage-execute-groups', false) + '</div>' + 177 '<div class="form-group"><label>Can manage IPFS actions in groups</label>' + buildGroupMultiSelect('modal-usage-manage-ipfs-groups', false) + '</div>' + 178 '<div class="form-group"><label>Can add PKP to groups</label>' + buildGroupMultiSelect('modal-usage-add-pkp-groups', false) + '</div>' + 179 '<div class="form-group"><label>Can remove PKP from groups</label>' + buildGroupMultiSelect('modal-usage-remove-pkp-groups', false) + '</div>'; 180 const footer = 181 (!isEdit ? '<button type="button" class="btn btn-outline" id="modal-all-options-btn" style="margin-right:auto;">All Options</button>' : '') + 182 '<button type="button" class="btn btn-outline" id="modal-cancel-btn">Cancel</button>' + 183 '<button type="button" class="btn btn-primary" id="modal-save-btn">' + (isEdit ? 'Save' : 'Add') + '</button>'; 184 openModal(isEdit ? 'Edit usage API key' : 'Add usage API key', body, footer); 185 if (isEdit && item) { 186 const setCb = (id, val) => { const el = document.getElementById(id); if (el) el.checked = !!val; }; 187 setCb('modal-usage-can-create-groups', item.can_create_groups); 188 setCb('modal-usage-can-delete-groups', item.can_delete_groups);
189 setCb('modal-usage-can-create-pkps', item.can_create_pkps); 190 const preSelect = (msId, ids) => { 191 const wrap = document.getElementById(msId); 192 if (!wrap || !ids) return; 193 const idSet = new Set(ids.map(String)); 194 wrap.querySelectorAll('input[type="checkbox"]').forEach((cb) => { if (idSet.has(cb.value)) cb.checked = true; }); 195 updateMultiSelectSummary(msId); 196 }; 197 preSelect('modal-usage-execute-groups', item.can_execute_in_groups); 198 preSelect('modal-usage-manage-ipfs-groups', item.can_manage_ipfs_ids_in_groups); 199 preSelect('modal-usage-add-pkp-groups', item.can_add_pkp_to_groups); 200 preSelect('modal-usage-remove-pkp-groups', item.can_remove_pkp_from_groups); 201 } 202 attachGroupMultiSelectLogic('modal-usage-execute-groups'); 203 attachGroupMultiSelectLogic('modal-usage-manage-ipfs-groups'); 204 attachGroupMultiSelectLogic('modal-usage-add-pkp-groups'); 205 attachGroupMultiSelectLogic('modal-usage-remove-pkp-groups'); 206 if (!isEdit) { 207 document.getElementById('modal-all-options-btn').addEventListener('click', () => { 208 document.getElementById('modal-usage-can-create-groups').checked = true; 209 document.getElementById('modal-usage-can-delete-groups').checked = true; 210 document.getElementById('modal-usage-can-create-pkps').checked = true; 211 ['modal-usage-execute-groups', 'modal-usage-manage-ipfs-groups', 'modal-usage-add-pkp-groups', 'modal-usage-remove-pkp-groups'].forEach((id) => { 212 const wrap = document.getElementById(id); 213 if (!wrap) return; 214 wrap.querySelectorAll('input[type="checkbox"]').forEach((cb) => { cb.checked = cb.value === '0'; }); 215 updateMultiSelectSummary(id); 216 }); 217 }); 218 } 219 document.getElementById('modal-cancel-btn').addEventListener('click', closeModal); 220 document.getElementById('modal-save-btn').addEventListener('click', async () => { 221 const name = document.getElementById('modal-usage-name').value.trim() || 'Usage Key'; 222 const description = document.getElementById('modal-usage-desc').value.trim() || ''; 223 const apiKey = getEffectiveApiKey(); 224 if (!isAuthenticated()) { 225 showStatus('overview-status-usage-keys', 'Log in first.', 'error'); 226 return; 227 } 228 const saveBtn = document.getElementById('modal-save-btn'); 229 if (saveBtn) saveBtn.disabled = true; 230 // Collect all form values BEFORE closeModal(). closeModal only hides the 231 // overlay today, but if it ever clears innerHTML these reads would return 232 // empty arrays/false and silently create a key with zero permissions. 233 const executeInGroups = getSelectedGroupIds('modal-usage-execute-groups'); 234 const manageIpfsIdsInGroups = getSelectedGroupIds('modal-usage-manage-ipfs-groups'); 235 const addPkpToGroups = getSelectedGroupIds('modal-usage-add-pkp-groups'); 236 const removePkpFromGroups = getSelectedGroupIds('modal-usage-remove-pkp-groups'); 237 const canCreateGroupsEl = document.getElementById('modal-usage-can-create-groups'); 238 const canDeleteGroupsEl = document.getElementById('modal-usage-can-delete-groups'); 239 const canCreatePkpsEl = document.getElementById('modal-usage-can-create-pkps'); 240 const canCreateGroups = !!(canCreateGroupsEl && canCreateGroupsEl.checked); 241 const canDeleteGroups = !!(canDeleteGroupsEl && canDeleteGroupsEl.checked); 242 const canCreatePkps = !!(canCreatePkpsEl && canCreatePkpsEl.checked); 243 closeModal(); 244 hideStatus('overview-status-usage-keys'); 245 if (isEdit) { 246 try { 247 showActionProgress('Updating usage API key', 'Saving changes to usage API key.'); 248 const client = await getClient(); 249 await client.updateUsageApiKey({ 250 apiKey, 251 usageApiKey: item.usage_api_key, 252 name, 253 description, 254 canCreateGroups: item.can_create_groups, 255 canDeleteGroups: item.can_delete_groups, 256 canCreatePkps: item.can_create_pkps, 257 manageIpfsIdsInGroups, 258 addPkpToGroups, 259 removePkpFromGroups, 260 executeInGroups, 261 }); 262 const store = getUsageKeysStore(); 263 const idx = store.findIndex((k) => k.usage_api_key === item.usage_api_key); 264 if (idx !== -1) { 265 store[idx].name = name; 266 store[idx].description = description; 267 store[idx].can_manage_ipfs_ids_in_groups = manageIpfsIdsInGroups; 268 store[idx].can_add_pkp_to_groups = addPkpToGroups; 269 store[idx].can_remove_pkp_from_groups = removePkpFromGroups; 270 store[idx].can_execute_in_groups = executeInGroups; 271 } 272 renderUsageKeysTable(); 273 showStatus('overview-status-usage-keys', 'Usage API key updated.', 'success'); 274 } catch (e) { 275 logError('updateUsageKey', e); 276 showStatus('overview-status-usage-keys', 'Error: ' + formatError(e), 'error'); 277 } finally { 278 closeActionProgress(); 279 } 280 } else {
281 try { 282 showActionProgress('Adding usage API key', 'Creating a new usage API key for this account.'); 283 const client = await getClient(); 284 const res = await client.addUsageApiKey({ 285 apiKey, name, description, 286 canCreateGroups, canDeleteGroups, canCreatePkps, 287 manageIpfsIdsInGroups, 288 addPkpToGroups, 289 removePkpFromGroups, 290 executeInGroups, 291 }); 292 const usageKey = res && res.usage_api_key ? res.usage_api_key : ''; 293 recordCreatedUsageKey(usageKey, name, description); 294 showStatus('overview-status-usage-keys', 'Usage API key added. Copy and store your key now (shown once): ' + usageKey, 'success'); 295 } catch (e) { 296 logError('addUsageKey', e); 297 // Sovereign mode mints the cleartext key locally before broadcasting the 298 // on-chain write. If the broadcast succeeded but its receipt couldn't be 299 // confirmed, the SDK attaches the key to the error (e.usage_api_key) so 300 // it isn't lost forever â the write may still land on-chain. Surface it 301 // with a warning instead of masking it behind the generic error. 302 if (e && e.usage_api_key) { 303 recordCreatedUsageKey(e.usage_api_key, name, description); 304 showStatus( 305 'overview-status-usage-keys', 306 'Usage API key created, but the transaction could not be confirmed â it may still be processing on-chain. Copy and store your key NOW (shown once, cannot be recovered): ' + e.usage_api_key + ' â then use "Load" shortly to verify it registered.', 307 'warning', 308 ); 309 } else { 310 showStatus('overview-status-usage-keys', 'Error: ' + formatError(e), 'error'); 311 } 312 } finally { 313 closeActionProgress(); 314 } 315 } 316 }); 317} 318 319async function confirmAndRemoveUsageKey(item) { 320 const keyOrId = item.usage_api_key || item.api_key || item.id || ''; 321 const masked = keyOrId ? (keyOrId.length > 12 ? maskApiKey(keyOrId) : keyOrId) : 'â'; 322 const msg = 'Remove usage API key "' + escapeHtml(masked) + '" from this account? This cannot be undone.'; 323 const confirmed = await confirmDelete(msg); 324 if (!confirmed) return; 325 const apiKey = getEffectiveApiKey(); 326 if (!isAuthenticated()) return; 327 hideStatus('overview-status-usage-keys'); 328 try { 329 showActionProgress('Removing usage API key', `Removing usage API key "${masked}".`); 330 const client = await getClient(); 331 await client.removeUsageApiKey({ apiKey, usageApiKey: item.usage_api_key }); 332 const store = getUsageKeysStore(); 333 const idx = store.findIndex((k) => k.usage_api_key === item.usage_api_key); 334 if (idx !== -1) store.splice(idx, 1); 335 renderUsageKeysTable(); 336 updateStatCards(); 337 showStatus('overview-status-usage-keys', 'Usage API key removed.', 'success'); 338 } catch (e) { 339 logError('removeUsageKey', e); 340 showStatus('overview-status-usage-keys', 'Error: ' + formatError(e), 'error'); 341 } finally { 342 closeActionProgress(); 343 } 344} 345 346// ----- Init ----- 347 348export function initKeys() { 349 renderUsageKeysTable(); 350 updateStatCards(); 351 document.getElementById('btn-load-usage-keys')?.addEventListener('click', () => loadUsageKeys()); 352 document.getElementById('btn-add-usage-key')?.addEventListener('click', () => openUsageKeyModal()); 353}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.