PageSourceSearch

https://dashboard.chipotle.litprotocol.com/dapps/dashboard/keys.js

js litprotocol.com collected 2026-10-02 04:11:05 UTC 17,548 bytes, 353 lines download raw bytes

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.