Extensions

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.

Extension Overview

Extensions in the Asterisms JS SDK can:

  • Add new resources with custom APIs
  • Extend existing resources with additional methods
  • Integrate third-party services (analytics, monitoring, etc.)
  • Customize authentication providers for specialized flows
  • Add middleware for request/response processing
  • Create custom storage adapters for different backends

Creating Custom Resources

Basic Resource Extension

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);
}

Register Custom Resource

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();

Custom Authentication Providers

OAuth Provider Example

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);

SAML Provider Example

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

Storage Adapters

Custom Storage Adapter

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'
  })
);

Middleware Extensions

Request/Response Middleware

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());

Analytics Middleware

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));

Plugin System

Plugin Interface

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);

Theme Plugin

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');

Testing Extensions

Extension Test Framework

// 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);
  });
});

Publishing Extensions

Package Structure

my-asterisms-extension/ ├── package.json ├── README.md ├── src/ │ ├── index.ts │ ├── CustomResource.ts │ └── types.ts ├── dist/ ├── tests/ └── docs/

Package.json

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

Export Extension

// 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';

Extension Registry

Community Extensions

Popular community extensions include:

  • @asterisms/extension-analytics: Google Analytics, Mixpanel integration
  • @asterisms/extension-storage-s3: AWS S3 storage adapter
  • @asterisms/extension-auth-oauth: OAuth 2.0 provider
  • @asterisms/extension-notifications: Enhanced notification features
  • @asterisms/extension-themes: Theme management system

Publishing Guidelines

  1. Follow naming convention: @scope/asterisms-extension-name
  2. Include comprehensive tests: Unit and integration tests
  3. Provide documentation: README with usage examples
  4. Version compatibility: Specify supported SDK versions
  5. TypeScript support: Include type definitions

Next Steps