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:
parent
70cf86889e
commit
69051a3479
|
|
@ -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
|
||||||
|
|
@ -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"}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
/**
|
/**
|
||||||
* @clawd/auth-runtime
|
* @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 { createSkillClient, SkillClient } from './client.js';
|
||||||
export { requestApi } from './http.js';
|
export type { SkillClientOptions } from './client.js';
|
||||||
export { sha256, getCacheFile, readCachedToken, writeCache, deleteCache, } from './cache.js';
|
|
||||||
export type { EnvConfig, SessionResponse, CachedTokenData, ApiResponse, HttpMethod, } from './types.js';
|
export type { EnvConfig, SessionResponse, CachedTokenData, ApiResponse, HttpMethod, } from './types.js';
|
||||||
//# sourceMappingURL=index.d.ts.map
|
//# sourceMappingURL=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"}
|
{"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"}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* @clawd/auth-runtime
|
* @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
|
// ---- Primary API ----
|
||||||
export { createEnvConfig, fetchSessionJson, getAccessToken, refreshAccessToken, isRetryableSessionError, requestApiWithAutoRefresh, } from './auth.js';
|
export { createSkillClient, SkillClient } from './client.js';
|
||||||
// HTTP utilities
|
|
||||||
export { requestApi } from './http.js';
|
|
||||||
// Cache utilities
|
|
||||||
export { sha256, getCacheFile, readCachedToken, writeCache, deleteCache, } from './cache.js';
|
|
||||||
//# sourceMappingURL=index.js.map
|
//# sourceMappingURL=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"}
|
{"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"}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
34
src/index.ts
34
src/index.ts
|
|
@ -1,34 +1,20 @@
|
||||||
/**
|
/**
|
||||||
* @clawd/auth-runtime
|
* @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
|
// ---- Primary API ----
|
||||||
export {
|
export { createSkillClient, SkillClient } from './client.js';
|
||||||
createEnvConfig,
|
export type { SkillClientOptions } from './client.js';
|
||||||
fetchSessionJson,
|
|
||||||
getAccessToken,
|
|
||||||
refreshAccessToken,
|
|
||||||
isRetryableSessionError,
|
|
||||||
requestApiWithAutoRefresh,
|
|
||||||
} from './auth.js';
|
|
||||||
|
|
||||||
// HTTP utilities
|
// ---- Types ----
|
||||||
export { requestApi } from './http.js';
|
|
||||||
|
|
||||||
// Cache utilities
|
|
||||||
export {
|
|
||||||
sha256,
|
|
||||||
getCacheFile,
|
|
||||||
readCachedToken,
|
|
||||||
writeCache,
|
|
||||||
deleteCache,
|
|
||||||
} from './cache.js';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export type {
|
export type {
|
||||||
EnvConfig,
|
EnvConfig,
|
||||||
SessionResponse,
|
SessionResponse,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue