プラグインの作成
このガイドでは、Authrimプラグインをゼロから作成する方法を説明します。
前提条件
| 要件 | バージョン |
|---|---|
| Node.js | 18+ |
| TypeScript | 5.0+ |
| Zod | 3.x |
必要な依存関係をインストールします:
pnpm add @authrim/ar-lib-plugin zodプラグインインターフェース
すべてのAuthrimプラグインはAuthrimPluginインターフェースを実装する必要があります:
interface AuthrimPlugin<TConfig = unknown> { /** 一意のプラグインID(例:'notifier-resend') */ readonly id: string;
/** セマンティックバージョン(例:'1.0.0') */ readonly version: string;
/** このプラグインが提供する能力 */ readonly capabilities: PluginCapability[];
/** 公式Authrimプラグインかどうか */ readonly official?: boolean;
/** 設定検証用のZodスキーマ */ readonly configSchema: z.ZodType<TConfig, z.ZodTypeDef, unknown>;
/** Admin UIダッシュボード用のメタデータ */ readonly meta: PluginMeta;
/** 能力をレジストリに登録(必須) */ register(registry: CapabilityRegistry, config: TConfig): void;
/** プラグインを初期化(オプション) */ initialize?(ctx: PluginContext, config: TConfig): Promise<void>;
/** アンロード時のクリーンアップ(オプション) */ shutdown?(): Promise<void>;
/** モニタリング用のヘルスチェック(オプション) */ healthCheck?(ctx?: PluginContext, config?: TConfig): Promise<HealthStatus>;}最小プラグインテンプレート
コピペ可能な完全なテンプレートです:
import { z } from 'zod';import type { AuthrimPlugin, PluginContext, CapabilityRegistry, HealthStatus,} from '@authrim/ar-lib-plugin';
// 1. Admin UI用の説明付き設定スキーマを定義const configSchema = z.object({ apiKey: z.string().min(1).describe('プロバイダーダッシュボードからのAPIキー'), endpoint: z.string().url().default('https://api.example.com').describe('APIエンドポイントURL'), timeout: z.number().int().min(1000).max(30000).default(10000).describe('リクエストタイムアウト(ms)'),});
type MyPluginConfig = z.infer<typeof configSchema>;
// 2. プラグインをエクスポートexport const myPlugin: AuthrimPlugin<MyPluginConfig> = { id: 'my-custom-plugin', version: '1.0.0', capabilities: ['notifier.custom'], configSchema,
meta: { name: 'My Custom Plugin', description: 'Example Service用のカスタム通知プラグイン', category: 'notification', icon: 'bell', author: { name: 'Your Name', }, license: 'MIT', stability: 'stable', minAuthrimVersion: '1.0.0', },
// 3. オプション:外部接続を初期化 async initialize(ctx: PluginContext, config: MyPluginConfig): Promise<void> { ctx.logger.info('Initializing plugin', { pluginId: this.id });
// 外部サービスの接続性を検証 const response = await fetch(`${config.endpoint}/health`, { signal: AbortSignal.timeout(config.timeout), });
if (!response.ok) { throw new Error(`Service unavailable: ${response.status}`); } },
// 4. 必須:能力を登録 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. オプション:ヘルスチェック 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. オプション:クリーンアップ async shutdown(): Promise<void> { // 接続をクローズ、リソースをクリーンアップ },};プラグインメタデータ
metaフィールドはAdmin UI用の情報を提供します:
interface PluginMeta { // 必須 name: string; // 表示名 description: string; // 短い説明(1-2文) category: PluginCategory; // 'notification' | 'identity' | 'authentication' | 'flow'
// 作者とライセンス(コミュニティプラグインでは必須) author?: { name: string; email?: string; url?: string; }; license?: string; // SPDX形式(例:"MIT", "Apache-2.0")
// 表示 icon?: string; // Lucideアイコン名またはURL tags?: string[]; // 検索タグ
// ドキュメント documentationUrl?: string; repositoryUrl?: string;
// 互換性 minAuthrimVersion?: string; // 最小必要Authrimバージョン
// ステータス stability?: 'stable' | 'beta' | 'alpha' | 'deprecated'; deprecationNotice?: string; // 非推奨の場合の移行手順
// 管理者メモ(内部専用) adminNotes?: string; // ヒント、既知の問題、デプロイメモ}能力タイプ
プラグインは{type}.{channel}形式で能力を宣言します:
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(将来の拡張)プラグインコンテキスト
PluginContextはAuthrimのインフラストラクチャへのアクセスを提供します:
interface PluginContext { readonly storage: PluginStorageAccess; // User, Client, Sessionストア readonly policy: IPolicyInfra; // 認可チェック readonly config: PluginConfigStore; // プラグイン設定 readonly logger: Logger; // 構造化ロギング readonly audit: AuditLogger; // 監査イベント readonly tenantId: string; // 現在のテナント readonly env: Env; // 環境バインディング}ヘルスチェック実装
healthCheckを実装してプラグインのステータスを報告します:
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' }, }, };}プラグインのテスト
ユニットテストにはVitestを使用します:
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, };
// 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); });});ベストプラクティス
エラーハンドリング
スローする代わりに構造化されたエラーを返します:
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, }; }}タイムアウト管理
外部呼び出しには常にタイムアウトを使用します:
const response = await fetch(url, { signal: AbortSignal.timeout(config.timeout),});セキュリティ
- 機密データ(APIキー、トークン)をログに出力しない
- SSRFを防ぐためにURLを検証
- 適切なタイムアウトを設定
- テンプレート内のユーザー入力をサニタイズ