Skip to main content
Glama
token-manager.ts4.8 kB
import type { OAuthToken } from '../types/auth.js' import { CryptoUtils } from '../utils/crypto.js' import { Logger } from '../utils/logger.js' export interface TokenStorage { set(key: string, value: string): Promise<void> | void get(key: string): Promise<string | undefined> | string | undefined delete(key: string): Promise<void> | void entries(): AsyncIterable<[string, string]> | Iterable<[string, string]> } class InMemoryTokenStorage implements TokenStorage { private map = new Map<string, string>() set(key: string, value: string): void { this.map.set(key, value) } get(key: string): string | undefined { return this.map.get(key) } delete(key: string): void { this.map.delete(key) } *entries(): Iterable<[string, string]> { yield* this.map.entries() } } export class TokenManager { private readonly storage: TokenStorage private readonly encKey: string constructor(options?: { storage?: TokenStorage; secret?: string }) { this.storage = options?.storage ?? autoDetectStorage() const g: any = globalThis as any const env = (g?.process?.env ?? g?.__WORKER_ENV ?? {}) as Record<string, string> const provided = options?.secret ?? (env as any).TOKEN_ENC_KEY if (!provided) { const envName = ((g?.process?.env ?? (g?.__WORKER_ENV ?? {})) as any).NODE_ENV ?? 'development' if (envName === 'production') { throw new Error('TOKEN_ENC_KEY is required in production for secure token storage') } Logger.warn('TOKEN_ENC_KEY missing; generating ephemeral dev key (tokens won\'t persist across restarts)') this.encKey = CryptoUtils.generateSecureRandom(32) } else { this.encKey = provided } } async storeToken(key: string, token: OAuthToken): Promise<void> { const serialized = JSON.stringify(token) const encrypted = CryptoUtils.encrypt(serialized, this.encKey) await this.storage.set(key, encrypted) } async getToken(key: string): Promise<OAuthToken | null> { const encrypted = await this.storage.get(key) if (!encrypted) return null try { const decrypted = CryptoUtils.decrypt(encrypted, this.encKey) return JSON.parse(decrypted) as OAuthToken } catch (err) { Logger.error('Failed to decrypt token; deleting corrupted entry', { key, err: String(err) }) await this.storage.delete(key) return null } } async cleanupExpiredTokens(): Promise<void> { const now = Date.now() for await (const [k, v] of this.storage.entries() as AsyncIterable<[string, string]>) { try { const tok = JSON.parse(CryptoUtils.decrypt(v, this.encKey)) as OAuthToken if (typeof tok.expires_at === 'number' && tok.expires_at <= now) { await this.storage.delete(k) } } catch { await this.storage.delete(k) } } } generateState(data: unknown): string { const payload = JSON.stringify({ d: data, t: Date.now() }) return CryptoUtils.encrypt(payload, this.encKey) } validateState(state: string, expectedData: unknown): boolean { try { const payload = JSON.parse(CryptoUtils.decrypt(state, this.encKey)) as { d: unknown } return JSON.stringify(payload.d) === JSON.stringify(expectedData) } catch { return false } } } export { InMemoryTokenStorage } /** * Auto-detects the best available storage backend. * - Cloudflare Workers: KV namespace bound as `TOKENS` * - Fallback: in-memory (non-persistent) */ function autoDetectStorage(): TokenStorage { const g: any = globalThis as any const env = g.__WORKER_ENV || {} const kv = env.TOKENS || g.TOKENS || g.TOKENS_KV if (kv && typeof kv.get === 'function' && typeof kv.put === 'function' && typeof kv.delete === 'function') { return new KVTokenStorage(kv) } return new InMemoryTokenStorage() } class KVTokenStorage implements TokenStorage { constructor(private readonly kv: { get: (k: string) => Promise<string | null>; put: (k: string, v: string, opts?: any) => Promise<void>; delete: (k: string) => Promise<void>; list?: (opts?: any) => Promise<{ keys: { name: string }[] }> }) {} async set(key: string, value: string): Promise<void> { await this.kv.put(key, value) } async get(key: string): Promise<string | undefined> { const v = await this.kv.get(key) return v === null ? undefined : v } async delete(key: string): Promise<void> { await this.kv.delete(key) } async *entries(): AsyncIterable<[string, string]> { if (typeof this.kv.list === 'function') { const { keys } = await this.kv.list() for (const k of keys) { const v = await this.kv.get(k.name) if (v !== null) yield [k.name, v] } } else { // KV without list support: nothing to iterate return } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/master-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server