The Asterisms JS SDK provides a flexible extension system that allows you to build custom functionality, integrate with third-party services, and extend the core capabilities of the SDK.
Extensions in the Asterisms JS SDK can:
import { BaseResource, ResourceInterface } from '@asterisms/sdk';
interface CustomResourceInterface extends ResourceInterface {
customMethod(): Promise<any>;
batchOperation(items: any[]): Promise<any[]>;
}
class CustomResource extends BaseResource implements CustomResourceInterface {
constructor(config: any) {
super(config);
}
async customMethod(): Promise<any> {
return await this.request({
method: 'GET',
url: '/custom/endpoint',
headers: this.getAuthHeaders()
});
}
async batchOperation(items: any[]): Promise<any[]> {
return await this.request({
method: 'POST',
url: '/custom/batch',
data: { items },
headers: this.getAuthHeaders()
});
}
// Override base resource methods if needed
protected getBaseUrl(): string {
return 'https://api.yourservice.com';
}
}
// Factory function
export function createCustomResource(config: any): CustomResourceInterface {
return new CustomResource(config);
}import { createAsterismsSDK } from '@asterisms/sdk';
import { createCustomResource } from './extensions/CustomResource';
const sdk = createAsterismsSDK({
bundleId: 'com.yourcompany.yourapp',
rootDomain: 'yourapp.com',
navigationAdapter: yourNavigationAdapter
});
// Register custom resource
sdk.registerResource(
'custom',
createCustomResource({
baseUrl: 'https://api.yourservice.com',
apiKey: 'your-api-key'
})
);
// Use custom resource
const result = await sdk.custom.customMethod();import { AuthProviderInterface, AuthResult } from '@asterisms/sdk';
interface OAuthConfig {
clientId: string;
clientSecret: string;
redirectUri: string;
authUrl: string;
tokenUrl: string;
scopes: string[];
}
class OAuthProvider implements AuthProviderInterface {
constructor(private config: OAuthConfig) {}
async authenticate(credentials: any): Promise<AuthResult> {
if (credentials.code) {
// Exchange authorization code for tokens
return await this.exchangeCodeForTokens(credentials.code);
} else {
// Initiate OAuth flow
return await this.initiateOAuthFlow();
}
}
async refresh(refreshToken: string): Promise<AuthResult> {
const response = await fetch(this.config.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const data = await response.json();
return {
token: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
user: await this.getUserInfo(data.access_token)
};
}
async logout(): Promise<void> {
// Revoke tokens if supported by provider
// Clear local state
}
private async initiateOAuthFlow(): Promise<AuthResult> {
const state = this.generateState();
const authUrl = new URL(this.config.authUrl);
authUrl.searchParams.set('client_id', this.config.clientId);
authUrl.searchParams.set('redirect_uri', this.config.redirectUri);
authUrl.searchParams.set('scope', this.config.scopes.join(' '));
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('response_type', 'code');
// Redirect to OAuth provider
window.location.href = authUrl.toString();
// This will be completed in the redirect callback
throw new Error('OAuth flow initiated');
}
private async exchangeCodeForTokens(code: string): Promise<AuthResult> {
const response = await fetch(this.config.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: this.config.redirectUri,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const data = await response.json();
return {
token: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
user: await this.getUserInfo(data.access_token)
};
}
private async getUserInfo(accessToken: string): Promise<any> {
// Fetch user information from OAuth provider
const response = await fetch('https://api.provider.com/user', {
headers: { Authorization: `Bearer ${accessToken}` }
});
return await response.json();
}
private generateState(): string {
return (
Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
);
}
}
// Usage
const oauthProvider = new OAuthProvider({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'https://yourapp.com/auth/callback',
authUrl: 'https://provider.com/oauth/authorize',
tokenUrl: 'https://provider.com/oauth/token',
scopes: ['read', 'write']
});
sdk.auth.registerProvider('oauth', oauthProvider);import { AuthProviderInterface, AuthResult } from '@asterisms/sdk';
interface SAMLConfig {
entityId: string;
ssoUrl: string;
certificate: string;
signRequests: boolean;
}
class SAMLProvider implements AuthProviderInterface {
constructor(private config: SAMLConfig) {}
async authenticate(credentials: any): Promise<AuthResult> {
if (credentials.samlResponse) {
return await this.processSAMLResponse(credentials.samlResponse);
} else {
return await this.initiateSAMLFlow();
}
}
private async initiateSAMLFlow(): Promise<AuthResult> {
const samlRequest = this.createSAMLRequest();
const encodedRequest = btoa(samlRequest);
const ssoUrl = new URL(this.config.ssoUrl);
ssoUrl.searchParams.set('SAMLRequest', encodedRequest);
window.location.href = ssoUrl.toString();
throw new Error('SAML flow initiated');
}
private async processSAMLResponse(samlResponse: string): Promise<AuthResult> {
// Validate and parse SAML response
const decodedResponse = atob(samlResponse);
const userInfo = this.parseSAMLResponse(decodedResponse);
return {
token: this.generateJWT(userInfo),
user: userInfo,
expiresIn: 3600 // 1 hour
};
}
private createSAMLRequest(): string {
// Generate SAML AuthnRequest XML
return `<?xml version="1.0" encoding="UTF-8"?>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="${this.generateID()}"
Version="2.0"
IssueInstant="${new Date().toISOString()}"
Destination="${this.config.ssoUrl}">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
${this.config.entityId}
</saml:Issuer>
</samlp:AuthnRequest>`;
}
private parseSAMLResponse(response: string): any {
// Parse SAML response and extract user attributes
// This is a simplified example - use proper SAML library
const parser = new DOMParser();
const doc = parser.parseFromString(response, 'text/xml');
return {
id: doc.querySelector('Attribute[Name="NameID"]')?.textContent,
email: doc.querySelector('Attribute[Name="Email"]')?.textContent,
name: doc.querySelector('Attribute[Name="DisplayName"]')?.textContent
};
}
private generateID(): string {
return '_' + Math.random().toString(36).substr(2, 9);
}
private generateJWT(userInfo: any): string {
// Generate JWT token (use proper JWT library)
const header = { alg: 'HS256', typ: 'JWT' };
const payload = { ...userInfo, iat: Date.now() / 1000 };
return btoa(JSON.stringify(header)) + '.' + btoa(JSON.stringify(payload)) + '.' + 'signature'; // Use proper signing
}
async refresh(refreshToken: string): Promise<AuthResult> {
throw new Error('SAML does not support token refresh');
}
async logout(): Promise<void> {
// Initiate SAML logout flow if supported
}
}import { StorageAdapterInterface } from '@asterisms/sdk';
interface S3Config {
bucket: string;
region: string;
accessKeyId: string;
secretAccessKey: string;
}
class S3StorageAdapter implements StorageAdapterInterface {
constructor(private config: S3Config) {}
async upload(file: File, options: any): Promise<any> {
const formData = new FormData();
formData.append('file', file);
formData.append('bucket', this.config.bucket);
const response = await fetch(
`https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/`,
{
method: 'POST',
body: formData,
headers: this.getAuthHeaders()
}
);
if (!response.ok) {
throw new Error('Upload failed');
}
return await response.json();
}
async download(fileId: string): Promise<Blob> {
const response = await fetch(
`https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${fileId}`,
{
headers: this.getAuthHeaders()
}
);
return await response.blob();
}
async delete(fileId: string): Promise<void> {
await fetch(`https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${fileId}`, {
method: 'DELETE',
headers: this.getAuthHeaders()
});
}
async list(options: any): Promise<any[]> {
const response = await fetch(
`https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/?list-type=2`,
{
headers: this.getAuthHeaders()
}
);
const data = await response.text();
return this.parseS3ListResponse(data);
}
private getAuthHeaders(): Record<string, string> {
// Implement AWS Signature Version 4
return {
Authorization: this.generateAWSSignature(),
'x-amz-date': new Date().toISOString()
};
}
private generateAWSSignature(): string {
// Implement AWS signature generation
return 'AWS4-HMAC-SHA256 ...';
}
private parseS3ListResponse(xml: string): any[] {
// Parse S3 XML response
const parser = new DOMParser();
const doc = parser.parseFromString(xml, 'text/xml');
const contents = doc.querySelectorAll('Contents');
return Array.from(contents).map((content) => ({
key: content.querySelector('Key')?.textContent,
size: parseInt(content.querySelector('Size')?.textContent || '0'),
lastModified: content.querySelector('LastModified')?.textContent
}));
}
}
// Register custom storage adapter
sdk.storage.registerAdapter(
's3',
new S3StorageAdapter({
bucket: 'your-bucket',
region: 'us-east-1',
accessKeyId: 'your-access-key',
secretAccessKey: 'your-secret-key'
})
);import { MiddlewareInterface, RequestConfig, ResponseData } from '@asterisms/sdk';
class LoggingMiddleware implements MiddlewareInterface {
async onRequest(config: RequestConfig): Promise<RequestConfig> {
console.log(`Request: ${config.method} ${config.url}`);
console.log('Headers:', config.headers);
// Add request ID for tracking
config.headers = {
...config.headers,
'X-Request-ID': this.generateRequestId()
};
return config;
}
async onResponse(response: ResponseData): Promise<ResponseData> {
console.log(`Response: ${response.status} ${response.statusText}`);
console.log('Duration:', response.duration);
return response;
}
async onError(error: any): Promise<any> {
console.error('Request failed:', error);
// Custom error handling
if (error.status === 429) {
// Implement retry logic
return this.retryRequest(error.config);
}
throw error;
}
private generateRequestId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
private async retryRequest(config: RequestConfig): Promise<any> {
// Implement exponential backoff
await this.delay(1000);
return fetch(config.url, config);
}
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Register middleware
sdk.use(new LoggingMiddleware());class AnalyticsMiddleware implements MiddlewareInterface {
constructor(private analyticsService: any) {}
async onRequest(config: RequestConfig): Promise<RequestConfig> {
// Track API usage
this.analyticsService.track('api_request', {
method: config.method,
endpoint: config.url,
timestamp: new Date()
});
return config;
}
async onResponse(response: ResponseData): Promise<ResponseData> {
// Track response metrics
this.analyticsService.track('api_response', {
status: response.status,
duration: response.duration,
size: response.data ? JSON.stringify(response.data).length : 0
});
return response;
}
async onError(error: any): Promise<any> {
// Track errors
this.analyticsService.track('api_error', {
status: error.status,
message: error.message,
endpoint: error.config?.url
});
throw error;
}
}
// Usage with external analytics service
import { Analytics } from 'analytics';
const analytics = Analytics({
app: 'my-app',
plugins: [
// Your analytics plugins
]
});
sdk.use(new AnalyticsMiddleware(analytics));interface PluginInterface {
name: string;
version: string;
install(sdk: any): void;
uninstall?(sdk: any): void;
}
class NotificationPlugin implements PluginInterface {
name = 'notification-plugin';
version = '1.0.0';
install(sdk: any): void {
// Extend notification resource
const originalNotification = sdk.notification;
sdk.notification = {
...originalNotification,
// Add push notification support
async subscribeToPush(): Promise<void> {
if ('serviceWorker' in navigator && 'PushManager' in window) {
const registration = await navigator.serviceWorker.register('/sw.js');
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.getVapidKey()
});
await originalNotification.registerPushSubscription(subscription);
}
},
// Add notification scheduling
async scheduleNotification(notification: any, delay: number): Promise<void> {
setTimeout(() => {
this.showNotification(notification);
}, delay);
}
};
}
private getVapidKey(): string {
return 'your-vapid-public-key';
}
private showNotification(notification: any): void {
if (Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.message,
icon: notification.icon
});
}
}
}
// Install plugin
const notificationPlugin = new NotificationPlugin();
sdk.use(notificationPlugin);class ThemePlugin implements PluginInterface {
name = 'theme-plugin';
version = '1.0.0';
private themes = {
light: {
primary: '#007cba',
secondary: '#6c757d',
background: '#ffffff',
text: '#333333'
},
dark: {
primary: '#4dabf7',
secondary: '#adb5bd',
background: '#1a1a1a',
text: '#ffffff'
}
};
install(sdk: any): void {
sdk.theme = {
setTheme: (themeName: string) => {
const theme = this.themes[themeName];
if (theme) {
this.applyTheme(theme);
localStorage.setItem('asterisms-theme', themeName);
}
},
getTheme: () => {
return localStorage.getItem('asterisms-theme') || 'light';
},
registerTheme: (name: string, theme: any) => {
this.themes[name] = theme;
}
};
// Apply saved theme
const savedTheme = sdk.theme.getTheme();
sdk.theme.setTheme(savedTheme);
}
private applyTheme(theme: any): void {
const root = document.documentElement;
Object.entries(theme).forEach(([key, value]) => {
root.style.setProperty(`--asterisms-${key}`, value as string);
});
}
}
// Usage
sdk.use(new ThemePlugin());
sdk.theme.setTheme('dark');// tests/extensions/custom-resource.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createTestSDK } from '../helpers/test-utils';
import { CustomResource } from '../../src/extensions/CustomResource';
describe('CustomResource Extension', () => {
let sdk: any;
let customResource: CustomResource;
beforeEach(() => {
sdk = createTestSDK();
customResource = new CustomResource({
baseUrl: 'https://api.test.com',
apiKey: 'test-key'
});
sdk.registerResource('custom', customResource);
});
it('should register custom resource', () => {
expect(sdk.custom).toBeDefined();
expect(typeof sdk.custom.customMethod).toBe('function');
});
it('should call custom method', async () => {
const mockResponse = { data: 'test' };
vi.spyOn(customResource, 'request').mockResolvedValue(mockResponse);
const result = await sdk.custom.customMethod();
expect(customResource.request).toHaveBeenCalledWith({
method: 'GET',
url: '/custom/endpoint',
headers: expect.any(Object)
});
expect(result).toEqual(mockResponse);
});
it('should handle batch operations', async () => {
const items = [{ id: 1 }, { id: 2 }];
const mockResponse = { results: items };
vi.spyOn(customResource, 'request').mockResolvedValue(mockResponse);
const result = await sdk.custom.batchOperation(items);
expect(customResource.request).toHaveBeenCalledWith({
method: 'POST',
url: '/custom/batch',
data: { items },
headers: expect.any(Object)
});
expect(result).toEqual(mockResponse);
});
});my-asterisms-extension/
├── package.json
├── README.md
├── src/
│ ├── index.ts
│ ├── CustomResource.ts
│ └── types.ts
├── dist/
├── tests/
└── docs/
{
"name": "@yourcompany/asterisms-extension-name",
"version": "1.0.0",
"description": "Custom extension for Asterisms JS SDK",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": ["asterisms", "sdk", "extension"],
"peerDependencies": {
"@asterisms/sdk": "^4.0.0"
},
"devDependencies": {
"@asterisms/sdk": "^4.0.0",
"typescript": "^5.0.0"
}
}// src/index.ts
export { CustomResource } from './CustomResource';
export { OAuthProvider } from './OAuthProvider';
export { S3StorageAdapter } from './S3StorageAdapter';
export * from './types';
// Re-export SDK types for convenience
export type {
ResourceInterface,
AuthProviderInterface,
StorageAdapterInterface
} from '@asterisms/sdk';Popular community extensions include:
@scope/asterisms-extension-name