import { requestApi } from './http.js'; import { getCacheFile, readCachedToken, writeCache, deleteCache } from './cache.js'; import { loadGlobalEnv, reloadGlobalEnv } from './env.js'; const SESSION_RETRYABLE_STATUS = new Set([401, 403]); const SESSION_RETRYABLE_BODY_MARKERS = [ 'session not found or expired', 'invalid or expired token', 'unauthorized', 'client key expired', 'client key revoked', ]; function buildConfig(options) { return { authBase: (options.authBase || process.env.AUTH_BASE || 'https://api-gw-test.yuanwei-lnc.com').replace(/\/$/, ''), clientKey: options.clientKey || process.env.CLIENT_KEY || '', authCacheDir: options.cacheDir || process.env.AUTH_CACHE_DIR || '/tmp/skill-auth-cache', authMinTtlSec: options.minTtlSec ?? parseInt(process.env.AUTH_MIN_TTL_SEC || '60', 10), }; } function isRetryable(response) { if (!SESSION_RETRYABLE_STATUS.has(response.status)) return false; const body = (response.body || '').toLowerCase(); if (!body) return true; return SESSION_RETRYABLE_BODY_MARKERS.some((m) => body.includes(m)); } function isKeyError(response) { const body = (response.body || '').toLowerCase(); return body.includes('client key revoked') || body.includes('client key expired'); } export class SkillClient { config; apiBase; dryRun; options; constructor(options = {}) { this.options = options; loadGlobalEnv(); this.config = buildConfig(options); this.apiBase = (options.apiBase || process.env.ECOM_BASE || this.config.authBase).replace(/\/$/, ''); this.dryRun = options.dryRun ?? false; if (!this.dryRun && !this.config.clientKey) { throw new Error('CLIENT_KEY is required. Set via env or pass clientKey option.'); } } /** Fetch raw session info (token + expiresIn) */ async session() { if (this.dryRun) { return { accessToken: '', expiresIn: 900 }; } return this.fetchSession(); } /** Fetch client config (metadata with hook info, provider keys, etc.) */ async clientConfig() { if (this.dryRun) { return { clientId: '', name: '', status: 'active', metadata: {} }; } const result = await requestApi('GET', `${this.config.authBase}/auth/skill-credit/client-config`, undefined, undefined, { 'X-Client-Key': this.config.clientKey }); if (result.status < 200 || result.status >= 300) { throw new Error(`Client config failed: HTTP ${result.status} — ${result.body}`); } return JSON.parse(result.body); } async get(path) { return this.request('GET', path); } async post(path, body) { return this.request('POST', path, body); } async put(path, body) { return this.request('PUT', path, body); } async patch(path, body) { return this.request('PATCH', path, body); } async delete(path, body) { return this.request('DELETE', path, body); } // ---- internal ---- async request(method, path, body) { if (this.dryRun) { return { status: 200, body: JSON.stringify({ dryRun: true, method, path }) }; } const url = `${this.apiBase}${path}`; const bodyStr = body != null ? JSON.stringify(body) : undefined; // 1. Try with cached or fresh token const token = await this.getToken(); const first = await requestApi(method, url, token, bodyStr); if (!isRetryable(first)) { return first; } // 2. Token rejected — clear cache, fetch new session with same key const freshToken = await this.refreshToken(); const second = await requestApi(method, url, freshToken, bodyStr); if (!isRetryable(second)) { return second; } // 3. Still failing — maybe CLIENT_KEY changed in .env, reload and retry this.reloadConfig(); const reloadedToken = await this.refreshToken(); return requestApi(method, url, reloadedToken, bodyStr); } async getToken() { const cacheFile = getCacheFile(this.config.authBase, this.config.clientKey, this.config.authCacheDir); const cached = readCachedToken(cacheFile, this.config.authMinTtlSec); if (cached) return cached; const session = await this.fetchSession(); writeCache(cacheFile, session); return session.accessToken; } async refreshToken() { const cacheFile = getCacheFile(this.config.authBase, this.config.clientKey, this.config.authCacheDir); deleteCache(cacheFile); const session = await this.fetchSession(); writeCache(cacheFile, session); return session.accessToken; } /** * Re-read ~/.openclaw/.env and rebuild config. * Called when auth keeps failing — the CLIENT_KEY may have been updated on disk. */ reloadConfig() { reloadGlobalEnv(); const oldKey = this.config.clientKey; this.config = buildConfig(this.options); this.apiBase = (this.options.apiBase || process.env.ECOM_BASE || this.config.authBase).replace(/\/$/, ''); if (this.config.clientKey !== oldKey) { // Clear old key's cache too const oldCacheFile = getCacheFile(this.config.authBase, oldKey, this.config.authCacheDir); deleteCache(oldCacheFile); } } async fetchSession() { const result = await requestApi('POST', `${this.config.authBase}/auth/skill-credit/session`, undefined, JSON.stringify({ clientKey: this.config.clientKey })); if (result.status < 200 || result.status >= 300) { throw new Error(`Auth session failed: HTTP ${result.status} — ${result.body}`); } const session = JSON.parse(result.body); if (!session.accessToken) { throw new Error(`Missing accessToken in session response: ${result.body}`); } return session; } } export function createSkillClient(options) { return new SkillClient(options); } //# sourceMappingURL=client.js.map