Plugin Capabilities
Authrim plugins provide capabilities through handler implementations. This guide covers each capability type in detail.
Capability Categories
Section titled “Capability Categories”| Category | Prefix | Registration Method |
|---|---|---|
| Notifier | notifier. | registry.registerNotifier() |
| Identity Provider | idp. | registry.registerIdP() |
| Authenticator | authenticator. | registry.registerAuthenticator() |
| Flow | flow. | Extension point; no public registry method yet |
Notifier Handler
Section titled “Notifier Handler”Notifier plugins send notifications via email, SMS, push, or custom channels.
Interface
Section titled “Interface”interface NotifierHandler { /** Send a notification */ send(notification: Notification): Promise<SendResult>;
/** Check if handler supports given options (optional) */ supports?(options: NotificationOptions): boolean;}Notification Object
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Identity Provider Handler”IdP plugins enable social login and federated authentication.
Interface
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Authenticator Handler”Authenticator plugins provide MFA and authentication methods.
Interface
Section titled “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
Section titled “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
Section titled “Example: TOTP Authenticator”A simplified example based on the TOTP authenticator plugin implementation. If you want it visible through Plugin Management API or Admin UI discovery, register it explicitly in the Worker that uses it:
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
Section titled “Flow Nodes”Registration Rules
Section titled “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
Section titled “Next Steps”- Configuration Schema - Advanced schema patterns and secret handling
- Deployment - Package and distribute your plugin