Skip to main content

Adapter Events and Extensions

This document provides a comprehensive guide to the adapter events and extensions system in the secure_project plugin architecture. These advanced features enable highly extensible adapter plugins that can emit and handle events, as well as be extended with additional functionality.

Table of Contentsโ€‹

Event Systemโ€‹

The event system allows adapters to emit events that can be handled by registered handlers. This enables loose coupling between adapters and other components, as well as providing a way to extend adapter functionality without modifying the adapter code.

Event Typesโ€‹

Events are identified by a string type and can contain arbitrary data. Some common event types include:

  • request: Emitted when a request is received by an adapter
  • response: Emitted when a response is sent by an adapter
  • error: Emitted when an error occurs in an adapter
  • initialize: Emitted when an adapter is initialized
  • middleware.before: Emitted before middleware is executed
  • middleware.after: Emitted after middleware is executed

Custom event types can be defined by adapters as needed.

Event Handlersโ€‹

Event handlers are registered with the AdapterEventManager and are called when a matching event is emitted. Handlers can be registered to handle specific event types from specific adapters, or to handle all events of a certain type.

Handler Interfaceโ€‹

interface AdapterEventHandler {
/**
* Unique identifier for the handler
*/
id: string;

/**
* Event types to handle (can include '*' to handle all events)
*/
events: string[];

/**
* Optional adapter ID to filter events (only handle events from this adapter)
*/
adapterId?: string;

/**
* Handler priority (higher numbers run first)
*/
priority: number;

/**
* Event handler function
*/
handler: (event: AdapterEvent) => Promise<void> | void;
}

Event Middlewareโ€‹

Event middleware provides a way to intercept and modify events before they reach handlers. Middleware can be used for logging, filtering, transforming, or enriching events.

Middleware Interfaceโ€‹

interface AdapterEventMiddleware {
/**
* Unique identifier for the middleware
*/
id: string;

/**
* Event types to handle (can include '*' to handle all events)
*/
events: string[];

/**
* Optional adapter ID to filter events (only handle events from this adapter)
*/
adapterId?: string;

/**
* Middleware priority (higher numbers run first)
*/
priority: number;

/**
* Event middleware function
*/
middleware: (event: AdapterEvent, next: () => Promise<void>) => Promise<void> | void;
}

Middleware uses a "pipeline" pattern, where each middleware function can decide whether to continue the pipeline by calling next() or to short-circuit it by not calling next().

Emitting Eventsโ€‹

Adapters can emit events using the AdapterManager.emitEvent() method, which is available in the context provided to adapters.

await adapterManager.emitEvent('my-adapter', 'request', {
req, // Request object
timestamp: Date.now(),
// Additional data...
});

Event Examplesโ€‹

Registering an Event Handlerโ€‹

// Create event manager
const eventManager = new AdapterEventManager();

// Register a handler for all request events
eventManager.registerHandler({
id: 'request-logger',
events: ['request'],
priority: 10,
handler: async (event) => {
console.log(`Request received by adapter [event.adapterId]:`, event.data.req.url);
}
});

// Register a handler for error events from a specific adapter
eventManager.registerHandler({
id: 'express-error-handler',
events: ['error'],
adapterId: 'express',
priority: 20,
handler: async (event) => {
console.error(`Error in Express adapter:`, event.data.error);
}
});

Adding Event Middlewareโ€‹

// Register middleware to enrich request events with user info
eventManager.registerMiddleware({
id: 'user-enricher',
events: ['request'],
priority: 100, // Run before other middleware
middleware: async (event, next) => {
if (event.data.req.headers.authorization) {
// Extract user from token
const token = event.data.req.headers.authorization.split(' ')[1];
const user = await getUserFromToken(token);

// Enrich event with user data
event.data.user = user;
}

// Continue middleware pipeline
await next();
}
});

// Register middleware to filter sensitive data
eventManager.registerMiddleware({
id: 'data-filter',
events: ['*'], // Apply to all events
priority: 50,
middleware: async (event, next) => {
// Filter sensitive data from event
if (event.data.req && event.data.req.body) {
event.data.req.body = filterSensitiveData(event.data.req.body);
}

// Continue middleware pipeline
await next();
}
});

Extension Systemโ€‹

The extension system allows adapters to be extended with additional functionality through plugins. Extensions can modify adapter behavior, add new features, or customize existing functionality.

Extension Pointsโ€‹

Extension points are identified by a string name and represent a specific place in the adapter's code where extensions can be applied. Some common extension points include:

  • middleware: The middleware created by the adapter
  • initialize: The adapter initialization process
  • request: The request handling process
  • response: The response handling process
  • error: The error handling process
  • config: The adapter configuration

Custom extension points can be defined by adapters as needed.

Creating Extensionsโ€‹

Extensions are registered with the AdapterExtensionManager and are applied to adapters at runtime.

Extension Interfaceโ€‹

interface AdapterExtension {
/**
* Unique identifier for the extension
*/
id: string;

/**
* Display name of the extension
*/
name: string;

/**
* Version of the extension
*/
version: string;

/**
* Description of the extension
*/
description: string;

/**
* Priority of the extension (higher numbers are applied first)
*/
priority?: number;

/**
* Target adapters to apply the extension to (can include '*' to apply to all adapters)
*/
targetAdapters: string[];

/**
* Extension point to apply the extension to
*/
extensionPoint: string;

/**
* Apply the extension to an adapter target
* @param adapter Adapter to apply the extension to
* @param target Target to apply the extension to (e.g., middleware)
* @param options Options for the extension
* @returns Enhanced target
*/
apply(adapter: AdapterPlugin, target: any, options: any): any;
}

Applying Extensionsโ€‹

Extensions are automatically applied by the AdapterManager when creating middleware or when explicitly calling AdapterManager.applyExtensions().

// Apply extensions to a target
const enhancedTarget = adapterManager.applyExtensions(
'express', // Adapter ID
'middleware', // Extension point
originalTarget, // Target to enhance
[/* options */] // Options for extensions
);

Extension Examplesโ€‹

Creating a Logging Extensionโ€‹

// Create an extension to add logging to Express middleware
const loggingExtension: AdapterExtension = {
id: 'express-logger',
name: 'Express Logger',
version: '1.0.0',
description: 'Adds request logging to Express middleware',
priority: 100, // High priority, apply first
targetAdapters: ['express'],
extensionPoint: 'middleware',

apply(adapter, middleware, options) {
// Return a wrapped middleware function
return (req, res, next) => {
// Log request
console.log(`[[new Date().toISOString()]] [req.method] [req.url]`);

// Track response time
const start = Date.now();

// Override res.end to log response
const originalEnd = res.end;
res.end = function(...args) {
// Calculate response time
const duration = Date.now() - start;

// Log response
console.log(`[[new Date().toISOString()]] [req.method] [req.url] - [res.statusCode] ([duration]ms)`);

// Call original end
return originalEnd.apply(this, args);
};

// Call original middleware
return middleware(req, res, next);
};
}
};

Creating a Rate Limiting Extensionโ€‹

// Create an extension to add rate limiting to any adapter
const rateLimitingExtension: AdapterExtension = {
id: 'rate-limiter',
name: 'Rate Limiter',
version: '1.0.0',
description: 'Adds rate limiting to adapter middleware',
priority: 90, // Apply after logging but before other extensions
targetAdapters: ['*'], // Apply to all adapters
extensionPoint: 'middleware',

apply(adapter, middleware, options) {
// Initialize rate limiter based on adapter type
const limiter = createRateLimiter(adapter.framework);

// Return a wrapper based on adapter framework
if (adapter.framework === 'express') {
// Express-style middleware
return (req, res, next) => {
// Check rate limit
if (limiter.check(req.ip)) {
// Call original middleware
return middleware(req, res, next);
} else {
// Rate limit exceeded
res.status(429).json({ error: 'Too many requests' });
}
};
} else if (adapter.framework === 'koa') {
// Koa-style middleware
return async (ctx, next) => {
// Check rate limit
if (limiter.check(ctx.ip)) {
// Call original middleware
await middleware(ctx, next);
} else {
// Rate limit exceeded
ctx.status = 429;
ctx.body = { error: 'Too many requests' };
}
};
}

// Default: return original middleware if we don't know how to wrap it
return middleware;
}
};

Advanced Usageโ€‹

Combining Events and Extensionsโ€‹

Events and extensions can be combined to create powerful, flexible adapter plugins. For example, an extension can emit events, or event handlers can modify adapter behavior using extensions.

Extension That Emits Eventsโ€‹

// Create an extension that emits events
const eventEmittingExtension: AdapterExtension = {
id: 'event-emitter',
name: 'Event Emitter',
version: '1.0.0',
description: 'Emits events from middleware',
targetAdapters: ['*'],
extensionPoint: 'middleware',

apply(adapter, middleware, options) {
// Get event manager from options
const { eventManager } = options.context || {};

if (!eventManager) {
// No event manager, return original middleware
return middleware;
}

// Return a wrapper based on adapter framework
if (adapter.framework === 'express') {
// Express-style middleware
return (req, res, next) => {
// Emit request event
eventManager.emit({
type: 'custom.request',
adapterId: adapter.id,
timestamp: Date.now(),
data: { req, res }
});

// Call original middleware
return middleware(req, res, next);
};
}

// Default: return original middleware if we don't know how to wrap it
return middleware;
}
};

Best Practicesโ€‹

When working with adapter events and extensions, follow these best practices:

  1. Use Unique IDs: Always use unique, descriptive IDs for handlers, middleware, and extensions.

  2. Prioritize Correctly: Set appropriate priorities to ensure handlers, middleware, and extensions are applied in the correct order.

  3. Handle Errors: Always catch and handle errors in handlers, middleware, and extensions to prevent crashes.

  4. Document Events and Extension Points: Clearly document the events and extension points your adapter provides.

  5. Be Framework-Agnostic: When possible, design extensions to work with multiple frameworks by checking the adapter.framework property.

  6. Use Typescript: Define strong types for your events and extension points to catch errors early.

  7. Test Thoroughly: Write comprehensive tests for your events and extensions to ensure they work correctly.

  8. Performance Considerations: Be mindful of performance when creating complex middleware pipelines or extension chains.

  9. Security Considerations: Validate and sanitize all input data in event handlers and extensions.

  10. Backward Compatibility: Maintain backward compatibility when updating events or extension points.

// Example of a well-documented, framework-agnostic extension
const securityExtension: AdapterExtension = {
id: 'security-headers',
name: 'Security Headers',
version: '1.0.0',
description: 'Adds security headers to responses',
priority: 200, // Very high priority
targetAdapters: ['*'],
extensionPoint: 'middleware',

apply(adapter, middleware, options) {
// Security headers to add
const securityHeaders = {
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Content-Security-Policy': "default-src 'self'",
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'no-referrer'
};

// Apply based on framework
switch (adapter.framework) {
case 'express':
return (req, res, next) => {
// Add security headers
Object.entries(securityHeaders).forEach(([header, value]) => {
res.setHeader(header, value);
});

// Call original middleware
return middleware(req, res, next);
};

case 'koa':
return async (ctx, next) => {
// Add security headers
Object.entries(securityHeaders).forEach(([header, value]) => {
ctx.set(header, value);
});

// Call original middleware
await middleware(ctx, next);
};

default:
// Log warning for unknown framework
console.warn(`Security headers extension does not support [adapter.framework] framework`);
return middleware;
}
}
};

By following these guidelines and examples, you can create a robust and flexible adapter plugin system that can be easily extended with new functionality.