Skip to content

Plugin Capabilities

Authrim plugins provide capabilities through handler implementations. This guide covers each capability type in detail.

Capability Categories

CategoryPrefixRegistration Method
Notifiernotifier.registry.registerNotifier()
Identity Provideridp.registry.registerIdP()
Authenticatorauthenticator.registry.registerAuthenticator()
Flowflow.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

  1. Uniqueness: Each capability channel can only have one handler
  2. First-wins: First registration takes precedence
  3. Error on conflict: Re-registering the same channel throws an error
// This will throw an error
registry.registerNotifier('email', handler1);
registry.registerNotifier('email', handler2); // Error: already registered

Next Steps