auth: add OpenAI API key storage
This commit is contained in:
+113
-4
@@ -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;}
|
||||
|
||||
Reference in New Issue
Block a user