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 pluginshasCapability()- 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
| Level | Number | Use Case |
|---|---|---|
| trace | 10 | Very verbose (cache hits/misses) |
| debug | 20 | Normal operations (requests, storage) |
| info | 30 | Important actions (manual flush, config) |
| warn | 40 | Degraded functionality (retries, fallbacks) |
| error | 50 | Operation failures |
| fatal | 60 | Critical 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 lateIf 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:
- Read the Plugin Guide for plugin development basics
- Explore the logging plugin README
- See Phase 3 spec:
specs/phase-3-plugin-extensibility/