feat: add SkillClient — single entry point for authenticated API calls

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 <noreply@anthropic.com>
This commit is contained in:
ywkj 2026-03-17 08:17:46 +08:00
parent 70cf86889e
commit 69051a3479
10 changed files with 326 additions and 41 deletions

34
dist/client.d.ts vendored Normal file
View File

@ -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<SessionResponse>;
get(path: string): Promise<ApiResponse>;
post(path: string, body?: unknown): Promise<ApiResponse>;
put(path: string, body?: unknown): Promise<ApiResponse>;
patch(path: string, body?: unknown): Promise<ApiResponse>;
delete(path: string, body?: unknown): Promise<ApiResponse>;
private request;
private getToken;
private refreshToken;
private fetchSession;
}
export declare function createSkillClient(options?: SkillClientOptions): SkillClient;
//# sourceMappingURL=client.d.ts.map

1
dist/client.d.ts.map vendored Normal file
View File

@ -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"}

108
dist/client.js vendored Normal file
View File

@ -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: '<dry-run-token>', 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

1
dist/client.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}

13
dist/index.d.ts vendored
View File

@ -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

2
dist/index.d.ts.map vendored
View File

@ -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"}
{"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"}

16
dist/index.js vendored
View File

@ -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

2
dist/index.js.map vendored
View File

@ -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"}
{"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"}

152
src/client.ts Normal file
View File

@ -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<SessionResponse> {
if (this.dryRun) {
return { accessToken: '<dry-run-token>', expiresIn: 900 };
}
return this.fetchSession();
}
async get(path: string): Promise<ApiResponse> {
return this.request('GET', path);
}
async post(path: string, body?: unknown): Promise<ApiResponse> {
return this.request('POST', path, body);
}
async put(path: string, body?: unknown): Promise<ApiResponse> {
return this.request('PUT', path, body);
}
async patch(path: string, body?: unknown): Promise<ApiResponse> {
return this.request('PATCH', path, body);
}
async delete(path: string, body?: unknown): Promise<ApiResponse> {
return this.request('DELETE', path, body);
}
// ---- internal ----
private async request(method: HttpMethod, path: string, body?: unknown): Promise<ApiResponse> {
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<string> {
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<string> {
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<SessionResponse> {
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);
}

View File

@ -1,34 +1,20 @@
/**
* @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';
// ---- 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,