コンテンツにスキップ

プラグインの作成

このガイドでは、Authrimプラグインをゼロから作成する方法を説明します。

前提条件

要件バージョン
Node.js18+
TypeScript5.0+
Zod3.x

必要な依存関係をインストールします:

Terminal window
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を検証
  • 適切なタイムアウトを設定
  • テンプレート内のユーザー入力をサニタイズ

次のステップ