From 69051a3479c9563f51ed366100614c6612256a5f Mon Sep 17 00:00:00 2001 From: ywkj Date: Tue, 17 Mar 2026 08:17:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20SkillClient=20=E2=80=94=20single?= =?UTF-8?q?=20entry=20point=20for=20authenticated=20API=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skills no longer need to manage config, tokens, or retry logic. Just `createSkillClient()` and call `client.post(path, body)`. Co-Authored-By: Claude Opus 4.6 --- dist/client.d.ts | 34 ++++++++++ dist/client.d.ts.map | 1 + dist/client.js | 108 ++++++++++++++++++++++++++++++ dist/client.js.map | 1 + dist/index.d.ts | 13 ++-- dist/index.d.ts.map | 2 +- dist/index.js | 16 ++--- dist/index.js.map | 2 +- src/client.ts | 152 +++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 38 ++++------- 10 files changed, 326 insertions(+), 41 deletions(-) create mode 100644 dist/client.d.ts create mode 100644 dist/client.d.ts.map create mode 100644 dist/client.js create mode 100644 dist/client.js.map create mode 100644 src/client.ts diff --git a/dist/client.d.ts b/dist/client.d.ts new file mode 100644 index 0000000..411664c --- /dev/null +++ b/dist/client.d.ts @@ -0,0 +1,34 @@ +import type { ApiResponse, SessionResponse } from './types.js'; +export interface SkillClientOptions { + /** Override AUTH_BASE (gateway URL for token acquisition) */ + authBase?: string; + /** Override CLIENT_KEY */ + clientKey?: string; + /** Override base URL for business API calls (defaults to authBase) */ + apiBase?: string; + /** Dry run mode — no real HTTP calls */ + dryRun?: boolean; + /** Cache directory (default: /tmp/skill-auth-cache) */ + cacheDir?: string; + /** Min TTL before token refresh, seconds (default: 60) */ + minTtlSec?: number; +} +export declare class SkillClient { + private readonly config; + private readonly apiBase; + private readonly dryRun; + constructor(options?: SkillClientOptions); + /** Fetch raw session info (token + hookUrl + hookToken + expiresIn) */ + session(): Promise; + get(path: string): Promise; + post(path: string, body?: unknown): Promise; + put(path: string, body?: unknown): Promise; + patch(path: string, body?: unknown): Promise; + delete(path: string, body?: unknown): Promise; + private request; + private getToken; + private refreshToken; + private fetchSession; +} +export declare function createSkillClient(options?: SkillClientOptions): SkillClient; +//# sourceMappingURL=client.d.ts.map \ No newline at end of file diff --git a/dist/client.d.ts.map b/dist/client.d.ts.map new file mode 100644 index 0000000..5040beb --- /dev/null +++ b/dist/client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,eAAe,EAAE,MAAM,YAAY,CAAC;AAatF,MAAM,WAAW,kBAAkB;IACjC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;gBAErB,OAAO,GAAE,kBAAuB;IAU5C,uEAAuE;IACjE,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;IAOnC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAIvC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IAIxD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IAIvD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IAIzD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;YAMlD,OAAO;YAoBP,QAAQ;YAUR,YAAY;YASZ,YAAY;CAmB3B;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAE3E"} \ No newline at end of file diff --git a/dist/client.js b/dist/client.js new file mode 100644 index 0000000..d0e142b --- /dev/null +++ b/dist/client.js @@ -0,0 +1,108 @@ +import { requestApi } from './http.js'; +import { getCacheFile, readCachedToken, writeCache, deleteCache } from './cache.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)); +} +export class SkillClient { + config; + apiBase; + dryRun; + constructor(options = {}) { + 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 + hookUrl + hookToken + expiresIn) */ + async session() { + if (this.dryRun) { + return { accessToken: '', expiresIn: 900 }; + } + return this.fetchSession(); + } + 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; + const token = await this.getToken(); + const first = await requestApi(method, url, token, bodyStr); + if (!isRetryable(first)) { + return first; + } + // Token expired — refresh and retry once + const freshToken = await this.refreshToken(); + return requestApi(method, url, freshToken, 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; + } + 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 \ No newline at end of file diff --git a/dist/client.js.map b/dist/client.js.map new file mode 100644 index 0000000..460c9dc --- /dev/null +++ b/dist/client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 30c3218..8e0c032 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,12 +1,15 @@ /** * @clawd/auth-runtime * - * Shared TypeScript auth runtime for OpenClaw skills. + * Shared auth runtime for OpenClaw skills. * - * Provides authentication, token caching, and HTTP utilities. + * Primary API — just use the client: + * + * import { createSkillClient } from '@clawd/auth-runtime'; + * const client = createSkillClient(); + * const res = await client.post('/ecom/tasks/scrape', payload); */ -export { createEnvConfig, fetchSessionJson, getAccessToken, refreshAccessToken, isRetryableSessionError, requestApiWithAutoRefresh, } from './auth.js'; -export { requestApi } from './http.js'; -export { sha256, getCacheFile, readCachedToken, writeCache, deleteCache, } from './cache.js'; +export { createSkillClient, SkillClient } from './client.js'; +export type { SkillClientOptions } from './client.js'; export type { EnvConfig, SessionResponse, CachedTokenData, ApiResponse, HttpMethod, } from './types.js'; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map index ee312b5..1c50fec 100644 --- a/dist/index.d.ts.map +++ b/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,OAAO,EACL,MAAM,EACN,YAAY,EACZ,eAAe,EACf,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,SAAS,EACT,eAAe,EACf,eAAe,EACf,WAAW,EACX,UAAU,GACX,MAAM,YAAY,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtD,YAAY,EACV,SAAS,EACT,eAAe,EACf,eAAe,EACf,WAAW,EACX,UAAU,GACX,MAAM,YAAY,CAAC"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 1a3c8ad..8888d2c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,14 +1,14 @@ /** * @clawd/auth-runtime * - * Shared TypeScript auth runtime for OpenClaw skills. + * Shared auth runtime for OpenClaw skills. * - * Provides authentication, token caching, and HTTP utilities. + * Primary API — just use the client: + * + * import { createSkillClient } from '@clawd/auth-runtime'; + * const client = createSkillClient(); + * const res = await client.post('/ecom/tasks/scrape', payload); */ -// Auth functions -export { createEnvConfig, fetchSessionJson, getAccessToken, refreshAccessToken, isRetryableSessionError, requestApiWithAutoRefresh, } from './auth.js'; -// HTTP utilities -export { requestApi } from './http.js'; -// Cache utilities -export { sha256, getCacheFile, readCachedToken, writeCache, deleteCache, } from './cache.js'; +// ---- Primary API ---- +export { createSkillClient, SkillClient } from './client.js'; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map index d1f0e2b..05d87c0 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,iBAAiB;AACjB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,WAAW,CAAC;AAEnB,iBAAiB;AACjB,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,kBAAkB;AAClB,OAAO,EACL,MAAM,EACN,YAAY,EACZ,eAAe,EACf,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wBAAwB;AACxB,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"} \ No newline at end of file diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..0c4c284 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,152 @@ +import type { ApiResponse, EnvConfig, HttpMethod, SessionResponse } from './types.js'; +import { requestApi } from './http.js'; +import { getCacheFile, readCachedToken, writeCache, deleteCache } from './cache.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', +]; + +export interface SkillClientOptions { + /** Override AUTH_BASE (gateway URL for token acquisition) */ + authBase?: string; + /** Override CLIENT_KEY */ + clientKey?: string; + /** Override base URL for business API calls (defaults to authBase) */ + apiBase?: string; + /** Dry run mode — no real HTTP calls */ + dryRun?: boolean; + /** Cache directory (default: /tmp/skill-auth-cache) */ + cacheDir?: string; + /** Min TTL before token refresh, seconds (default: 60) */ + minTtlSec?: number; +} + +function buildConfig(options: SkillClientOptions): EnvConfig { + 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: ApiResponse): boolean { + 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)); +} + +export class SkillClient { + private readonly config: EnvConfig; + private readonly apiBase: string; + private readonly dryRun: boolean; + + constructor(options: SkillClientOptions = {}) { + 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 + hookUrl + hookToken + expiresIn) */ + async session(): Promise { + if (this.dryRun) { + return { accessToken: '', expiresIn: 900 }; + } + return this.fetchSession(); + } + + async get(path: string): Promise { + return this.request('GET', path); + } + + async post(path: string, body?: unknown): Promise { + return this.request('POST', path, body); + } + + async put(path: string, body?: unknown): Promise { + return this.request('PUT', path, body); + } + + async patch(path: string, body?: unknown): Promise { + return this.request('PATCH', path, body); + } + + async delete(path: string, body?: unknown): Promise { + return this.request('DELETE', path, body); + } + + // ---- internal ---- + + private async request(method: HttpMethod, path: string, body?: unknown): Promise { + 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; + + const token = await this.getToken(); + const first = await requestApi(method, url, token, bodyStr); + + if (!isRetryable(first)) { + return first; + } + + // Token expired — refresh and retry once + const freshToken = await this.refreshToken(); + return requestApi(method, url, freshToken, bodyStr); + } + + private async getToken(): Promise { + 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; + } + + private async refreshToken(): Promise { + 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; + } + + private async fetchSession(): Promise { + 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) as SessionResponse; + if (!session.accessToken) { + throw new Error(`Missing accessToken in session response: ${result.body}`); + } + + return session; + } +} + +export function createSkillClient(options?: SkillClientOptions): SkillClient { + return new SkillClient(options); +} diff --git a/src/index.ts b/src/index.ts index 4c15754..34a6921 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,20 @@ /** * @clawd/auth-runtime - * - * Shared TypeScript auth runtime for OpenClaw skills. - * - * Provides authentication, token caching, and HTTP utilities. + * + * Shared auth runtime for OpenClaw skills. + * + * Primary API — just use the client: + * + * import { createSkillClient } from '@clawd/auth-runtime'; + * const client = createSkillClient(); + * const res = await client.post('/ecom/tasks/scrape', payload); */ -// Auth functions -export { - createEnvConfig, - fetchSessionJson, - getAccessToken, - refreshAccessToken, - isRetryableSessionError, - requestApiWithAutoRefresh, -} from './auth.js'; +// ---- Primary API ---- +export { createSkillClient, SkillClient } from './client.js'; +export type { SkillClientOptions } from './client.js'; -// HTTP utilities -export { requestApi } from './http.js'; - -// Cache utilities -export { - sha256, - getCacheFile, - readCachedToken, - writeCache, - deleteCache, -} from './cache.js'; - -// Types +// ---- Types ---- export type { EnvConfig, SessionResponse,