astro-tokenkit
v1.0.17
Published
A powerful API client for Astro with automatic token rotation, session management, and seamless context integration.
Maintainers
Readme
Astro TokenKit
A powerful, type-safe API client for Astro with automatic token rotation, session management, and seamless context integration.
Features
- 🚀 Built for Astro: Deep integration with Astro's middleware and context.
- 🔄 Automatic Token Rotation: Handles access and refresh tokens automatically behind the scenes.
- 🔒 Secure by Default: Uses HttpOnly cookies for token storage.
- 🧩 Flexible Context: Supports both internal
AsyncLocalStorageand external context management. - 🛠 Type-Safe: Built with TypeScript for a first-class developer experience.
- 📡 Powerful Interceptors: Easily add custom logic for requests, responses, and errors.
Installation
pnpm add astro-tokenkitQuick Start
1. Add the Integration
Configure TokenKit in your astro.config.mjs. This sets the global configuration for the entire app.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import { tokenKit } from 'astro-tokenkit';
export default defineConfig({
integrations: [
tokenKit({
baseURL: 'https://api.yourserver.com',
auth: {
login: '/auth/login',
refresh: '/auth/refresh',
}
})
],
});2. Setup Middleware
Create src/middleware.ts to automatically handle context binding and token rotation. You can use the exported api singleton's middleware:
// src/middleware.ts
import { api } from 'astro-tokenkit';
export const onRequest = api.middleware();3. Use in Pages
Now you can use the api client anywhere in your Astro pages or components without worrying about passing context.
---
// src/pages/profile.astro
import { api } from 'astro-tokenkit';
// Request methods return an APIResponse object
const { data: user } = await api.get('/me');
---
<h1>Welcome, {user.name}</h1>Global Configuration
TokenKit supports a global configuration via the tokenKit integration or setConfig. All ClientConfig properties can be set globally.
import { setConfig } from 'astro-tokenkit';
setConfig({
baseURL: 'https://api.example.com',
auth: {
login: '/auth/login',
refresh: '/auth/refresh',
}
});API Singleton
The library exports a global api instance that is automatically synchronized with your configuration.
- Dynamic Sync: If you update the configuration via
setConfig(), theapiinstance immediately reflects these changes. - Shared Manager: The
apiinstance uses a globalTokenManagerwhich ensures that token refreshes are synchronized across all requests (preventing race conditions). - Middleware Integration: Use
api.middleware()for a seamless setup in Astro.
If you need a specialized client with a different configuration, you can still create one:
import { createClient } from 'astro-tokenkit';
const specializedClient = createClient({
baseURL: 'https://another-api.com'
});Configuration
Client Configuration
| Property | Type | Description |
| :--- | :--- | :--- |
| baseURL | string | Required. Base URL for all requests. |
| auth | AuthConfig | Optional authentication configuration. |
| headers | Record<string, string> | Default headers for all requests. |
| timeout | number | Request timeout in milliseconds (default: 30000). |
| retry | RetryConfig | Retry strategy for failed requests. |
| interceptors| InterceptorsConfig | Request/Response/Error interceptors. |
| context | AsyncLocalStorage | External AsyncLocalStorage instance. |
| getContextStore| () => TokenKitContext| Custom method to retrieve the context store. |
| setContextStore| (ctx) => void| Custom method to set the context store. |
| runWithContext| Function| Custom runner to bind context. |
Auth Configuration
| Property | Type | Description |
| :--- | :--- | :--- |
| login | string | Endpoint path for login (POST). |
| refresh | string | Endpoint path for token refresh (POST). |
| logout | string | Endpoint path for logout (POST). |
| contentType | 'application/json' \| 'application/x-www-form-urlencoded' | Content type for auth requests (default: application/json). |
| headers | Record<string, string> | Extra headers for login/refresh requests. |
| loginData | Record<string, any> | Extra data to be sent with login request. |
| refreshData | Record<string, any> | Extra data to be sent with refresh request. |
| refreshRequestField | string | Field name for the refresh token in the refresh request (default: refreshToken). |
| fields | FieldMapping | Custom mapping for token fields in API responses (accessToken, refreshToken, expiresAt, expiresIn, tokenType, sessionPayload). |
| parseLogin | Function | Custom parser for login response: (body: any) => TokenBundle. |
| parseRefresh| Function | Custom parser for refresh response: (body: any) => TokenBundle. |
| injectToken | Function | Custom token injection: (token: string, type?: string) => string (default: Bearer). |
| cookies | CookieConfig | Configuration for auth cookies. |
| policy | RefreshPolicy | Strategy for when to trigger token refresh. |
Login Options
| Property | Type | Description |
| :--- | :--- | :--- |
| onLogin | Function | Callback after successful login: (bundle, body, ctx) => void. |
| onError | Function | Callback after failed login: (error, ctx) => void. |
| headers | Record<string, string> | Extra headers for this specific login request. |
| data | Record<string, any> | Extra data for this specific login request. |
Request Auth Overrides
When calling api.get(), api.post(), etc., you can override auth configuration (e.g., for multi-tenancy). Headers provided in the request options are automatically propagated to any automatic token refresh operations:
await api.get('/data', {
headers: { 'x-tenant-name': 'lynx' },
auth: {
data: { extra_refresh_param: 'value' }
}
});Advanced Usage
Manual Context
If you prefer not to use middleware, you can bind the Astro context manually for a specific scope:
import { runWithContext } from 'astro-tokenkit';
const { data } = await runWithContext(Astro, () => api.get('/data'));Interceptors
const api = createClient({
baseURL: '...',
interceptors: {
request: [
(config, ctx) => {
config.headers = { ...config.headers, 'X-Custom': 'Value' };
return config;
}
]
}
});Login and Logout
// In an API route or server-side component
const { data: bundle } = await api.login({ username, password }, {
onLogin: (bundle, body, ctx) => {
// Post-login logic (e.g., sync session to another store)
console.log('User logged in!', bundle.sessionPayload);
},
onError: (error, ctx) => {
// Handle error (e.g., log it or perform cleanup)
console.error('Login failed:', error.message);
}
});
await api.logout();Using Promises (.then, .catch, .finally)
All API methods return a Promise that resolves to an APIResponse object. You can use traditional promise chaining:
// Example with GET request
api.get('/me')
.then(({ data: user, status }) => {
console.log(`User ${user.name} fetched with status ${status}`);
})
.catch(err => {
console.error('Failed to fetch user:', err.message);
})
.finally(() => {
console.log('Request finished');
});
// Example with login
api.login(credentials)
.then(({ data: token }) => {
console.log('Successfully logged in!', token.accessToken);
})
.catch(err => {
if (err instanceof AuthError) {
console.error('Authentication failed:', err.message);
} else {
console.error('An unexpected error occurred:', err.message);
}
})
.finally(() => {
// E.g. stop loading state
});Note: Since all methods return an
APIResponseobject, you can use destructuring in.then()to access the data directly, which allows for clean syntax like.then(({ data: token }) => ... ).
License
MIT © oamm
