anvilsign in

collin/strudel-claude

1// Bring-your-own Anthropic key. The key is kept in this browser's
2// localStorage and sent to the local backend with each request, which uses it
3// for that request only. If the backend has its own ANTHROPIC_API_KEY, a
4// personal key is optional and simply takes precedence.
5import { getConfig } from './config.js';
6import { closeAll, bindPanel } from './panel.js';
7
8const KEY = 'strudel-claude-api-key';
9
10export const getKey = () => {
11 try {
12 return localStorage.getItem(KEY) || '';
13 } catch {
14 return '';
15 }
16};
17
18const setKey = (value) => {
19 try {
20 if (value) localStorage.setItem(KEY, value);
21 else localStorage.removeItem(KEY);
22 } catch {
23 /* storage blocked — the key just won't survive a reload */
24 }
25};
26
27// Headers for a backend call: the key rides along only when the user set one.
28export function authHeaders() {
29 const key = getKey();
30 return key ? { 'x-anthropic-key': key } : {};
31}
32
33// Show enough of a key to recognise it, never the whole thing.
34const mask = (key) => (key.length > 12 ? `${key.slice(0, 7)}…${key.slice(-4)}` : 'key set');
35
36export function initApiKeyUI({ onChange } = {}) {
37 const button = document.getElementById('key-btn');
38 const label = document.getElementById('key-state');
39 const panel = document.getElementById('key-panel');
40 const field = document.getElementById('key-input');
41 const note = document.getElementById('key-note');
42 const saveBtn = document.getElementById('key-save');
43 const clearBtn = document.getElementById('key-clear');
44
45 // Until /api/config answers we don't know whether the server has a key, and
46 // we'd rather let a request through than block it on a guess.
47 let hasServerKey = true;
48 let configKnown = false;
49
50 const render = () => {
51 const key = getKey();
52 label.textContent = key ? mask(key) : 'Add API key';
53 button.classList.toggle('key-missing', !key && configKnown && !hasServerKey);
54 clearBtn.hidden = !key;
55 note.textContent =
56 configKnown && hasServerKey
57 ? 'Optional — the server already has a key. Yours takes precedence.'
58 : 'Stored in this browser only, sent to the local server with each request.';
59 };
60
61 bindPanel(button, panel, () => {
62 field.value = getKey();
63 field.focus();
64 field.select();
65 });
66
67 const commit = (value) => {
68 setKey(value.trim());
69 render();
70 closeAll();
71 onChange?.(getKey());
72 };
73
74 saveBtn.addEventListener('click', () => commit(field.value));
75 clearBtn.addEventListener('click', () => commit(''));
76 field.addEventListener('keydown', (e) => {
77 e.stopPropagation();
78 if (e.key === 'Enter') commit(field.value);
79 else if (e.key === 'Escape') closeAll();
80 });
81
82 render();
83
84 // Ask the backend whether a key of our own is actually needed. A null config
85 // means the backend is down or predates the endpoint — the chat itself will
86 // report that, so leave the optimistic default in place.
87 getConfig().then((cfg) => {
88 if (!cfg) return;
89 hasServerKey = Boolean(cfg.hasServerKey);
90 configKnown = true;
91 render();
92 });
93
94 return { needsKey: () => !getKey() && configKnown && !hasServerKey };
95}