Skip to content

Plugin Capabilities

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

CategoryPrefixRegistration Method
Notifiernotifier.registry.registerNotifier()
Identity Provideridp.registry.registerIdP()
Authenticatorauthenticator.registry.registerAuthenticator()
Flowflow.Extension point; no public registry method yet

Notifier plugins send notifications via email, SMS, push, or custom channels.

interface NotifierHandler {
/** Send a notification */
send(notification: Notification): Promise<SendResult>;
/** Check if handler supports given options (optional) */
supports?(options: NotificationOptions): boolean;
}
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
}
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
}

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);
},
};
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,
};
}
},
});
},
};

IdP plugins enable social login and federated authentication.

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>;
}
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
}
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
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 plugins provide MFA and authentication methods.

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>;
}
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>;
}

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;
}

  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