/**
* 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;
}