feat: initial auth-runtime module
This commit is contained in:
commit
70cf86889e
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
# @clawd/auth-runtime
|
||||||
|
|
||||||
|
Shared TypeScript auth runtime for OpenClaw skills.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Token caching with configurable TTL
|
||||||
|
- Automatic token refresh
|
||||||
|
- Session-expired auto retry (401/403)
|
||||||
|
- Environment-based configuration
|
||||||
|
- Type-safe API
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add file:../../_shared/auth-runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
createEnvConfig,
|
||||||
|
getAccessToken,
|
||||||
|
requestApiWithAutoRefresh,
|
||||||
|
} from '@clawd/auth-runtime';
|
||||||
|
|
||||||
|
const config = createEnvConfig();
|
||||||
|
const token = await getAccessToken(false, config);
|
||||||
|
|
||||||
|
const result = await requestApiWithAutoRefresh(
|
||||||
|
'POST',
|
||||||
|
`${config.authBase}/ecom/tasks/scrape`,
|
||||||
|
false,
|
||||||
|
config,
|
||||||
|
JSON.stringify({ url: 'https://detail.1688.com/offer/123.html' }),
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
#### `createEnvConfig(): EnvConfig`
|
||||||
|
|
||||||
|
Create configuration from environment variables:
|
||||||
|
- `AUTH_BASE`: Auth base URL (default: https://api-gw-test.yuanwei-lnc.com)
|
||||||
|
- `CLIENT_KEY`: Client key (required)
|
||||||
|
- `AUTH_CACHE_DIR`: Cache directory (default: /tmp/skill-auth-cache)
|
||||||
|
- `AUTH_MIN_TTL_SEC`: Minimum token TTL in seconds (default: 60)
|
||||||
|
|
||||||
|
#### `getAccessToken(dryRun, config): Promise<string>`
|
||||||
|
|
||||||
|
Get access token with caching.
|
||||||
|
|
||||||
|
#### `refreshAccessToken(dryRun, config): Promise<string>`
|
||||||
|
|
||||||
|
Refresh access token (bypass cache).
|
||||||
|
|
||||||
|
#### `fetchSessionJson(dryRun, config): Promise<SessionResponse>`
|
||||||
|
|
||||||
|
Fetch session JSON from auth endpoint.
|
||||||
|
|
||||||
|
#### `requestApi(method, url, authToken?, body?): Promise<ApiResponse>`
|
||||||
|
|
||||||
|
Make HTTP request with optional authorization header.
|
||||||
|
|
||||||
|
#### `isRetryableSessionError(response): boolean`
|
||||||
|
|
||||||
|
Check whether response likely indicates expired runtime session.
|
||||||
|
|
||||||
|
#### `requestApiWithAutoRefresh(method, url, dryRun, config, body?, accessToken?): Promise<ApiResponse>`
|
||||||
|
|
||||||
|
Call API with one automatic token refresh + retry on session-expired style errors.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
#### `EnvConfig`
|
||||||
|
|
||||||
|
Environment configuration.
|
||||||
|
|
||||||
|
#### `SessionResponse`
|
||||||
|
|
||||||
|
Session response from auth endpoint.
|
||||||
|
|
||||||
|
#### `CachedTokenData`
|
||||||
|
|
||||||
|
Cached token data.
|
||||||
|
|
||||||
|
#### `HttpMethod`
|
||||||
|
|
||||||
|
Supported HTTP methods.
|
||||||
|
|
||||||
|
#### `ApiResponse`
|
||||||
|
|
||||||
|
HTTP response.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "@clawd/auth-runtime",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.3.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { ApiResponse, EnvConfig, HttpMethod, SessionResponse } from './types.js';
|
||||||
|
/**
|
||||||
|
* Create environment configuration from process.env
|
||||||
|
*/
|
||||||
|
export declare function createEnvConfig(): EnvConfig;
|
||||||
|
/**
|
||||||
|
* Fetch session JSON from auth endpoint
|
||||||
|
*/
|
||||||
|
export declare function fetchSessionJson(dryRun: boolean, config: EnvConfig): Promise<SessionResponse>;
|
||||||
|
/**
|
||||||
|
* Get access token with caching
|
||||||
|
*/
|
||||||
|
export declare function getAccessToken(dryRun: boolean, config: EnvConfig): Promise<string>;
|
||||||
|
/**
|
||||||
|
* Refresh access token (bypass cache)
|
||||||
|
*/
|
||||||
|
export declare function refreshAccessToken(dryRun: boolean, config: EnvConfig): Promise<string>;
|
||||||
|
/**
|
||||||
|
* Check whether response likely indicates expired/invalid runtime session.
|
||||||
|
*/
|
||||||
|
export declare function isRetryableSessionError(response: ApiResponse): boolean;
|
||||||
|
/**
|
||||||
|
* Make API request with automatic runtime token refresh and one retry.
|
||||||
|
*/
|
||||||
|
export declare function requestApiWithAutoRefresh(method: HttpMethod, url: string, dryRun: boolean, config: EnvConfig, body?: string, accessToken?: string): Promise<ApiResponse>;
|
||||||
|
//# sourceMappingURL=auth.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAatF;;GAEG;AACH,wBAAgB,eAAe,IAAI,SAAS,CAO3C;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,eAAe,CAAC,CAiC1B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,MAAM,CAAC,CAoBjB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAWtE;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,SAAS,EACjB,IAAI,CAAC,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAUtB"}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* Create environment configuration from process.env
|
||||||
|
*/
|
||||||
|
export function createEnvConfig() {
|
||||||
|
return {
|
||||||
|
authBase: (process.env.AUTH_BASE || 'https://api-gw-test.yuanwei-lnc.com').replace(/\/$/, ''),
|
||||||
|
clientKey: process.env.CLIENT_KEY || '',
|
||||||
|
authCacheDir: process.env.AUTH_CACHE_DIR || '/tmp/skill-auth-cache',
|
||||||
|
authMinTtlSec: parseInt(process.env.AUTH_MIN_TTL_SEC || '60', 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Fetch session JSON from auth endpoint
|
||||||
|
*/
|
||||||
|
export async function fetchSessionJson(dryRun, config) {
|
||||||
|
if (dryRun) {
|
||||||
|
return {
|
||||||
|
accessToken: '<dry-run-token>',
|
||||||
|
hookUrl: '<dry-run-hook-url>',
|
||||||
|
hookToken: '<dry-run-hook-token>',
|
||||||
|
expiresIn: 900,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!config.clientKey) {
|
||||||
|
throw new Error('CLIENT_KEY is required');
|
||||||
|
}
|
||||||
|
const payload = JSON.stringify({ clientKey: config.clientKey });
|
||||||
|
const result = await requestApi('POST', `${config.authBase}/auth/skill-credit/session`, undefined, payload);
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error(`Auth session request 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;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get access token with caching
|
||||||
|
*/
|
||||||
|
export async function getAccessToken(dryRun, config) {
|
||||||
|
if (dryRun) {
|
||||||
|
return '<dry-run-token>';
|
||||||
|
}
|
||||||
|
if (!config.clientKey) {
|
||||||
|
throw new Error('CLIENT_KEY is required');
|
||||||
|
}
|
||||||
|
const cacheFile = getCacheFile(config.authBase, config.clientKey, config.authCacheDir);
|
||||||
|
const cachedToken = readCachedToken(cacheFile, config.authMinTtlSec);
|
||||||
|
if (cachedToken) {
|
||||||
|
return cachedToken;
|
||||||
|
}
|
||||||
|
const session = await fetchSessionJson(dryRun, config);
|
||||||
|
writeCache(cacheFile, session);
|
||||||
|
return session.accessToken;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Refresh access token (bypass cache)
|
||||||
|
*/
|
||||||
|
export async function refreshAccessToken(dryRun, config) {
|
||||||
|
if (dryRun) {
|
||||||
|
return '<dry-run-token>';
|
||||||
|
}
|
||||||
|
if (!config.clientKey) {
|
||||||
|
throw new Error('CLIENT_KEY is required');
|
||||||
|
}
|
||||||
|
const cacheFile = getCacheFile(config.authBase, config.clientKey, config.authCacheDir);
|
||||||
|
// Remove cache file if exists
|
||||||
|
deleteCache(cacheFile);
|
||||||
|
return getAccessToken(dryRun, config);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check whether response likely indicates expired/invalid runtime session.
|
||||||
|
*/
|
||||||
|
export function isRetryableSessionError(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((marker) => body.includes(marker));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Make API request with automatic runtime token refresh and one retry.
|
||||||
|
*/
|
||||||
|
export async function requestApiWithAutoRefresh(method, url, dryRun, config, body, accessToken) {
|
||||||
|
const token = accessToken || await getAccessToken(dryRun, config);
|
||||||
|
const first = await requestApi(method, url, token, body);
|
||||||
|
if (!isRetryableSessionError(first)) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
const freshToken = await refreshAccessToken(dryRun, config);
|
||||||
|
return requestApi(method, url, freshToken, body);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=auth.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Generate SHA256 hash for cache key
|
||||||
|
*/
|
||||||
|
export declare function sha256(input: string): string;
|
||||||
|
/**
|
||||||
|
* Get cache file path for current AUTH_BASE and CLIENT_KEY
|
||||||
|
*/
|
||||||
|
export declare function getCacheFile(authBase: string, clientKey: string, cacheDir: string): string;
|
||||||
|
/**
|
||||||
|
* Read cached token if valid
|
||||||
|
*/
|
||||||
|
export declare function readCachedToken(cacheFile: string, minTtlSec: number): string | null;
|
||||||
|
/**
|
||||||
|
* Write token to cache
|
||||||
|
*/
|
||||||
|
export declare function writeCache(cacheFile: string, sessionJson: {
|
||||||
|
accessToken: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}): void;
|
||||||
|
/**
|
||||||
|
* Delete cache file if exists
|
||||||
|
*/
|
||||||
|
export declare function deleteCache(cacheFile: string): void;
|
||||||
|
//# sourceMappingURL=cache.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAsBf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD,IAAI,CAYN;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAInD"}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
/**
|
||||||
|
* Generate SHA256 hash for cache key
|
||||||
|
*/
|
||||||
|
export function sha256(input) {
|
||||||
|
return crypto.createHash('sha256').update(input).digest('hex');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get cache file path for current AUTH_BASE and CLIENT_KEY
|
||||||
|
*/
|
||||||
|
export function getCacheFile(authBase, clientKey, cacheDir) {
|
||||||
|
const key = sha256(`${authBase}|${clientKey}`);
|
||||||
|
if (!fs.existsSync(cacheDir)) {
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return path.join(cacheDir, `session_${key}.json`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read cached token if valid
|
||||||
|
*/
|
||||||
|
export function readCachedToken(cacheFile, minTtlSec) {
|
||||||
|
if (!fs.existsSync(cacheFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
if (!data.accessToken || data.expiresAtEpoch <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Check if token is still valid (with min TTL buffer)
|
||||||
|
if (now + minTtlSec >= data.expiresAtEpoch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data.accessToken;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Write token to cache
|
||||||
|
*/
|
||||||
|
export function writeCache(cacheFile, sessionJson) {
|
||||||
|
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||||
|
const expiresIn = sessionJson.expiresIn > 0 ? sessionJson.expiresIn : 900;
|
||||||
|
const expiresAtEpoch = nowEpoch + expiresIn;
|
||||||
|
const cacheData = {
|
||||||
|
accessToken: sessionJson.accessToken,
|
||||||
|
expiresAtEpoch,
|
||||||
|
createdAtEpoch: nowEpoch,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Delete cache file if exists
|
||||||
|
*/
|
||||||
|
export function deleteCache(cacheFile) {
|
||||||
|
if (fs.existsSync(cacheFile)) {
|
||||||
|
fs.unlinkSync(cacheFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=cache.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC;;GAEG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,SAAiB,EAAE,QAAgB;IAChF,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC,CAAC;IAE/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAC7B,SAAiB,EACjB,SAAiB;IAEjB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAoB,CAAC;QAChF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAE1C,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,cAAc,IAAI,CAAC,EAAE,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,sDAAsD;QACtD,IAAI,GAAG,GAAG,SAAS,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YAC3C,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CACxB,SAAiB,EACjB,WAAuD;IAEvD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1E,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;IAE5C,MAAM,SAAS,GAAoB;QACjC,WAAW,EAAE,WAAW,CAAC,WAAW;QACpC,cAAc;QACd,cAAc,EAAE,QAAQ;KACzB,CAAC;IAEF,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAC3E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB;IAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC"}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import type { ApiResponse, HttpMethod } from './types.js';
|
||||||
|
/**
|
||||||
|
* Make HTTP request to API
|
||||||
|
*/
|
||||||
|
export declare function requestApi(method: HttpMethod, url: string, authToken?: string, body?: string): Promise<ApiResponse>;
|
||||||
|
//# sourceMappingURL=http.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1D;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,CAAC,CAyBtB"}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Make HTTP request to API
|
||||||
|
*/
|
||||||
|
export async function requestApi(method, url, authToken, body) {
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (authToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${authToken}`;
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
if (body) {
|
||||||
|
options.body = body;
|
||||||
|
}
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
const responseBody = await response.text();
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
body: responseBody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=http.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAAkB,EAClB,GAAW,EACX,SAAkB,EAClB,IAAa;IAEb,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;KACnC,CAAC;IAEF,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,SAAS,EAAE,CAAC;IACnD,CAAC;IAED,MAAM,OAAO,GAAgB;QAC3B,MAAM;QACN,OAAO;KACR,CAAC;IAEF,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IACtB,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAE3C,OAAO;QACL,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,IAAI,EAAE,YAAY;KACnB,CAAC;AACJ,CAAC"}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* @clawd/auth-runtime
|
||||||
|
*
|
||||||
|
* Shared TypeScript auth runtime for OpenClaw skills.
|
||||||
|
*
|
||||||
|
* Provides authentication, token caching, and HTTP utilities.
|
||||||
|
*/
|
||||||
|
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 type { EnvConfig, SessionResponse, CachedTokenData, ApiResponse, HttpMethod, } from './types.js';
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
|
|
@ -0,0 +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"}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* @clawd/auth-runtime
|
||||||
|
*
|
||||||
|
* Shared TypeScript auth runtime for OpenClaw skills.
|
||||||
|
*
|
||||||
|
* Provides authentication, token caching, and HTTP utilities.
|
||||||
|
*/
|
||||||
|
// 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';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
|
|
@ -0,0 +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"}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* Environment configuration for auth runtime
|
||||||
|
*/
|
||||||
|
export interface EnvConfig {
|
||||||
|
/** Authentication base URL */
|
||||||
|
authBase: string;
|
||||||
|
/** Client key for authentication */
|
||||||
|
clientKey: string;
|
||||||
|
/** Directory for storing auth cache files */
|
||||||
|
authCacheDir: string;
|
||||||
|
/** Minimum TTL for cached tokens in seconds */
|
||||||
|
authMinTtlSec: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Session response from /auth/skill-credit/session
|
||||||
|
*/
|
||||||
|
export interface SessionResponse {
|
||||||
|
accessToken: string;
|
||||||
|
ownerSessionToken?: string;
|
||||||
|
hookUrl?: string;
|
||||||
|
hookToken?: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cached token data stored on disk
|
||||||
|
*/
|
||||||
|
export interface CachedTokenData {
|
||||||
|
accessToken: string;
|
||||||
|
expiresAtEpoch: number;
|
||||||
|
createdAtEpoch: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* HTTP method used by requestApi
|
||||||
|
*/
|
||||||
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
|
||||||
|
/**
|
||||||
|
* HTTP API response
|
||||||
|
*/
|
||||||
|
export interface ApiResponse {
|
||||||
|
status: number;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=types.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd"}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=types.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bootstrap auth-runtime into the skill store.
|
||||||
|
# Run this ONCE before installing any skills.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# curl -fsSL https://your-forgejo/raw/auth-runtime/main/install.sh | bash
|
||||||
|
# # or:
|
||||||
|
# ./install.sh [skill-store-dir]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STORE="${1:-$HOME/.openclaw/skills}"
|
||||||
|
DEST="$STORE/_shared/auth-runtime"
|
||||||
|
REPO="${AUTH_RUNTIME_REPO:-}" # set via env or hardcode below
|
||||||
|
|
||||||
|
# Hardcode your Forgejo URL here:
|
||||||
|
# REPO="https://git.yourserver.com/you/auth-runtime.git"
|
||||||
|
|
||||||
|
if [[ -z "$REPO" ]]; then
|
||||||
|
echo "Error: set AUTH_RUNTIME_REPO=https://git.yourserver.com/you/auth-runtime.git"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$DEST/.git" ]]; then
|
||||||
|
echo "Updating auth-runtime..."
|
||||||
|
git -C "$DEST" pull --ff-only
|
||||||
|
else
|
||||||
|
echo "Installing auth-runtime to $DEST..."
|
||||||
|
mkdir -p "$STORE/_shared"
|
||||||
|
git clone "$REPO" "$DEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "auth-runtime ready at $DEST"
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "@clawd/auth-runtime",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Shared TypeScript auth runtime for OpenClaw skills",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"runtime",
|
||||||
|
"openclaw"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.3.3",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create environment configuration from process.env
|
||||||
|
*/
|
||||||
|
export function createEnvConfig(): EnvConfig {
|
||||||
|
return {
|
||||||
|
authBase: (process.env.AUTH_BASE || 'https://api-gw-test.yuanwei-lnc.com').replace(/\/$/, ''),
|
||||||
|
clientKey: process.env.CLIENT_KEY || '',
|
||||||
|
authCacheDir: process.env.AUTH_CACHE_DIR || '/tmp/skill-auth-cache',
|
||||||
|
authMinTtlSec: parseInt(process.env.AUTH_MIN_TTL_SEC || '60', 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch session JSON from auth endpoint
|
||||||
|
*/
|
||||||
|
export async function fetchSessionJson(
|
||||||
|
dryRun: boolean,
|
||||||
|
config: EnvConfig
|
||||||
|
): Promise<SessionResponse> {
|
||||||
|
if (dryRun) {
|
||||||
|
return {
|
||||||
|
accessToken: '<dry-run-token>',
|
||||||
|
hookUrl: '<dry-run-hook-url>',
|
||||||
|
hookToken: '<dry-run-hook-token>',
|
||||||
|
expiresIn: 900,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.clientKey) {
|
||||||
|
throw new Error('CLIENT_KEY is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.stringify({ clientKey: config.clientKey });
|
||||||
|
const result = await requestApi(
|
||||||
|
'POST',
|
||||||
|
`${config.authBase}/auth/skill-credit/session`,
|
||||||
|
undefined,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error(`Auth session request 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access token with caching
|
||||||
|
*/
|
||||||
|
export async function getAccessToken(
|
||||||
|
dryRun: boolean,
|
||||||
|
config: EnvConfig
|
||||||
|
): Promise<string> {
|
||||||
|
if (dryRun) {
|
||||||
|
return '<dry-run-token>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.clientKey) {
|
||||||
|
throw new Error('CLIENT_KEY is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheFile = getCacheFile(config.authBase, config.clientKey, config.authCacheDir);
|
||||||
|
const cachedToken = readCachedToken(cacheFile, config.authMinTtlSec);
|
||||||
|
|
||||||
|
if (cachedToken) {
|
||||||
|
return cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await fetchSessionJson(dryRun, config);
|
||||||
|
writeCache(cacheFile, session);
|
||||||
|
|
||||||
|
return session.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh access token (bypass cache)
|
||||||
|
*/
|
||||||
|
export async function refreshAccessToken(
|
||||||
|
dryRun: boolean,
|
||||||
|
config: EnvConfig
|
||||||
|
): Promise<string> {
|
||||||
|
if (dryRun) {
|
||||||
|
return '<dry-run-token>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.clientKey) {
|
||||||
|
throw new Error('CLIENT_KEY is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheFile = getCacheFile(config.authBase, config.clientKey, config.authCacheDir);
|
||||||
|
|
||||||
|
// Remove cache file if exists
|
||||||
|
deleteCache(cacheFile);
|
||||||
|
|
||||||
|
return getAccessToken(dryRun, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether response likely indicates expired/invalid runtime session.
|
||||||
|
*/
|
||||||
|
export function isRetryableSessionError(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((marker) => body.includes(marker));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make API request with automatic runtime token refresh and one retry.
|
||||||
|
*/
|
||||||
|
export async function requestApiWithAutoRefresh(
|
||||||
|
method: HttpMethod,
|
||||||
|
url: string,
|
||||||
|
dryRun: boolean,
|
||||||
|
config: EnvConfig,
|
||||||
|
body?: string,
|
||||||
|
accessToken?: string,
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
const token = accessToken || await getAccessToken(dryRun, config);
|
||||||
|
const first = await requestApi(method, url, token, body);
|
||||||
|
|
||||||
|
if (!isRetryableSessionError(first)) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
const freshToken = await refreshAccessToken(dryRun, config);
|
||||||
|
return requestApi(method, url, freshToken, body);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import type { CachedTokenData } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate SHA256 hash for cache key
|
||||||
|
*/
|
||||||
|
export function sha256(input: string): string {
|
||||||
|
return crypto.createHash('sha256').update(input).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache file path for current AUTH_BASE and CLIENT_KEY
|
||||||
|
*/
|
||||||
|
export function getCacheFile(authBase: string, clientKey: string, cacheDir: string): string {
|
||||||
|
const key = sha256(`${authBase}|${clientKey}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(cacheDir)) {
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(cacheDir, `session_${key}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read cached token if valid
|
||||||
|
*/
|
||||||
|
export function readCachedToken(
|
||||||
|
cacheFile: string,
|
||||||
|
minTtlSec: number
|
||||||
|
): string | null {
|
||||||
|
if (!fs.existsSync(cacheFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(cacheFile, 'utf-8')) as CachedTokenData;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
if (!data.accessToken || data.expiresAtEpoch <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is still valid (with min TTL buffer)
|
||||||
|
if (now + minTtlSec >= data.expiresAtEpoch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.accessToken;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write token to cache
|
||||||
|
*/
|
||||||
|
export function writeCache(
|
||||||
|
cacheFile: string,
|
||||||
|
sessionJson: { accessToken: string; expiresIn: number }
|
||||||
|
): void {
|
||||||
|
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||||
|
const expiresIn = sessionJson.expiresIn > 0 ? sessionJson.expiresIn : 900;
|
||||||
|
const expiresAtEpoch = nowEpoch + expiresIn;
|
||||||
|
|
||||||
|
const cacheData: CachedTokenData = {
|
||||||
|
accessToken: sessionJson.accessToken,
|
||||||
|
expiresAtEpoch,
|
||||||
|
createdAtEpoch: nowEpoch,
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete cache file if exists
|
||||||
|
*/
|
||||||
|
export function deleteCache(cacheFile: string): void {
|
||||||
|
if (fs.existsSync(cacheFile)) {
|
||||||
|
fs.unlinkSync(cacheFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { ApiResponse, HttpMethod } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make HTTP request to API
|
||||||
|
*/
|
||||||
|
export async function requestApi(
|
||||||
|
method: HttpMethod,
|
||||||
|
url: string,
|
||||||
|
authToken?: string,
|
||||||
|
body?: string
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${authToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
options.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
const responseBody = await response.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
body: responseBody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* @clawd/auth-runtime
|
||||||
|
*
|
||||||
|
* Shared TypeScript auth runtime for OpenClaw skills.
|
||||||
|
*
|
||||||
|
* Provides authentication, token caching, and HTTP utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type {
|
||||||
|
EnvConfig,
|
||||||
|
SessionResponse,
|
||||||
|
CachedTokenData,
|
||||||
|
ApiResponse,
|
||||||
|
HttpMethod,
|
||||||
|
} from './types.js';
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Environment configuration for auth runtime
|
||||||
|
*/
|
||||||
|
export interface EnvConfig {
|
||||||
|
/** Authentication base URL */
|
||||||
|
authBase: string;
|
||||||
|
/** Client key for authentication */
|
||||||
|
clientKey: string;
|
||||||
|
/** Directory for storing auth cache files */
|
||||||
|
authCacheDir: string;
|
||||||
|
/** Minimum TTL for cached tokens in seconds */
|
||||||
|
authMinTtlSec: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session response from /auth/skill-credit/session
|
||||||
|
*/
|
||||||
|
export interface SessionResponse {
|
||||||
|
accessToken: string;
|
||||||
|
ownerSessionToken?: string;
|
||||||
|
hookUrl?: string;
|
||||||
|
hookToken?: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached token data stored on disk
|
||||||
|
*/
|
||||||
|
export interface CachedTokenData {
|
||||||
|
accessToken: string;
|
||||||
|
expiresAtEpoch: number;
|
||||||
|
createdAtEpoch: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP method used by requestApi
|
||||||
|
*/
|
||||||
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP API response
|
||||||
|
*/
|
||||||
|
export interface ApiResponse {
|
||||||
|
status: number;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue