fix: move API keys to .env.local, remove localStorage dual system
Single source of truth: OPENAI_API_KEY in .env.local. Deleted the apiKeyService class/singleton that stored keys in localStorage. All server code reads process.env directly. FirstRunModal and Settings now show .env.local instructions instead of key input. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
49ff3678a3
commit
3b04a1bad0
@@ -1,5 +1,6 @@
|
||||
import { openai as openaiProvider } from '@ai-sdk/openai';
|
||||
import { generateText } from 'ai';
|
||||
import { hasValidOpenAiKey } from '../storage/apiKeys';
|
||||
|
||||
export interface DescriptionInput {
|
||||
title: string;
|
||||
@@ -15,16 +16,8 @@ export interface DescriptionInput {
|
||||
dimensions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a valid OpenAI API key configured.
|
||||
* Checks both environment variable and validates format.
|
||||
*/
|
||||
export function hasValidOpenAiKey(): boolean {
|
||||
const key = process.env.OPENAI_API_KEY;
|
||||
if (!key || key === 'your-openai-api-key-here') return false;
|
||||
// Valid OpenAI keys start with sk- or sk-proj-
|
||||
return key.startsWith('sk-') && key.length > 20;
|
||||
}
|
||||
// Re-export for backwards compatibility — canonical source is ../storage/apiKeys
|
||||
export { hasValidOpenAiKey } from '../storage/apiKeys';
|
||||
|
||||
/**
|
||||
* Generate a simple fallback description without AI.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getSQLiteClient } from './sqlite-client';
|
||||
import { openai as openaiProvider } from '@ai-sdk/openai';
|
||||
import { generateText } from 'ai';
|
||||
import { hasValidOpenAiKey } from './descriptionService';
|
||||
import { hasValidOpenAiKey } from '../storage/apiKeys';
|
||||
|
||||
export interface Dimension {
|
||||
name: string;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { getSQLiteClient } from './sqlite-client';
|
||||
import { Edge, EdgeContext, EdgeData, EdgeCreatedVia, NodeConnection, Node } from '@/types/database';
|
||||
import { eventBroadcaster } from '../events';
|
||||
import { nodeService } from './nodes';
|
||||
import { apiKeyService } from '../storage/apiKeys';
|
||||
import { generateText } from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { z } from 'zod';
|
||||
@@ -54,7 +53,7 @@ async function inferEdgeContext(params: {
|
||||
|
||||
// If no API key is configured, degrade gracefully.
|
||||
// We still enforce explanation, but fall back to "related_to" classification.
|
||||
const apiKey = apiKeyService.getOpenAiKey();
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
return { type: 'related_to', confidence: 0.0, swap_direction: false };
|
||||
}
|
||||
@@ -119,7 +118,7 @@ async function autoInferEdge(params: {
|
||||
}): Promise<{ explanation: string; type: EdgeContext['type']; confidence: number; swap_direction: boolean }> {
|
||||
const { fromNode, toNode } = params;
|
||||
|
||||
const apiKey = apiKeyService.getOpenAiKey();
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
// Fallback without AI
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import OpenAI from 'openai';
|
||||
import { apiKeyService } from './storage/apiKeys';
|
||||
|
||||
// Initialize OpenAI client with dynamic API key support
|
||||
function getOpenAiClient(): OpenAI {
|
||||
const apiKey = apiKeyService.getOpenAiKey();
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('OpenAI API key required. Please:\n1. Click the Settings icon (⚙️) in the bottom left\n2. Go to API Keys tab\n3. Add your OpenAI API key\n\nGet your key at: https://platform.openai.com/api-keys');
|
||||
throw new Error('OpenAI API key not configured. Add OPENAI_API_KEY to your .env.local file.');
|
||||
}
|
||||
return new OpenAI({ apiKey });
|
||||
}
|
||||
@@ -41,4 +39,4 @@ export class EmbeddingService {
|
||||
static validateEmbedding(embedding: number[]): boolean {
|
||||
return Array.isArray(embedding) && embedding.length === 1536;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-143
@@ -1,148 +1,34 @@
|
||||
// API Key Storage Service
|
||||
// Handles storage and retrieval of OpenAI API key
|
||||
// API Key helpers
|
||||
// Single source of truth: .env.local → process.env.OPENAI_API_KEY
|
||||
|
||||
export interface ApiKeyStatus {
|
||||
openai: 'connected' | 'failed' | 'testing' | 'not-set';
|
||||
/**
|
||||
* Check if a valid OpenAI API key is configured in .env.local
|
||||
*/
|
||||
export function hasValidOpenAiKey(): boolean {
|
||||
const key = process.env.OPENAI_API_KEY;
|
||||
if (!key || key === 'your-openai-api-key-here') return false;
|
||||
return key.startsWith('sk-') && key.length > 20;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'ra-h-api-keys';
|
||||
const FIRST_RUN_KEY = 'ra-h-first-run-complete';
|
||||
|
||||
export class ApiKeyService {
|
||||
private static instance: ApiKeyService;
|
||||
private openaiKey: string | undefined;
|
||||
private status: ApiKeyStatus = {
|
||||
openai: 'not-set',
|
||||
};
|
||||
|
||||
static getInstance(): ApiKeyService {
|
||||
if (!ApiKeyService.instance) {
|
||||
ApiKeyService.instance = new ApiKeyService();
|
||||
}
|
||||
return ApiKeyService.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.loadKeys();
|
||||
}
|
||||
|
||||
// Load keys from localStorage
|
||||
private loadKeys(): void {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
this.openaiKey = parsed.openai;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load API keys from storage:', error);
|
||||
this.openaiKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Save keys to localStorage
|
||||
private saveKeys(): void {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ openai: this.openaiKey }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save API keys to storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get OpenAI API key (user key or fallback to env)
|
||||
getOpenAiKey(): string | undefined {
|
||||
// Priority: User key > Environment key
|
||||
return this.openaiKey || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
|
||||
// Set OpenAI API key
|
||||
setOpenAiKey(key: string): void {
|
||||
if (this.validateOpenAiKey(key)) {
|
||||
this.openaiKey = key;
|
||||
this.saveKeys();
|
||||
} else {
|
||||
throw new Error('Invalid OpenAI API key format');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear OpenAI key
|
||||
clearOpenAiKey(): void {
|
||||
this.openaiKey = undefined;
|
||||
this.saveKeys();
|
||||
this.status.openai = 'not-set';
|
||||
}
|
||||
|
||||
// Get masked key for display (show only last 4 characters)
|
||||
getMaskedOpenAiKey(): string {
|
||||
if (!this.openaiKey) return '';
|
||||
return '••••••••••••••••••••' + this.openaiKey.slice(-4);
|
||||
}
|
||||
|
||||
// Check if user has provided a key
|
||||
hasOpenAiKey(): boolean {
|
||||
return !!this.openaiKey;
|
||||
}
|
||||
|
||||
// Validate OpenAI key format
|
||||
private validateOpenAiKey(key: string): boolean {
|
||||
return (
|
||||
typeof key === 'string' &&
|
||||
key.length > 20 &&
|
||||
(key.startsWith('sk-') || key.startsWith('sk-proj-'))
|
||||
);
|
||||
}
|
||||
|
||||
// Test connection to OpenAI
|
||||
async testOpenAiConnection(key?: string): Promise<boolean> {
|
||||
const testKey = key || this.getOpenAiKey();
|
||||
if (!testKey) return false;
|
||||
|
||||
this.status.openai = 'testing';
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.openai.com/v1/models', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${testKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const isConnected = response.ok;
|
||||
this.status.openai = isConnected ? 'connected' : 'failed';
|
||||
return isConnected;
|
||||
} catch (error) {
|
||||
console.error('OpenAI connection test failed:', error);
|
||||
this.status.openai = 'failed';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get connection status
|
||||
getStatus(): ApiKeyStatus {
|
||||
return { ...this.status };
|
||||
}
|
||||
|
||||
// Update status
|
||||
updateStatus(status: ApiKeyStatus['openai']): void {
|
||||
this.status.openai = status;
|
||||
}
|
||||
|
||||
// First-run tracking
|
||||
isFirstRun(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return !localStorage.getItem(FIRST_RUN_KEY);
|
||||
}
|
||||
|
||||
markFirstRunComplete(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the OpenAI API key from environment
|
||||
*/
|
||||
export function getOpenAiKey(): string | undefined {
|
||||
const key = process.env.OPENAI_API_KEY;
|
||||
if (!key || key === 'your-openai-api-key-here') return undefined;
|
||||
return key;
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const apiKeyService = ApiKeyService.getInstance();
|
||||
/**
|
||||
* Check if first run (no API key configured)
|
||||
*/
|
||||
export function isFirstRun(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return !localStorage.getItem('ra-h-first-run-complete');
|
||||
}
|
||||
|
||||
export function markFirstRunComplete(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('ra-h-first-run-complete', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user