Creating Plugins
This guide walks you through creating an Authrim plugin from scratch.
Prerequisites
| Requirement | Version |
|---|---|
| Node.js | 18+ |
| TypeScript | 5.0+ |
| Zod | 3.x |
Install the required dependencies:
pnpm add @authrim/ar-lib-plugin zodPlugin Interface
Every Authrim plugin must implement the AuthrimPlugin interface:
interface AuthrimPlugin<TConfig = unknown> { /** Unique plugin ID (e.g., 'notifier-resend') */ readonly id: string;
/** Semantic version (e.g., '1.0.0') */ readonly version: string;
/** Capabilities this plugin provides */ readonly capabilities: PluginCapability[];
/** Whether this is an official Authrim plugin */ readonly official?: boolean;
/** Zod schema for configuration validation */ readonly configSchema: z.ZodType<TConfig, z.ZodTypeDef, unknown>;
/** UI metadata for Admin dashboard */ readonly meta: PluginMeta;
/** Register capabilities with the registry (required) */ register(registry: CapabilityRegistry, config: TConfig): void;
/** Initialize the plugin (optional) */ initialize?(ctx: PluginContext, config: TConfig): Promise<void>;
/** Cleanup on unload (optional) */ shutdown?(): Promise<void>;
/** Health check for monitoring (optional) */ healthCheck?(ctx?: PluginContext, config?: TConfig): Promise<HealthStatus>;}Minimal Plugin Template
Here’s a complete, copy-paste ready template:
import { z } from 'zod';import type { AuthrimPlugin, PluginContext, CapabilityRegistry, HealthStatus,} from '@authrim/ar-lib-plugin';
// 1. Define configuration schema with descriptions for Admin UIconst configSchema = z.object({ apiKey: z.string().min(1).describe('Your API key from the provider dashboard'), endpoint: z.string().url().default('https://api.example.com').describe('API endpoint URL'), timeout: z.number().int().min(1000).max(30000).default(10000).describe('Request timeout (ms)'),});
type MyPluginConfig = z.infer<typeof configSchema>;
// 2. Export the pluginexport const myPlugin: AuthrimPlugin<MyPluginConfig> = { id: 'my-custom-plugin', version: '1.0.0', capabilities: ['notifier.custom'], configSchema,
meta: { name: 'My Custom Plugin', description: 'A custom notification plugin for Example Service', category: 'notification', icon: 'bell', author: { name: 'Your Name', }, license: 'MIT', stability: 'stable', minAuthrimVersion: '1.0.0', },
// 3. Optional: Initialize external connections async initialize(ctx: PluginContext, config: MyPluginConfig): Promise<void> { ctx.logger.info('Initializing plugin', { pluginId: this.id });
// Validate external service connectivity const response = await fetch(`${config.endpoint}/health`, { signal: AbortSignal.timeout(config.timeout), });
if (!response.ok) { throw new Error(`Service unavailable: ${response.status}`); } },
// 4. Required: Register capabilities register(registry: CapabilityRegistry, config: MyPluginConfig): void { registry.registerNotifier('custom', { async send(notification) { const response = await fetch(`${config.endpoint}/send`, { method: 'POST', headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: notification.to, message: notification.body, }), signal: AbortSignal.timeout(config.timeout), });
if (!response.ok) { return { success: false, error: `API error: ${response.status}`, retryable: response.status >= 500, }; }
const result = await response.json(); return { success: true, messageId: result.id, }; }, }); },
// 5. Optional: Health check async healthCheck(ctx, config): Promise<HealthStatus> { if (!config) { return { status: 'unhealthy', message: 'No configuration available' }; }
try { const response = await fetch(`${config.endpoint}/health`, { signal: AbortSignal.timeout(5000), });
if (response.ok) { return { status: 'healthy', timestamp: Date.now() }; }
return { status: 'degraded', message: `API returned ${response.status}`, timestamp: Date.now(), }; } catch (error) { return { status: 'unhealthy', message: error instanceof Error ? error.message : 'Unknown error', timestamp: Date.now(), }; } },
// 6. Optional: Cleanup async shutdown(): Promise<void> { // Close connections, cleanup resources },};Plugin Metadata
The meta field provides information for the Admin UI:
interface PluginMeta { // Required name: string; // Display name description: string; // Short description (1-2 sentences) category: PluginCategory; // 'notification' | 'identity' | 'authentication' | 'flow'
// Author & License (required for community plugins) author?: { name: string; email?: string; url?: string; }; license?: string; // SPDX format (e.g., "MIT", "Apache-2.0")
// Display icon?: string; // Lucide icon name or URL tags?: string[]; // Search tags
// Documentation documentationUrl?: string; repositoryUrl?: string;
// Compatibility minAuthrimVersion?: string; // Minimum required Authrim version
// Status stability?: 'stable' | 'beta' | 'alpha' | 'deprecated'; deprecationNotice?: string; // Migration instructions if deprecated
// Admin notes (internal only) adminNotes?: string; // Tips, known issues, deployment notes}Capability Types
Plugins declare their capabilities using a {type}.{channel} format:
type PluginCapability = | `notifier.${string}` // notifier.email, notifier.sms, notifier.push | `idp.${string}` // idp.google, idp.saml, idp.oidc | `authenticator.${string}` // authenticator.passkey, authenticator.totp | `flow.${string}`; // flow.otp-send (future extension)Plugin Context
The PluginContext provides access to Authrim’s infrastructure:
interface PluginContext { readonly storage: PluginStorageAccess; // User, Client, Session stores readonly policy: IPolicyInfra; // Authorization checks readonly config: PluginConfigStore; // Plugin configuration readonly logger: Logger; // Structured logging readonly audit: AuditLogger; // Audit events readonly tenantId: string; // Current tenant readonly env: Env; // Environment bindings}Health Check Implementation
Implement healthCheck to report plugin status:
async healthCheck(ctx, config): Promise<HealthStatus> { return { status: 'healthy', // 'healthy' | 'degraded' | 'unhealthy' message: 'All systems operational', timestamp: Date.now(), checks: { api: { status: 'pass', message: 'API reachable' }, credentials: { status: 'pass', message: 'Credentials valid' }, }, };}Testing Your Plugin
Use Vitest for unit testing:
import { describe, it, expect, vi } from 'vitest';import { CapabilityRegistry } from '@authrim/ar-lib-plugin';import { myPlugin } from './my-plugin';
describe('MyPlugin', () => { it('should register notifier capability', () => { const registry = new CapabilityRegistry(); const config = { apiKey: 'test-key', endpoint: 'https://api.example.com', timeout: 10000, };
myPlugin.register(registry, config);
expect(registry.getNotifier('custom')).toBeDefined(); expect(registry.listCapabilities()).toContain('notifier.custom'); });
it('should send notification successfully', async () => { const registry = new CapabilityRegistry(); const config = { apiKey: 'test-key', endpoint: 'https://api.example.com', timeout: 10000, };
// Mock fetch global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ id: 'msg-123' }), });
myPlugin.register(registry, config); const notifier = registry.getNotifier('custom')!;
const result = await notifier.send({ channel: 'custom', body: 'Test message', });
expect(result.success).toBe(true); expect(result.messageId).toBe('msg-123'); });
it('should handle API errors', async () => { const registry = new CapabilityRegistry(); const config = { apiKey: 'test-key', endpoint: 'https://api.example.com', timeout: 10000, };
global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500, });
myPlugin.register(registry, config); const notifier = registry.getNotifier('custom')!;
const result = await notifier.send({ channel: 'custom', body: 'Test message', });
expect(result.success).toBe(false); expect(result.retryable).toBe(true); });});Best Practices
Error Handling
Always return structured errors instead of throwing:
async send(notification) { try { const response = await fetch(url, options);
if (!response.ok) { return { success: false, error: `API error: ${response.status}`, retryable: response.status >= 500, }; }
return { success: true, messageId: result.id }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', retryable: true, }; }}Timeout Management
Always use timeouts for external calls:
const response = await fetch(url, { signal: AbortSignal.timeout(config.timeout),});Security
- Never log sensitive data (API keys, tokens)
- Validate URLs to prevent SSRF
- Set appropriate timeouts
- Sanitize user input in templates
Next Steps
- Plugin Capabilities - Learn about handler implementations
- Configuration Schema - Advanced schema patterns
- Deployment - Package and distribute your plugin