auth: add OpenAI API key storage

This commit is contained in:
William Valentin
2026-02-15 10:26:19 -08:00
parent 9755487793
commit bcf6c377d5
3 changed files with 184 additions and 6 deletions
+113 -4
View File
@@ -25,10 +25,71 @@ export interface OpenAIOAuthInfo {
created_at: string;
}
export interface OpenAIApiKeyInfo {
api_key: string;
created_at: string;
}
interface OpenAIStoreEntry {
oauth?: OpenAIOAuthInfo;
api_key?: OpenAIApiKeyInfo;
}
interface AuthStore {
// Leave github entry untyped here so this module does not depend on github.ts.
github?: unknown;
openai?: OpenAIOAuthInfo;
/** OpenAI credentials. Backward compatible with legacy OAuth-only entries. */
openai?: OpenAIStoreEntry | OpenAIOAuthInfo;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isOpenAIOAuthInfo(value: unknown): value is OpenAIOAuthInfo {
if (!isRecord(value)) {
return false;
}
return typeof value.access_token === 'string'
&& typeof value.refresh_token === 'string'
&& typeof value.expires_at === 'number'
&& typeof value.created_at === 'string';
}
function isOpenAIApiKeyInfo(value: unknown): value is OpenAIApiKeyInfo {
if (!isRecord(value)) {
return false;
}
return typeof value.api_key === 'string'
&& typeof value.created_at === 'string';
}
function readOpenAIEntry(store: AuthStore): OpenAIStoreEntry | null {
const raw = store.openai as unknown;
if (!raw) {
return null;
}
// Legacy format: auth.json.openai stored the OAuth info directly.
if (isOpenAIOAuthInfo(raw)) {
return { oauth: raw };
}
if (!isRecord(raw)) {
return null;
}
const oauth = isOpenAIOAuthInfo(raw.oauth) ? raw.oauth : undefined;
const apiKey = isOpenAIApiKeyInfo(raw.api_key) ? raw.api_key : undefined;
return { oauth, api_key: apiKey };
}
function writeOpenAIEntry(store: AuthStore, entry: OpenAIStoreEntry | null): void {
if (!entry || (!entry.oauth && !entry.api_key)) {
delete store.openai;
return;
}
store.openai = entry;
}
interface DeviceAuthResponse {
@@ -83,21 +144,69 @@ function writeAuthStore(store: AuthStore): void {
export function loadStoredOpenAIAuth(): OpenAIOAuthInfo | null {
const store = readAuthStore();
return store.openai ?? null;
const entry = readOpenAIEntry(store);
return entry?.oauth ?? null;
}
export function storeOpenAIAuth(info: OpenAIOAuthInfo): void {
const store = readAuthStore();
store.openai = info;
const entry = readOpenAIEntry(store) ?? {};
entry.oauth = info;
writeOpenAIEntry(store, entry);
writeAuthStore(store);
}
export function clearOpenAIAuth(): void {
const store = readAuthStore();
delete store.openai;
const entry = readOpenAIEntry(store);
if (entry) {
delete entry.oauth;
writeOpenAIEntry(store, entry);
} else {
delete store.openai;
}
writeAuthStore(store);
}
export function loadStoredOpenAIApiKey(): string | null {
const store = readAuthStore();
const entry = readOpenAIEntry(store);
return entry?.api_key?.api_key ?? null;
}
export function storeOpenAIApiKey(key: string): void {
const trimmed = key.trim();
if (!trimmed) {
throw new Error('OpenAI API key is empty');
}
const store = readAuthStore();
const entry = readOpenAIEntry(store) ?? {};
entry.api_key = { api_key: trimmed, created_at: new Date().toISOString() };
writeOpenAIEntry(store, entry);
writeAuthStore(store);
}
export function clearOpenAIApiKey(): void {
const store = readAuthStore();
const entry = readOpenAIEntry(store);
if (!entry) {
return;
}
delete entry.api_key;
writeOpenAIEntry(store, entry);
writeAuthStore(store);
}
/**
* Get an OpenAI API key from any available source.
* Priority: OPENAI_API_KEY → stored auth.json.
*/
export function getOpenAIApiKey(): string | null {
return process.env.OPENAI_API_KEY
?? loadStoredOpenAIApiKey()
?? null;
}
export function parseJwtClaims(token: string): IdTokenClaims | undefined {
const parts = token.split('.');
if (parts.length !== 3) {return undefined;}