LLM: keep API key in localStorage only, default to muse-spark-1.3

Secrets must never be baked into the static build. Stop reading
VITE_AI_API_KEY from env — key comes only from localStorage via the
⚙️ config dialog (flow-state:llm). Gateway URL / model may still come
from env as non-secret defaults.

- src/llm/config.js: DEFAULTS model → meta/muse-spark-1.3-contributor
  (cheap, per user request), readEnv no longer touches apiKey,
  getLlmConfig resolves key only from stored.apiKey.
- .env.example: drop VITE_AI_API_KEY=, document browser-only key flow.
- src/main.js: dialog copy now says key is browser-only, never baked.

Build + lint:scenes + tests still green. No VITE_AI_API_KEY string
remains in source.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-09-13 20:08:06 +02:00
co-authored by Claude
parent b7c3537fcb
commit 904ae09486
3 changed files with 17 additions and 16 deletions
+11 -10
View File
@@ -1,14 +1,16 @@
// LLM gateway config — localStorage overrides > VITE env > defaults.
// LLM gateway config — localStorage for the secret, env only for non-secrets.
//
// The app is a static Vite site with no server; the gateway credential lives
// in the browser. Never commit a key — paste it in the panel (→ localStorage)
// or in an untracked .env.local.
// The app is a static Vite site with no server. The API key is a secret and
// must never be baked into the build — it lives only in the browser's
// localStorage (→ ⚙️ dialog). Gateway URL and model are non-secret and may
// come from VITE env as defaults with a localStorage override; the key never
// does.
const STORAGE_KEY = 'flow-state:llm';
const DEFAULTS = {
gatewayUrl: 'https://ai-gateway.vercel.sh/v1',
model: 'openai/gpt-4o-mini',
model: 'meta/muse-spark-1.3-contributor',
apiKey: '',
};
@@ -18,10 +20,10 @@ function readEnv() {
return {
gatewayUrl: env.VITE_AI_GATEWAY_URL || null,
model: env.VITE_AI_MODEL || null,
apiKey: env.VITE_AI_API_KEY || null,
// apiKey intentionally NOT read from env — see module comment.
};
} catch {
return { gatewayUrl: null, model: null, apiKey: null };
return { gatewayUrl: null, model: null };
}
}
@@ -36,14 +38,13 @@ function readStorage() {
}
}
/** Resolved config — localStorage wins, then env, then defaults. */
/** Resolved config — key from localStorage only (never env); URL/model from localStorage > env > defaults. */
export function getLlmConfig() {
const env = readEnv();
const stored = readStorage();
const gatewayUrl = stored.gatewayUrl || env.gatewayUrl || DEFAULTS.gatewayUrl;
const model = stored.model || env.model || DEFAULTS.model;
const apiKey = stored.apiKey != null ? stored.apiKey : (env.apiKey != null ? env.apiKey : DEFAULTS.apiKey);
const key = typeof apiKey === 'string' ? apiKey.trim() : '';
const key = typeof stored.apiKey === 'string' ? stored.apiKey.trim() : '';
return {
gatewayUrl: String(gatewayUrl || DEFAULTS.gatewayUrl).trim() || DEFAULTS.gatewayUrl,
model: String(model || DEFAULTS.model).trim() || DEFAULTS.model,
+2 -2
View File
@@ -902,8 +902,8 @@ function openLlmConfigDialog() {
<div class="hint" style="margin-bottom:10px;">Stored in <code>localStorage</code> (<code>flow-state:llm</code>). Never committed. Leave API key empty to disable.</div>
<label class="llm-field"><span>Gateway URL</span><input id="llm-gw" type="text" placeholder="${cfg.defaults.gatewayUrl}" value="${escapeHtml(cfg.stored.gatewayUrl || '')}"></label>
<label class="llm-field"><span>Model</span><input id="llm-model" type="text" placeholder="${cfg.defaults.model}" value="${escapeHtml(cfg.stored.model || '')}"></label>
<label class="llm-field"><span>API key</span><input id="llm-key" type="password" autocomplete="off" placeholder="(env or empty)" value="${escapeHtml(cfg.stored.apiKey || '')}"></label>
<div class="hint">Env fallback: <code>VITE_AI_GATEWAY_URL</code> / <code>VITE_AI_MODEL</code> / <code>VITE_AI_API_KEY</code> in <code>.env.local</code>. See <code>.env.example</code>.</div>
<label class="llm-field"><span>API key</span><input id="llm-key" type="password" autocomplete="off" placeholder="(empty to disable — stored locally)" value="${escapeHtml(cfg.stored.apiKey || '')}"></label>
<div class="hint">Gateway URL / model may also come from <code>.env.local</code> (<code>VITE_AI_GATEWAY_URL</code> / <code>VITE_AI_MODEL</code>); API key is browser-only via this dialog and never baked into the build. See <code>.env.example</code>.</div>
<div class="hint">Budget: ${counts.loadCount}/${counts.perLoad} this load · ${counts.sessionCount}/${counts.perSession} this session. Counters reset on reload / tab close.</div>
<div class="llm-actions">
<button id="llm-cancel">Cancel</button>