Files
OpenJarvis/frontend/src/lib/api.auth.test.ts
T
8d33cb58fa Fix secure cloud key storage and Supabase key config (#587)
Route desktop cloud-key saves/status through the OS credential store (keyring with per-platform native backends: apple-native / windows-native / sync-secret-service), migrate the legacy plaintext ~/.openjarvis/cloud-keys.env into it, remove browser localStorage persistence of provider keys, and push key updates to the running server via /v1/cloud/reload (legacy env-file fallback retained). Remove hardcoded Supabase anon JWTs from frontend/docs source and make VITE_SUPABASE_ANON_KEY a required build var. Adds libdbus-1-dev to the Linux desktop build and a CI build var. Closes #220.

NOTE (post-merge follow-ups, not covered by CI): add the VITE_SUPABASE_ANON_KEY repo secret with the rotated key (release/docs builds otherwise use a placeholder), rotate the previously-committed Supabase anon key, and run a desktop save->restart->read smoke test to confirm keychain persistence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:11:32 -07:00

89 lines
2.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Regression for #266: the frontend must send the local API key as a Bearer
// token on /v1 + /api requests, or `jarvis serve` with a key configured 401s
// every data-plane call. These tests cover the pure helpers (getApiKey,
// authHeaders) that source the key and build the header.
const SETTINGS_KEY = 'openjarvis-settings';
// Minimal in-memory localStorage stub so the helpers can run under node
// (no jsdom dependency).
class MemoryStorage {
private store = new Map<string, string>();
getItem(k: string): string | null {
return this.store.has(k) ? (this.store.get(k) as string) : null;
}
setItem(k: string, v: string): void {
this.store.set(k, String(v));
}
removeItem(k: string): void {
this.store.delete(k);
}
clear(): void {
this.store.clear();
}
}
beforeEach(() => {
vi.resetModules();
vi.stubEnv('VITE_SUPABASE_ANON_KEY', 'test-anon-key');
(globalThis as unknown as { localStorage: MemoryStorage }).localStorage =
new MemoryStorage();
});
afterEach(() => {
vi.unstubAllEnvs();
(globalThis as unknown as { localStorage?: MemoryStorage }).localStorage =
undefined;
});
async function freshApi() {
// Re-import to pick up the current localStorage stub.
return await import('./api');
}
describe('getApiKey', () => {
it('returns empty string when no key is configured', async () => {
const { getApiKey } = await freshApi();
expect(getApiKey()).toBe('');
});
it('reads apiKey from the openjarvis-settings localStorage blob', async () => {
localStorage.setItem(
SETTINGS_KEY,
JSON.stringify({ apiUrl: 'http://x', apiKey: 'sk-local-123' }),
);
const { getApiKey } = await freshApi();
expect(getApiKey()).toBe('sk-local-123');
});
it('returns empty string when the blob has no apiKey field', async () => {
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ apiUrl: 'http://x' }));
const { getApiKey } = await freshApi();
expect(getApiKey()).toBe('');
});
});
describe('authHeaders', () => {
it('omits Authorization when no key is set (keyless default unchanged)', async () => {
const { authHeaders } = await freshApi();
expect(authHeaders()).toEqual({});
});
it('adds a Bearer Authorization header when a key is set', async () => {
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ apiKey: 'sk-local-123' }));
const { authHeaders } = await freshApi();
expect(authHeaders()).toEqual({ Authorization: 'Bearer sk-local-123' });
});
it('merges extra headers alongside Authorization', async () => {
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ apiKey: 'sk-local-123' }));
const { authHeaders } = await freshApi();
expect(authHeaders({ 'Content-Type': 'application/json' })).toEqual({
'Content-Type': 'application/json',
Authorization: 'Bearer sk-local-123',
});
});
});