Skip to Content
SDK Kit is in active development. APIs may change.
Plugin Extensibility

Plugin Extensibility

Learn how to create plugins that provide capabilities to other plugins using hold() and hasCapability().

Table of Contents


Introduction

Phase 3 introduces plugin extensibility - the ability for plugins to provide capabilities that other plugins can use. This enables powerful patterns like logging, metrics, and validation across all plugins.

Key Features

  • hold() - Provide capabilities to other plugins
  • hasCapability() - Check if a capability is available
  • Type-safe - Module augmentation for TypeScript safety
  • Optional - Plugins work with or without held capabilities

All held capabilities use optional chaining (plugin.log?.()) for graceful degradation.


Capability System

Providing Capabilities with hold()

Plugins can provide capabilities to other plugins using hold():

import type { PluginFunction } from '@prosdevlab/sdk-kit'; export const loggingPlugin: PluginFunction = (plugin, instance, config) => { plugin.ns('logging'); // Provide 'log' capability to other plugins plugin.hold({ log: (level, message, data) => { // Logging implementation console.log(`[${level}] ${message}`, data); } }); // Expose public API plugin.expose({ onLog: (callback) => { /* ... */ } }); };

Consuming Capabilities

Other plugins can use held capabilities via optional chaining:

export const storagePlugin: PluginFunction = (plugin) => { plugin.ns('storage'); plugin.expose({ set: (key, value) => { // Log operation (if logging plugin loaded) plugin.log?.('debug', 'Setting value', { key, backend: 'localStorage' }); localStorage.setItem(key, JSON.stringify(value)); } }); };

Always use optional chaining (plugin.log?.()) when consuming held capabilities. This ensures your plugin works even if the capability provider isn’t loaded.

Checking Capability Availability

Use hasCapability() to check if a capability is available:

export const myPlugin: PluginFunction = (plugin) => { plugin.ns('my-plugin'); plugin.expose({ doSomething: () => { if (plugin.hasCapability('log')) { plugin.log('info', 'Capability available'); } else { console.warn('Logging not available'); } } }); };

Type Safety with Module Augmentation

For type-safe held capabilities, use module augmentation:

// In logging plugin types declare module '@prosdevlab/sdk-kit' { interface Plugin { log?: (level: LogLevel, message: string, data?: unknown) => void; } } type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';

Now TypeScript knows about plugin.log and provides autocomplete!


Logging Plugin

The logging plugin is the reference implementation of plugin extensibility.

Features

  • Production-grade - Uses Pino for structured logging (Node.js)
  • Browser support - Falls back to native console
  • Configurable - Log levels, pretty printing, custom formatters
  • Privacy-first - Never logs PII, values, bodies, or auth headers

Usage

Install

Already included in @prosdevlab/sdk-kit-plugins.

Configure

import { SDK } from '@prosdevlab/sdk-kit'; import { loggingPlugin, storagePlugin } from '@prosdevlab/sdk-kit-plugins'; const sdk = new SDK({ logging: { level: 'debug', // trace | debug | info | warn | error | fatal prettyPrint: process.env.NODE_ENV === 'development' } });

Load Plugin

// IMPORTANT: Load logging plugin BEFORE plugins that use it sdk.use(loggingPlugin) .use(storagePlugin) .use(transportPlugin) .use(queuePlugin); await sdk.init();

Listen to Logs

sdk.logging.onLog((entry) => { // Send to external service if (entry.level === 'error' || entry.level === 'fatal') { Sentry.captureException(new Error(entry.message)); } });

Log Levels

LevelNumberUse Case
trace10Very verbose (cache hits/misses)
debug20Normal operations (requests, storage)
info30Important actions (manual flush, config)
warn40Degraded functionality (retries, fallbacks)
error50Operation failures
fatal60Critical system failures

Automatic Logging

These plugins automatically log operations when the logging plugin is loaded:

Storage Plugin:

  • Backend initialization
  • Set/get/remove operations
  • Cache hits/misses
  • Backend fallbacks

Transport Plugin:

  • Request start/complete
  • Retry attempts
  • Request failures
  • Timing metrics

Queue Plugin:

  • Items added
  • Queue full (FIFO eviction)
  • Manual/auto flush
  • Flush duration

Load Order

Plugins that provide capabilities MUST be loaded BEFORE plugins that consume them.

✅ Correct Order

sdk.use(loggingPlugin) // Provider FIRST .use(storagePlugin) // Consumer after .use(transportPlugin);

❌ Incorrect Order

sdk.use(storagePlugin) // Consumer first = no logs .use(loggingPlugin); // Provider too late

If a consumer is loaded before a provider, the held capability will be undefined and calls will be no-ops (thanks to optional chaining).

Checking Load Order

Use hasCapability() to verify:

if (!plugin.hasCapability('log')) { console.warn('Logging plugin not loaded or loaded after this plugin'); }

Privacy & Security

CRITICAL: Never log sensitive data. This is a core principle of SDK Kit.

❌ NEVER LOG

  • User data values - { value: userData }
  • Request/response bodies - { body: request.body }
  • Authentication - passwords, tokens, API keys, auth headers
  • PII - emails, names, phone numbers, credit cards, SSN

✅ ALWAYS LOG

  • Metadata - keys (not values), backend types, methods, URLs
  • Metrics - counts, durations, timestamps, status codes
  • Configuration - TTL values, thresholds, intervals (not secrets)

Example: Storage Plugin

// ✅ GOOD - Logs metadata only plugin.log?.('debug', 'Setting value', { key: 'user:123', // ✅ Key is OK backend: 'localStorage', // ✅ Backend type is OK ttl: 3600 // ✅ TTL is OK }); // NEVER: { value: sensitiveData } ❌ // ✅ GOOD - Logs key, not value plugin.log?.('trace', 'Cache hit', { key: namespacedKey, backend: 'localStorage' });

Example: Transport Plugin

// ✅ GOOD - Logs URL and timing, not body plugin.log?.('debug', 'Sending request', { method: 'POST', // ✅ Method is OK url: 'https://api.example.com/users', // ✅ URL is OK transport: 'fetch' // ✅ Transport type is OK }); // NEVER: { body: request.body } ❌ // NEVER: { headers: request.headers } ❌ plugin.log?.('debug', 'Request completed', { statusCode: 200, // ✅ Status code is OK duration: 150 // ✅ Duration is OK }); // NEVER: { data: response.data } ❌

Privacy Tests

All plugins with logging MUST include privacy tests:

it('should never log sensitive data', async () => { const logs: LogEntry[] = []; sdk.logging.onLog((entry) => logs.push(entry)); // Perform operation with sensitive data sdk.storage.set('credentials', { password: 'secret123', token: 'bearer-xyz', creditCard: '4111111111111111' }); // Verify logs don't contain sensitive data const allLogsStr = JSON.stringify(logs); expect(allLogsStr).not.toContain('secret123'); expect(allLogsStr).not.toContain('bearer-xyz'); expect(allLogsStr).not.toContain('4111111111111111'); // But should contain metadata expect(allLogsStr).toContain('storage'); expect(allLogsStr).toContain('credentials'); // Key is OK });

Examples

Example 1: Metrics Plugin

Create a metrics plugin that other plugins can use:

export const metricsPlugin: PluginFunction = (plugin, instance, config) => { plugin.ns('metrics'); const metrics = new Map<string, number>(); // Provide 'metric' capability plugin.hold({ metric: (name: string, value: number) => { metrics.set(name, (metrics.get(name) || 0) + value); } }); // Expose API plugin.expose({ getMetrics: () => Object.fromEntries(metrics), reset: () => metrics.clear() }); }; // Other plugins use it export const myPlugin: PluginFunction = (plugin) => { plugin.expose({ doSomething: async () => { const startTime = Date.now(); try { const result = await operation(); plugin.metric?.('operation.success', 1); plugin.metric?.('operation.duration', Date.now() - startTime); return result; } catch (error) { plugin.metric?.('operation.failure', 1); throw error; } } }); };

Example 2: Validation Plugin

Create a validation plugin for data quality:

export const validationPlugin: PluginFunction = (plugin) => { plugin.ns('validation'); plugin.hold({ validate: (schema: Schema, data: unknown) => { // Validation logic return { valid: true, errors: [] }; } }); }; // Consumer export const dataPlugin: PluginFunction = (plugin) => { plugin.expose({ saveData: (data: unknown) => { const result = plugin.validate?.(mySchema, data); if (!result?.valid) { plugin.log?.('warn', 'Validation failed', { errors: result?.errors.length }); throw new Error('Invalid data'); } // Save data... } }); };

Best Practices

1. Always Use Optional Chaining

// ✅ GOOD plugin.log?.('info', 'Message'); // ❌ BAD - Throws if logging plugin not loaded plugin.log('info', 'Message');

2. Provide Meaningful Context

// ✅ GOOD - Includes context plugin.log?.('debug', 'Request completed', { statusCode: 200, duration: 150, url: request.url }); // ❌ BAD - No context plugin.log?.('debug', 'Done');

3. Use Appropriate Log Levels

// ✅ GOOD plugin.log?.('trace', 'Cache hit', { key }); // Very verbose plugin.log?.('debug', 'Setting value', { key }); // Normal ops plugin.log?.('info', 'Manual flush', { count }); // Important plugin.log?.('warn', 'Retrying', { attempt }); // Degraded plugin.log?.('error', 'Failed', { error }); // Failures // ❌ BAD - Everything at 'info' plugin.log?.('info', 'Cache hit'); plugin.log?.('info', 'Error occurred');

4. Check Capability Before Expensive Operations

// ✅ GOOD - Avoid expensive work if not needed if (plugin.hasCapability('log')) { const expensiveDebugInfo = computeExpensiveData(); plugin.log('debug', 'Debug info', expensiveDebugInfo); } // ❌ BAD - Always computes even if not logging plugin.log?.('debug', 'Info', computeExpensiveData());

5. Document Load Order Requirements

In your plugin README:

## Load Order This plugin requires the logging plugin to be loaded first: \`\`\`typescript ✅ sdk.use(loggingPlugin).use(myPlugin); ❌ sdk.use(myPlugin).use(loggingPlugin); // No logs \`\`\`

6. Test Without Capability Provider

it('should work without logging plugin', async () => { const sdk = new SDK(); sdk.use(myPlugin); // No logging plugin expect(() => { sdk.myPlugin.doSomething(); }).not.toThrow(); });

Summary

Plugin extensibility enables powerful cross-plugin features while maintaining:

  • Optional dependencies - Plugins work with or without capability providers
  • Type safety - Module augmentation provides autocomplete
  • Privacy - Strict rules prevent logging sensitive data
  • Load order control - Providers before consumers
  • Graceful degradation - Optional chaining ensures no errors

Next Steps:

Last updated on