Add persistent API key storage with remember option

Previously the Anthropic API key was session-only and lost on Chrome
restart. Now:

- KEYS.API_KEY_SAVED added to storage schema (chrome.storage.local)
- Background service worker restores saved key into session on startup,
  so the key is available immediately without re-entering it
- SET_API_KEY handler accepts a `remember` flag: if true, writes to
  both session and local; if false, clears the local copy
- Settings panel: "Remember across sessions" checkbox next to Save,
  "Forget saved key" button appears when a key is persisted
- Status text distinguishes "Key saved across sessions" vs
  "Key active this session only"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBD2dN2KEjzz3UQFa9hEpu
This commit is contained in:
Claude 2026-06-27 06:58:43 +00:00
parent cac81d3a86
commit 9b17585756
No known key found for this signature in database
8 changed files with 209 additions and 90 deletions

View file

@ -632,7 +632,9 @@ var KEYS = {
FIELD_PROFILES: 'agrifine_field_profiles',
SETTINGS: 'agrifine_settings',
FARM_MEMORY: 'agrifine_farm_memory',
API_KEY: 'agrifine_api_key' // session only
API_KEY: 'agrifine_api_key',
// session storage (always)
API_KEY_SAVED: 'agrifine_api_key_saved' // local storage (when user opts to remember)
};
// ── Generic helpers ──────────────────────────────────────────────────────────
@ -1308,9 +1310,14 @@ chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
})["catch"](console.error);
// Restore saved API key into session on service worker startup
(0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.localGet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.KEYS.API_KEY_SAVED).then(function (saved) {
if (saved) (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.sessionSet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.KEYS.API_KEY, saved)["catch"](function () {});
})["catch"](function () {});
// ── Message router ────────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
var _message$payload;
var _message$payload2;
switch (message.type) {
case 'ANTHROPIC_REQUEST':
handleAnthropicRequest(message.payload).then(sendResponse)["catch"](function (err) {
@ -1322,16 +1329,27 @@ chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
// keep channel open for async response
case 'SET_API_KEY':
(0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.sessionSet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.KEYS.API_KEY, message.payload.key).then(function () {
return sendResponse({
ok: true
{
var _message$payload = message.payload,
key = _message$payload.key,
remember = _message$payload.remember;
var ops = [(0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.sessionSet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.KEYS.API_KEY, key)];
if (remember) {
ops.push((0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.localSet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.KEYS.API_KEY_SAVED, key));
} else {
ops.push((0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.localSet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_0__.KEYS.API_KEY_SAVED, null));
}
Promise.all(ops).then(function () {
return sendResponse({
ok: true
});
})["catch"](function (err) {
return sendResponse({
error: err.message
});
});
})["catch"](function (err) {
return sendResponse({
error: err.message
});
});
return true;
return true;
}
case 'GET_PAGE_CONTENT':
sendResponse({
ok: true
@ -1359,7 +1377,7 @@ chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
});
return true;
case 'READ_TAB_CONTENT':
readTabContent((_message$payload = message.payload) === null || _message$payload === void 0 ? void 0 : _message$payload.tab_id).then(sendResponse)["catch"](function (err) {
readTabContent((_message$payload2 = message.payload) === null || _message$payload2 === void 0 ? void 0 : _message$payload2.tab_id).then(sendResponse)["catch"](function (err) {
return sendResponse({
error: err.message
});

View file

@ -606,6 +606,9 @@ video {
.h-10 {
height: 2.5rem;
}
.h-3 {
height: 0.75rem;
}
.h-4 {
height: 1rem;
}
@ -639,6 +642,9 @@ video {
.w-10 {
width: 2.5rem;
}
.w-3 {
width: 0.75rem;
}
.w-4 {
width: 1rem;
}
@ -673,6 +679,11 @@ video {
.cursor-pointer {
cursor: pointer;
}
.select-none {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.resize-none {
resize: none;
}
@ -1015,6 +1026,9 @@ video {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.accent-agri-500 {
accent-color: #22c55e;
}
.opacity-0 {
opacity: 0;
}

View file

@ -35,7 +35,14 @@
<button id="btn-save-key"
class="text-xs bg-agri-600 hover:bg-agri-700 text-white px-3 py-1.5 rounded-lg transition font-medium">Save</button>
</div>
<p id="api-key-status" class="text-xs mt-1.5" style="color:#3d4f66;"></p>
<div class="flex items-center justify-between mt-2">
<label class="flex items-center gap-1.5 cursor-pointer select-none">
<input id="api-key-remember" type="checkbox" class="accent-agri-500 w-3 h-3" />
<span class="text-xs" style="color:#3d4f66;">Remember across sessions</span>
</label>
<button id="btn-forget-key" class="text-xs hover:text-red-400 transition hidden" style="color:#3d4f66;">Forget saved key</button>
</div>
<p id="api-key-status" class="text-xs mt-1" style="color:#3d4f66;"></p>
<label class="block text-[10px] uppercase tracking-widest font-semibold mb-2 mt-3" style="color:#3d4f66;">AG-Refine App URL</label>
<div class="flex gap-2">

View file

@ -3290,7 +3290,9 @@ var KEYS = {
FIELD_PROFILES: 'agrifine_field_profiles',
SETTINGS: 'agrifine_settings',
FARM_MEMORY: 'agrifine_farm_memory',
API_KEY: 'agrifine_api_key' // session only
API_KEY: 'agrifine_api_key',
// session storage (always)
API_KEY_SAVED: 'agrifine_api_key_saved' // local storage (when user opts to remember)
};
// ── Generic helpers ──────────────────────────────────────────────────────────
@ -4202,16 +4204,16 @@ function activateTab(_x) {
return _activateTab.apply(this, arguments);
} // ── Settings panel ────────────────────────────────────────────────────────────
function _activateTab() {
_activateTab = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(id) {
_activateTab = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(id) {
var main;
return _regenerator().w(function (_context5) {
while (1) switch (_context5.n) {
return _regenerator().w(function (_context6) {
while (1) switch (_context6.n) {
case 0:
if (moduleMap[id]) {
_context5.n = 1;
_context6.n = 1;
break;
}
return _context5.a(2);
return _context6.a(2);
case 1:
activeModuleId = id;
document.querySelectorAll('.tab-btn').forEach(function (btn) {
@ -4221,12 +4223,12 @@ function _activateTab() {
});
main = document.getElementById('main-content');
main.innerHTML = '';
_context5.n = 2;
_context6.n = 2;
return moduleMap[id].render(main);
case 2:
return _context5.a(2);
return _context6.a(2);
}
}, _callee5);
}, _callee6);
}));
return _activateTab.apply(this, arguments);
}
@ -4236,86 +4238,118 @@ function setupSettings() {
var saveBtn = document.getElementById('btn-save-key');
var input = document.getElementById('api-key-input');
var status = document.getElementById('api-key-status');
var rememberChk = document.getElementById('api-key-remember');
var forgetBtn = document.getElementById('btn-forget-key');
var agRefineInput = document.getElementById('agrefine-url-input');
var agRefineStatus = document.getElementById('agrefine-url-status');
var agRefineSaveBtn = document.getElementById('btn-save-agrefine-url');
btn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
var existing, agUrl;
var sessionKey, savedKey, agUrl;
return _regenerator().w(function (_context) {
while (1) switch (_context.n) {
case 0:
panel.classList.toggle('hidden');
if (panel.classList.contains('hidden')) {
_context.n = 3;
_context.n = 4;
break;
}
_context.n = 1;
return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_7__.sessionGet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_7__.KEYS.API_KEY);
case 1:
existing = _context.v;
if (existing) {
sessionKey = _context.v;
_context.n = 2;
return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_7__.localGet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_7__.KEYS.API_KEY_SAVED);
case 2:
savedKey = _context.v;
if (sessionKey || savedKey) {
input.value = '';
input.placeholder = 'Key set — enter new key to replace';
status.textContent = '✓ API key is active this session';
status.textContent = savedKey ? '✓ Key saved across sessions' : '✓ Key active this session only';
status.style.color = '#4ade80';
}
_context.n = 2;
if (savedKey) {
rememberChk.checked = true;
forgetBtn.classList.remove('hidden');
}
_context.n = 3;
return (0,_utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_8__.getAgRefineUrl)();
case 2:
case 3:
agUrl = _context.v;
if (agUrl) agRefineInput.value = agUrl;
case 3:
case 4:
return _context.a(2);
}
}, _callee);
})));
agRefineSaveBtn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
var url;
saveBtn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
var key, remember;
return _regenerator().w(function (_context2) {
while (1) switch (_context2.n) {
case 0:
key = input.value.trim();
if (key.startsWith('sk-ant-')) {
_context2.n = 1;
break;
}
status.textContent = 'Key must start with sk-ant-';
status.style.color = '#f87171';
return _context2.a(2);
case 1:
remember = rememberChk.checked;
_context2.n = 2;
return chrome.runtime.sendMessage({
type: 'SET_API_KEY',
payload: {
key: key,
remember: remember
}
});
case 2:
input.value = '';
input.placeholder = 'Key set — enter new key to replace';
status.textContent = remember ? '✓ Key saved across sessions' : '✓ Saved for this session';
status.style.color = '#4ade80';
forgetBtn.classList.toggle('hidden', !remember);
case 3:
return _context2.a(2);
}
}, _callee2);
})));
forgetBtn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
return _regenerator().w(function (_context3) {
while (1) switch (_context3.n) {
case 0:
_context3.n = 1;
return (0,_utils_storage_js__WEBPACK_IMPORTED_MODULE_7__.localSet)(_utils_storage_js__WEBPACK_IMPORTED_MODULE_7__.KEYS.API_KEY_SAVED, null);
case 1:
rememberChk.checked = false;
forgetBtn.classList.add('hidden');
status.textContent = 'Saved key removed';
status.style.color = '#3d4f66';
case 2:
return _context3.a(2);
}
}, _callee3);
})));
agRefineSaveBtn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() {
var url;
return _regenerator().w(function (_context4) {
while (1) switch (_context4.n) {
case 0:
url = agRefineInput.value.trim();
_context2.n = 1;
_context4.n = 1;
return (0,_utils_agrefine_bridge_js__WEBPACK_IMPORTED_MODULE_8__.setAgRefineUrl)(url);
case 1:
agRefineStatus.textContent = url ? "\u2713 AG-Refine URL saved" : '✓ Cleared';
agRefineStatus.textContent = url ? '✓ AG-Refine URL saved' : '✓ Cleared';
agRefineStatus.style.color = '#4ade80';
setTimeout(function () {
agRefineStatus.style.color = '#3d4f66';
agRefineStatus.textContent = 'Used to sync fields and outputs from your AG-Refine app.';
}, 2500);
case 2:
return _context2.a(2);
return _context4.a(2);
}
}, _callee2);
})));
saveBtn.addEventListener('click', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
var key;
return _regenerator().w(function (_context3) {
while (1) switch (_context3.n) {
case 0:
key = input.value.trim();
if (key.startsWith('sk-ant-')) {
_context3.n = 1;
break;
}
status.textContent = 'Key must start with sk-ant-';
return _context3.a(2);
case 1:
_context3.n = 2;
return chrome.runtime.sendMessage({
type: 'SET_API_KEY',
payload: {
key: key
}
});
case 2:
input.value = '';
input.placeholder = 'Key set — enter new key to replace';
status.textContent = '✓ Saved for this session';
case 3:
return _context3.a(2);
}
}, _callee3);
}, _callee4);
})));
}
@ -4332,19 +4366,19 @@ function keepAlive() {
}
// ── Init ──────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() {
return _regenerator().w(function (_context4) {
while (1) switch (_context4.n) {
document.addEventListener('DOMContentLoaded', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5() {
return _regenerator().w(function (_context5) {
while (1) switch (_context5.n) {
case 0:
setupTabs();
setupSettings();
keepAlive();
_context4.n = 1;
_context5.n = 1;
return activateTab(activeModuleId);
case 1:
return _context4.a(2);
return _context5.a(2);
}
}, _callee4);
}, _callee5);
})));
})();

View file

@ -1,10 +1,15 @@
import { sessionGet, sessionSet, KEYS } from '../utils/storage.js';
import { sessionGet, sessionSet, localGet, localSet, KEYS } from '../utils/storage.js';
import { fetchAnthropic } from '../utils/api.js';
import { syncFromAgRefine } from '../utils/agrefine-bridge.js';
// Open the side panel when the action icon is clicked
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error);
// Restore saved API key into session on service worker startup
localGet(KEYS.API_KEY_SAVED).then((saved) => {
if (saved) sessionSet(KEYS.API_KEY, saved).catch(() => {});
}).catch(() => {});
// ── Message router ────────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
switch (message.type) {
@ -14,11 +19,19 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
);
return true; // keep channel open for async response
case 'SET_API_KEY':
sessionSet(KEYS.API_KEY, message.payload.key)
case 'SET_API_KEY': {
const { key, remember } = message.payload;
const ops = [sessionSet(KEYS.API_KEY, key)];
if (remember) {
ops.push(localSet(KEYS.API_KEY_SAVED, key));
} else {
ops.push(localSet(KEYS.API_KEY_SAVED, null));
}
Promise.all(ops)
.then(() => sendResponse({ ok: true }))
.catch((err) => sendResponse({ error: err.message }));
return true;
}
case 'GET_PAGE_CONTENT':
sendResponse({ ok: true });

View file

@ -5,7 +5,7 @@ import { FieldProfileModule } from '../modules/field-profile/index.js';
import { DashboardModule } from '../modules/dashboard/index.js';
import { CarbonEstimatorModule } from '../modules/carbon-estimator/index.js';
import { AgRefineModule } from '../ag-refine/index.js';
import { sessionSet, sessionGet, KEYS } from '../utils/storage.js';
import { sessionSet, sessionGet, localGet, localSet, KEYS } from '../utils/storage.js';
import { getAgRefineUrl, setAgRefineUrl } from '../utils/agrefine-bridge.js';
// ── Module registry ───────────────────────────────────────────────────────────
@ -50,6 +50,8 @@ function setupSettings() {
const saveBtn = document.getElementById('btn-save-key');
const input = document.getElementById('api-key-input');
const status = document.getElementById('api-key-status');
const rememberChk = document.getElementById('api-key-remember');
const forgetBtn = document.getElementById('btn-forget-key');
const agRefineInput = document.getElementById('agrefine-url-input');
const agRefineStatus = document.getElementById('agrefine-url-status');
@ -58,35 +60,58 @@ function setupSettings() {
btn.addEventListener('click', async () => {
panel.classList.toggle('hidden');
if (!panel.classList.contains('hidden')) {
const existing = await sessionGet(KEYS.API_KEY);
if (existing) {
const sessionKey = await sessionGet(KEYS.API_KEY);
const savedKey = await localGet(KEYS.API_KEY_SAVED);
if (sessionKey || savedKey) {
input.value = '';
input.placeholder = 'Key set — enter new key to replace';
status.textContent = '✓ API key is active this session';
status.textContent = savedKey
? '✓ Key saved across sessions'
: '✓ Key active this session only';
status.style.color = '#4ade80';
}
if (savedKey) {
rememberChk.checked = true;
forgetBtn.classList.remove('hidden');
}
const agUrl = await getAgRefineUrl();
if (agUrl) agRefineInput.value = agUrl;
}
});
agRefineSaveBtn.addEventListener('click', async () => {
const url = agRefineInput.value.trim();
await setAgRefineUrl(url);
agRefineStatus.textContent = url ? `✓ AG-Refine URL saved` : '✓ Cleared';
agRefineStatus.style.color = '#4ade80';
setTimeout(() => { agRefineStatus.style.color = '#3d4f66'; agRefineStatus.textContent = 'Used to sync fields and outputs from your AG-Refine app.'; }, 2500);
});
saveBtn.addEventListener('click', async () => {
const key = input.value.trim();
if (!key.startsWith('sk-ant-')) {
status.textContent = 'Key must start with sk-ant-';
status.style.color = '#f87171';
return;
}
await chrome.runtime.sendMessage({ type: 'SET_API_KEY', payload: { key } });
const remember = rememberChk.checked;
await chrome.runtime.sendMessage({ type: 'SET_API_KEY', payload: { key, remember } });
input.value = '';
input.placeholder = 'Key set — enter new key to replace';
status.textContent = '✓ Saved for this session';
status.textContent = remember ? '✓ Key saved across sessions' : '✓ Saved for this session';
status.style.color = '#4ade80';
forgetBtn.classList.toggle('hidden', !remember);
});
forgetBtn.addEventListener('click', async () => {
await localSet(KEYS.API_KEY_SAVED, null);
rememberChk.checked = false;
forgetBtn.classList.add('hidden');
status.textContent = 'Saved key removed';
status.style.color = '#3d4f66';
});
agRefineSaveBtn.addEventListener('click', async () => {
const url = agRefineInput.value.trim();
await setAgRefineUrl(url);
agRefineStatus.textContent = url ? '✓ AG-Refine URL saved' : '✓ Cleared';
agRefineStatus.style.color = '#4ade80';
setTimeout(() => {
agRefineStatus.style.color = '#3d4f66';
agRefineStatus.textContent = 'Used to sync fields and outputs from your AG-Refine app.';
}, 2500);
});
}

View file

@ -35,7 +35,14 @@
<button id="btn-save-key"
class="text-xs bg-agri-600 hover:bg-agri-700 text-white px-3 py-1.5 rounded-lg transition font-medium">Save</button>
</div>
<p id="api-key-status" class="text-xs mt-1.5" style="color:#3d4f66;"></p>
<div class="flex items-center justify-between mt-2">
<label class="flex items-center gap-1.5 cursor-pointer select-none">
<input id="api-key-remember" type="checkbox" class="accent-agri-500 w-3 h-3" />
<span class="text-xs" style="color:#3d4f66;">Remember across sessions</span>
</label>
<button id="btn-forget-key" class="text-xs hover:text-red-400 transition hidden" style="color:#3d4f66;">Forget saved key</button>
</div>
<p id="api-key-status" class="text-xs mt-1" style="color:#3d4f66;"></p>
<label class="block text-[10px] uppercase tracking-widest font-semibold mb-2 mt-3" style="color:#3d4f66;">AG-Refine App URL</label>
<div class="flex gap-2">

View file

@ -32,7 +32,8 @@ export const KEYS = {
FIELD_PROFILES: 'agrifine_field_profiles',
SETTINGS: 'agrifine_settings',
FARM_MEMORY: 'agrifine_farm_memory',
API_KEY: 'agrifine_api_key', // session only
API_KEY: 'agrifine_api_key', // session storage (always)
API_KEY_SAVED: 'agrifine_api_key_saved', // local storage (when user opts to remember)
};
// ── Generic helpers ──────────────────────────────────────────────────────────