Skip to main content
Glama
pkce-manager.ts2.96 kB
// PKCE (Proof Key for Code Exchange) manager with cross-platform support // Generates code_verifier and S256 code_challenge and tracks them per state export interface PkceRecord { verifier: string method: 'S256' | 'plain' createdAt: number expiresAt: number } export interface PkceManagerOptions { ttlMs?: number } function getCrypto(): any { const g: any = globalThis as any if (g.crypto && g.crypto.subtle && g.crypto.getRandomValues) return g.crypto as any // Node fallback try { // eslint-disable-next-line @typescript-eslint/no-var-requires const nodeCrypto = require('node:crypto') return nodeCrypto.webcrypto as any } catch { throw new Error('Secure crypto not available in this environment') } } function base64UrlEncode(bytes: Uint8Array): string { let str = '' for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]) // btoa is available in browser/worker; Node 18 has global btoa via Buffer workaround // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const b64 = typeof btoa === 'function' ? btoa(str) : Buffer.from(bytes).toString('base64') return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') } function randomString(length = 64): string { const crypto = getCrypto() const bytes = new Uint8Array(length) crypto.getRandomValues(bytes) // Allowed characters for verifier are [A-Z a-z 0-9 - . _ ~] const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~' let out = '' for (let i = 0; i < length; i++) out += chars[bytes[i] % chars.length] return out } export class PKCEManager { private readonly store = new Map<string, PkceRecord>() private readonly ttl: number constructor(options?: PkceManagerOptions) { this.ttl = options?.ttlMs ?? 10 * 60_000 // 10 minutes } async generate(state: string): Promise<{ challenge: string; method: 'S256'; verifier: string }> { const verifier = randomString(64) const challenge = await this.computeS256(verifier) const now = Date.now() this.store.set(state, { verifier, method: 'S256', createdAt: now, expiresAt: now + this.ttl, }) return { challenge, method: 'S256', verifier } } getVerifier(state: string, consume = true): string | undefined { const rec = this.store.get(state) if (!rec) return undefined if (rec.expiresAt <= Date.now()) { this.store.delete(state) return undefined } if (consume) this.store.delete(state) return rec.verifier } cleanup(): void { const now = Date.now() for (const [k, v] of this.store) if (v.expiresAt <= now) this.store.delete(k) } private async computeS256(verifier: string): Promise<string> { const crypto = getCrypto() const enc = new TextEncoder().encode(verifier) const digest = await crypto.subtle.digest('SHA-256', enc) return base64UrlEncode(new Uint8Array(digest)) } }

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