/** * GSS-TEC Sovereign Node — Client SDK * * Drop this on any website. It handles: * 1. Leasing a 24h JWT from Cloudflare (once per day, cached in localStorage) * 2. Sending chat messages directly to the HF Engine with that JWT * 3. Auto-renewing the JWT when it expires * * Usage: * * */ class GSSClient { /** * @param {object} opts * @param {string} opts.apiKey - Your GSS subscriber API key * @param {string} opts.cfWorkerUrl - Cloudflare Worker base URL * @param {string} opts.hfEngineUrl - Hugging Face Space base URL * @param {string} [opts.model] - Groq model name (default: llama-3.3-70b-versatile) * @param {string} [opts.storageKey] - localStorage key for JWT cache (default: gss_jwt) */ constructor(opts) { if (!opts.apiKey || !opts.cfWorkerUrl || !opts.hfEngineUrl) { throw new Error('[GSSClient] apiKey, cfWorkerUrl, and hfEngineUrl are required'); } this.apiKey = opts.apiKey; this.cfWorkerUrl = opts.cfWorkerUrl.replace(/\/$/, ''); this.hfEngineUrl = opts.hfEngineUrl.replace(/\/$/, ''); this.model = opts.model || 'llama-3.1-8b-instant'; this.storageKey = opts.storageKey || 'gss_jwt'; this._token = null; // Start keep-alive sync (only in browser context) if (typeof window !== 'undefined') this._startKeepAlive(); } // ── JWT management ────────────────────────────────────────────────────────── /** Load cached token from localStorage, return null if missing/expired */ _loadCachedToken() { try { const raw = localStorage.getItem(this.storageKey); if (!raw) return null; const { token, exp, hf_space } = JSON.parse(raw); if (Date.now() / 1000 < exp - 300) { if (hf_space) this.hfEngineUrl = hf_space.replace(/\/$/, ''); return token; } localStorage.removeItem(this.storageKey); } catch { /* ignore */ } return null; } /** Decode JWT payload without verifying (client-side only, for exp check) */ _decodeExp(token) { try { const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))); return payload.exp || 0; } catch { return 0; } } /** Fetch a fresh 24h JWT from Cloudflare */ async _lease() { const res = await fetch(`${this.cfWorkerUrl}/auth/lease`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_key: this.apiKey }), }); if (res.status === 403) { throw new Error('[GSSClient] Subscription expired or invalid API key'); } if (!res.ok) { throw new Error(`[GSSClient] Lease failed: ${res.status}`); } const { token, hf_space } = await res.json(); const exp = this._decodeExp(token); localStorage.setItem(this.storageKey, JSON.stringify({ token, exp, hf_space })); this._token = token; // Switch to the assigned HF Space for load balancing if (hf_space) this.hfEngineUrl = hf_space.replace(/\/$/, ''); return token; } /** Get a valid token — from cache or fresh lease */ async _getToken() { if (!this._token) { this._token = this._loadCachedToken(); } if (!this._token) { this._token = await this._lease(); } return this._token; } // ── Chat ──────────────────────────────────────────────────────────────────── /** * Send a chat request to the HF Engine. * * @param {Array<{role: string, content: string}>} messages * @param {object} [opts] * @param {string} [opts.model] - Override model for this request * @param {number} [opts.temperature] - 0.0–2.0 * @param {number} [opts.max_tokens] - Max response tokens * @returns {Promise} The assistant's reply text */ async chat(messages, opts = {}) { const token = await this._getToken(); const body = { model: opts.model || this.model, messages, temperature: opts.temperature ?? 0.7, max_tokens: opts.max_tokens ?? 1024, }; // Route to Ollama if provider specified if (opts.provider) { body.provider = opts.provider; } const res = await fetch(`${this.hfEngineUrl}/v1/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); // If JWT rejected (expired or invalid), clear cache and retry once if (res.status === 401 || res.status === 403) { localStorage.removeItem(this.storageKey); this._token = null; return this.chat(messages, opts); } if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); throw new Error(`[GSSClient] Chat error ${res.status}: ${err.error || res.statusText}`); } const data = await res.json(); return data.choices?.[0]?.message?.content ?? ''; } // ── Utility ───────────────────────────────────────────────────────────────── /** Ping the HF Engine to check it's alive */ async ping() { const res = await fetch(`${this.hfEngineUrl}/ping`); return res.json(); } /** Force a fresh JWT lease (useful after subscription renewal) */ async renewToken() { localStorage.removeItem(this.storageKey); this._token = null; return this._lease(); } // ── Keep-alive sync ──────────────────────────────────────────────────────── /** Signal Cloudflare to push this subscriber's keys to HF Space. * Called automatically every 30 minutes to keep the key pool warm. */ async _syncKeys() { try { const token = await this._getToken(); const res = await fetch(`${this.cfWorkerUrl}/auth/keep-alive`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, }); // Token rejected — clear cache so next chat triggers a fresh lease if (res.status === 401 || res.status === 403) { localStorage.removeItem(this.storageKey); this._token = null; } } catch (_) { /* silent — non-critical */ } } _startKeepAlive() { // Fire once 5s after page load, then every 30 minutes setTimeout(() => this._syncKeys(), 5000); setInterval(() => this._syncKeys(), 30 * 60 * 1000); } } // Support both browser global and CommonJS/ESM if (typeof module !== 'undefined' && module.exports) { module.exports = GSSClient; } else if (typeof window !== 'undefined') { window.GSSClient = GSSClient; }