Plugin Capabilities
Authrim plugins provide capabilities through handler implementations. This guide covers each capability type in detail.
Capability Categories
| Category | Prefix | Registration Method |
|---|---|---|
| Notifier | notifier. | registry.registerNotifier() |
| Identity Provider | idp. | registry.registerIdP() |
| Authenticator | authenticator. | registry.registerAuthenticator() |
| Flow | flow. | Coming soon |
Notifier Handler
Notifier plugins send notifications via email, SMS, push, or custom channels.
Interface
interface NotifierHandler { /** Send a notification */ send(notification: Notification): Promise<SendResult>;
/** Check if handler supports given options (optional) */ supports?(options: NotificationOptions): boolean;}Notification Object
interface Notification { channel: string; // 'email', 'sms', 'push', 'custom' to: string; // Recipient address from?: string; // Sender (optional, uses default) subject?: string; // Subject line (for email) body: string; // Message content replyTo?: string; // Reply-to address (email) cc?: string[]; // CC recipients (email) bcc?: string[]; // BCC recipients (email) templateId?: string; // Template identifier templateVars?: Record<string, unknown>; // Template variables metadata?: Record<string, unknown>; // Custom metadata}SendResult Object
interface SendResult { success: boolean; // Whether send succeeded messageId?: string; // Provider's message ID error?: string; // Error message if failed errorCode?: string; // Provider's error code retryable?: boolean; // Can this be retried? providerResponse?: unknown; // Raw provider response}Example: Console Notifier
A simplified version of the built-in console notifier:
import { z } from 'zod';import type { AuthrimPlugin, Notification, SendResult } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ prefix: z.string().default('[NOTIFY]').describe('Log prefix'), logLevel: z.enum(['debug', 'info', 'warn']).default('info'),});
type ConsoleConfig = z.infer<typeof configSchema>;
export const consoleNotifierPlugin: AuthrimPlugin<ConsoleConfig> = { id: 'notifier-console', version: '1.0.0', capabilities: ['notifier.email', 'notifier.sms', 'notifier.push'], configSchema,
meta: { name: 'Console Notifier', description: 'Logs notifications to console (development only)', category: 'notification', icon: 'terminal', },
register(registry, config) { const handler = { async send(notification: Notification): Promise<SendResult> { const messageId = `console-${Date.now()}`;
console.log(`${config.prefix} Notification sent:`, { messageId, channel: notification.channel, to: notification.to, subject: notification.subject, bodyPreview: notification.body.slice(0, 100), });
return { success: true, messageId }; },
supports(): boolean { return true; // Supports all channels }, };
// Register for all supported channels registry.registerNotifier('email', handler); registry.registerNotifier('sms', handler); registry.registerNotifier('push', handler); },};Example: Email Notifier with Resend
import { z } from 'zod';import type { AuthrimPlugin, Notification, SendResult } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ apiKey: z.string().min(1).describe('Resend API key'), defaultFrom: z.string().email().describe('Default sender email'), replyTo: z.string().email().optional().describe('Reply-to address'), timeout: z.number().default(10000).describe('Request timeout (ms)'),});
type ResendConfig = z.infer<typeof configSchema>;
export const resendPlugin: AuthrimPlugin<ResendConfig> = { id: 'notifier-resend', version: '1.0.0', capabilities: ['notifier.email'], configSchema,
meta: { name: 'Resend Email', description: 'Send transactional emails via Resend API', category: 'notification', icon: 'mail', },
register(registry, config) { registry.registerNotifier('email', { async send(notification: Notification): Promise<SendResult> { try { const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: notification.from || config.defaultFrom, to: notification.to, subject: notification.subject || 'Notification', html: notification.body, reply_to: notification.replyTo || config.replyTo, }), signal: AbortSignal.timeout(config.timeout), });
const result = await response.json();
if (!response.ok) { return { success: false, error: result.message || 'API error', errorCode: result.statusCode?.toString(), retryable: response.status >= 500, }; }
return { success: true, messageId: result.id, providerResponse: result, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', retryable: true, }; } }, }); },};Identity Provider Handler
IdP plugins enable social login and federated authentication.
Interface
interface IdPHandler { /** Generate authorization URL for OAuth flow */ getAuthorizationUrl(params: IdPAuthParams): Promise<string>;
/** Exchange authorization code for tokens */ exchangeCode(params: IdPExchangeParams): Promise<IdPTokenResult>;
/** Fetch user information using access token */ getUserInfo(accessToken: string): Promise<IdPUserInfo>;
/** Validate and decode ID token (optional) */ validateIdToken?(idToken: string): Promise<IdPClaims>;}Parameter Types
interface IdPAuthParams { redirectUri: string; state: string; nonce?: string; scopes?: string[]; extraParams?: Record<string, string>;}
interface IdPExchangeParams { code: string; redirectUri: string; codeVerifier?: string; // For PKCE}
interface IdPTokenResult { accessToken: string; refreshToken?: string; idToken?: string; expiresIn?: number; tokenType: string; scope?: string;}
interface IdPUserInfo { sub: string; // Subject identifier (unique user ID) email?: string; emailVerified?: boolean; name?: string; givenName?: string; familyName?: string; picture?: string; locale?: string; [key: string]: unknown; // Additional claims}OAuth 2.0 Flow
sequenceDiagram
participant User
participant App
participant Authrim
participant IdP as Identity Provider
User->>App: Click "Sign in with X"
App->>Authrim: Initiate OAuth
Authrim->>IdP: getAuthorizationUrl()
IdP-->>User: Redirect to login
User->>IdP: Authenticate
IdP-->>Authrim: Redirect with code
Authrim->>IdP: exchangeCode()
IdP-->>Authrim: Access token
Authrim->>IdP: getUserInfo()
IdP-->>Authrim: User profile
Authrim-->>App: Session created
Example: GitHub IdP Plugin
import { z } from 'zod';import type { AuthrimPlugin, IdPAuthParams, IdPExchangeParams } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ clientId: z.string().min(1).describe('GitHub OAuth App Client ID'), clientSecret: z.string().min(1).describe('GitHub OAuth App Client Secret'), scopes: z.array(z.string()).default(['read:user', 'user:email']),});
type GitHubConfig = z.infer<typeof configSchema>;
export const githubIdpPlugin: AuthrimPlugin<GitHubConfig> = { id: 'idp-github', version: '1.0.0', capabilities: ['idp.github'], configSchema,
meta: { name: 'GitHub', description: 'Sign in with GitHub', category: 'identity', icon: 'github', },
register(registry, config) { registry.registerIdP('github', { async getAuthorizationUrl(params: IdPAuthParams): Promise<string> { const url = new URL('https://github.com/login/oauth/authorize'); url.searchParams.set('client_id', config.clientId); url.searchParams.set('redirect_uri', params.redirectUri); url.searchParams.set('state', params.state); url.searchParams.set('scope', (params.scopes ?? config.scopes).join(' ')); return url.toString(); },
async exchangeCode(params: IdPExchangeParams) { const response = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ client_id: config.clientId, client_secret: config.clientSecret, code: params.code, redirect_uri: params.redirectUri, }), });
const result = await response.json();
if (result.error) { throw new Error(result.error_description || result.error); }
return { accessToken: result.access_token, tokenType: result.token_type || 'bearer', scope: result.scope, }; },
async getUserInfo(accessToken: string) { const userResponse = await fetch('https://api.github.com/user', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', }, });
if (!userResponse.ok) { throw new Error(`GitHub API error: ${userResponse.status}`); }
const user = await userResponse.json();
// Fetch primary email let email: string | undefined; let emailVerified = false;
const emailResponse = await fetch('https://api.github.com/user/emails', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', }, });
if (emailResponse.ok) { const emails = await emailResponse.json(); const primary = emails.find((e: any) => e.primary); if (primary) { email = primary.email; emailVerified = primary.verified; } }
return { sub: user.id.toString(), email, emailVerified, name: user.name || user.login, picture: user.avatar_url, }; }, }); },};Authenticator Handler
Authenticator plugins provide MFA and authentication methods.
Interface
interface AuthenticatorHandler { /** Start an authentication challenge */ startChallenge(params: AuthChallengeParams): Promise<AuthChallengeResult>;
/** Verify the user's response */ verifyResponse(params: AuthVerifyParams): Promise<AuthVerifyResult>;
/** Check if authenticator is available for user (optional) */ isAvailable?(userId: string): Promise<boolean>;}Parameter Types
interface AuthChallengeParams { userId: string; sessionId: string; metadata?: Record<string, unknown>;}
interface AuthChallengeResult { challengeId: string; challenge: unknown; // Authenticator-specific data expiresAt: number; // Unix timestamp metadata?: Record<string, unknown>;}
interface AuthVerifyParams { challengeId: string; response: unknown; // User's response userId: string; sessionId: string;}
interface AuthVerifyResult { success: boolean; credentialId?: string; error?: string; metadata?: Record<string, unknown>;}Example: TOTP Authenticator
A simplified version of the built-in TOTP plugin:
import { z } from 'zod';import type { AuthrimPlugin } from '@authrim/ar-lib-plugin';
const configSchema = z.object({ issuer: z.string().default('Authrim').describe('Issuer name for authenticator apps'), digits: z.literal(6).or(z.literal(8)).default(6).describe('OTP code length'), period: z.number().int().min(15).max(120).default(30).describe('Code refresh interval (seconds)'), window: z.number().int().min(0).max(5).default(1).describe('Time drift tolerance (±steps)'),});
type TOTPConfig = z.infer<typeof configSchema>;
export const totpPlugin: AuthrimPlugin<TOTPConfig> = { id: 'authenticator-totp', version: '1.0.0', capabilities: ['authenticator.totp'], configSchema,
meta: { name: 'TOTP Authenticator', description: 'Time-based One-Time Password (RFC 6238)', category: 'authentication', icon: 'shield-check', },
register(registry, config) { registry.registerAuthenticator('totp', { async startChallenge(params) { const isSetup = params.metadata?.setup === true; const challengeId = crypto.randomUUID(); const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes
if (isSetup) { // Generate secret for new TOTP setup const secret = generateSecret(); const otpauthUri = generateOtpauthUri({ secret, issuer: config.issuer, accountName: params.metadata?.email as string || params.userId, digits: config.digits, period: config.period, });
return { challengeId, challenge: { type: 'totp_setup', secret: base32Encode(secret), otpauthUri, digits: config.digits, period: config.period, }, expiresAt, }; }
// Verification challenge return { challengeId, challenge: { type: 'totp_verify', digits: config.digits, period: config.period, }, expiresAt, }; },
async verifyResponse(params) { const { code, secret } = params.response as { code: string; secret?: string };
if (!code || !/^\d{6,8}$/.test(code)) { return { success: false, error: 'Invalid code format' }; }
// Verify TOTP code (implementation uses Web Crypto API) const isValid = await verifyTOTP(secret, code, { digits: config.digits, period: config.period, window: config.window, });
if (isValid) { return { success: true, credentialId: params.challengeId, }; }
return { success: false, error: 'Invalid code' }; },
async isAvailable(userId: string) { // Check if user has TOTP set up return true; }, }); },};
// Helper functions (simplified)function generateSecret(): Uint8Array { const secret = new Uint8Array(20); crypto.getRandomValues(secret); return secret;}
function base32Encode(data: Uint8Array): string { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let result = ''; // ... encoding logic return result;}
function generateOtpauthUri(params: any): string { const { secret, issuer, accountName, digits, period } = params; return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}?secret=${base32Encode(secret)}&issuer=${issuer}&algorithm=SHA1&digits=${digits}&period=${period}`;}
async function verifyTOTP(secret: string, code: string, options: any): Promise<boolean> { // TOTP verification using Web Crypto API // ... verification logic return true;}Flow Nodes
Registration Rules
- Uniqueness: Each capability channel can only have one handler
- First-wins: First registration takes precedence
- Error on conflict: Re-registering the same channel throws an error
// This will throw an errorregistry.registerNotifier('email', handler1);registry.registerNotifier('email', handler2); // Error: already registeredNext Steps
- Configuration Schema - Advanced schema patterns and secret handling
- Deployment - Package and distribute your plugin