diff --git a/CHANGELOG.md b/CHANGELOG.md
index 432a134..66be871 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.1.2-beta] - 2025-08-15
+
+### Added
+- **MiAuth authentication support** - Alternative authentication method for Misskey servers
+- **Comprehensive error handling system** - Granular exception classes for different error scenarios
+
+### Changed
+- **Error handling architecture** - Replaced generic exceptions with specific `MisskeyAuthException` subclasses
+- **OAuth client improvements** - Enhanced error mapping and better exception handling
+- **MiAuth client implementation** - Complete MiAuth authentication flow with proper error handling
+
+### Features
+- `MisskeyMiAuthClient` - Main MiAuth authentication client
+- `MisskeyMiAuthConfig` - Configuration class for MiAuth authentication
+- `MiAuthTokenResponse` - Response model for MiAuth authentication
+- Enhanced exception classes including:
+ - `OAuthNotSupportedException` - Server doesn't support OAuth 2.0
+ - `MiAuthDeniedException` - User denied MiAuth permission
+ - `NetworkException` - Network connectivity issues
+ - `UserCancelledException` - User cancelled authentication
+ - `CallbackSchemeErrorException` - URL scheme configuration errors
+ - And 15+ more specific exception classes
+
## [0.1.1-beta] - 2025-08-12
### Changed
diff --git a/README.md b/README.md
index a91ac16..f4943cf 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ Add this to your package's `pubspec.yaml` file:
```yaml
dependencies:
- misskey_auth: ^0.1.1-beta
+ misskey_auth: ^0.1.2-beta
```
### Quick Start
@@ -134,6 +134,63 @@ Add to `android/app/src/main/AndroidManifest.xml`:
```
+#### Differences in MiAuth and OAuth Configuration (Key Points for App Integration)
+- This configuration (registration of the URL scheme) is done on the "app side." It is not included in the library's Manifest.
+- Both methods require a "custom URL scheme" to return from an external browser to the app.
+- The difference lies in how to specify "where to return from the browser."
+- OAuth: Since it needs to return to an HTTPS `redirect_uri` from the authorization server, `redirect.html` placed there ultimately redirects back to `yourscheme://...` for the app.
+- MiAuth: The `callback` query of the authentication start URL specifies `yourscheme://...` from the beginning (no need for `https`).
+
+##### Example of MiAuth
+
+```dart
+import 'package:misskey_auth/misskey_auth.dart';
+
+final miClient = MisskeyMiAuthClient();
+final miConfig = MisskeyMiAuthConfig(
+ host: 'misskey.io',
+ appName: 'Your App',
+ callbackScheme: 'yourscheme', // Scheme registered on the app side
+ permissions: ['read:account', 'write:notes'],
+ iconUrl: 'https://example.com/icon.png', // Optional
+);
+final miRes = await miClient.authenticate(miConfig);
+```
+
+##### Example of OAuth
+
+```dart
+import 'package:misskey_auth/misskey_auth.dart';
+
+final oauthClient = MisskeyOAuthClient();
+final oauthConfig = MisskeyOAuthConfig(
+ host: 'misskey.io',
+ clientId: 'https://yourpage/yourapp/',
+ redirectUri: 'https://yourpage/yourapp/redirect.html',
+ scope: 'read:account write:notes',
+ callbackScheme: 'yourscheme', // Scheme registered on the app side
+);
+final token = await oauthClient.authenticate(oauthConfig);
+```
+
+##### How to Support Both Methods in the Same App
+- By registering the same `scheme` (e.g., `yourscheme`) in iOS's `Info.plist` and Android's `AndroidManifest.xml`, it can be shared between OAuth and MiAuth.
+- If you implement the OAuth `redirect.html` to redirect to `yourscheme://oauth/callback?...`, you can reuse the same path expression (`yourscheme://oauth/callback`) for MiAuth's `callback`.
+- For Android, matching only on the `scheme` is sufficient as shown below (the `host` and `path` are optional).
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
### API Reference
#### MisskeyOAuthConfig
@@ -152,26 +209,52 @@ class MisskeyOAuthConfig {
#### MisskeyOAuthClient
-Main client for handling Misskey authentication.
+Main client for handling Misskey OAuth authentication.
```dart
class MisskeyOAuthClient {
/// Authenticate with Misskey server
- Future authenticate(MisskeyOAuthConfig config);
+ Future authenticate(MisskeyOAuthConfig config);
+
+ /// Get OAuth server information
+ Future getOAuthServerInfo(String host);
- /// Check if server supports OAuth 2.0
- Future isOAuthSupported(String host);
+ /// Get stored access token
+ Future getStoredAccessToken();
+
+ /// Clear stored tokens
+ Future clearTokens();
+}
+```
+
+#### MisskeyMiAuthClient
+
+Main client for handling Misskey MiAuth authentication.
+
+```dart
+class MisskeyMiAuthClient {
+ /// Authenticate with Misskey server using MiAuth
+ Future authenticate(MisskeyMiAuthConfig config);
+
+ /// Get stored access token
+ Future getStoredAccessToken();
+
+ /// Clear stored tokens
+ Future clearTokens();
}
```
### Error Handling
-The library provides custom exceptions for different error scenarios:
+The library provides comprehensive error handling with custom exception classes for different scenarios. For detailed information about each exception class and their usage, please refer to the documentation on pub.dev.
-- `MisskeyAuthException` - Base exception class
-- `OAuthNotSupportedException` - When server doesn't support OAuth 2.0
-- `AuthenticationFailedException` - When authentication fails
-- `TokenExchangeException` - When token exchange fails
+The library includes exception classes for:
+- Authentication configuration errors
+- Network and connectivity issues
+- OAuth and MiAuth specific errors
+- User cancellation and authorization failures
+- Secure storage operations
+- Response parsing errors
### Common Errors
@@ -251,7 +334,7 @@ MisskeyのOAuth認証・MiAuth認証をFlutterアプリで簡単に扱うため
```yaml
dependencies:
- misskey_auth: ^0.1.1-beta
+ misskey_auth: ^0.1.2-beta
```
### クイックスタート
@@ -355,6 +438,65 @@ final token = await client.authenticate(config);
```
+#### MiAuth と OAuth の設定の違い(アプリ組み込み時のポイント)
+
+- この設定(URLスキームの登録)は「アプリ側」で行います。ライブラリ内のManifestには含めません。
+- 両方式とも、外部ブラウザからアプリへ戻すために「カスタムURLスキーム」が必要です。
+- 相違点は「ブラウザからどこに戻すか」の指定方法です。
+ - OAuth: 認可サーバーからはHTTPSの`redirect_uri`に戻る必要があるため、そこに配置した`redirect.html`が最終的に`yourscheme://...`へリダイレクトしてアプリに戻します。
+ - MiAuth: 認証開始URLの`callback`クエリに、最初から`yourscheme://...`を指定します(`https`は不要)。
+
+##### MiAuth の例(Dart)
+
+```dart
+import 'package:misskey_auth/misskey_auth.dart';
+
+final miClient = MisskeyMiAuthClient();
+final miConfig = MisskeyMiAuthConfig(
+ host: 'misskey.io',
+ appName: 'Your App',
+ callbackScheme: 'yourscheme', // アプリ側で登録したスキーム
+ permissions: ['read:account', 'write:notes'],
+ iconUrl: 'https://example.com/icon.png', // 任意
+);
+final miRes = await miClient.authenticate(miConfig);
+```
+
+##### OAuth の例
+
+```dart
+import 'package:misskey_auth/misskey_auth.dart';
+
+final oauthClient = MisskeyOAuthClient();
+final oauthConfig = MisskeyOAuthConfig(
+ host: 'misskey.io',
+ clientId: 'https://yourpage/yourapp/',
+ redirectUri: 'https://yourpage/yourapp/redirect.html',
+ scope: 'read:account write:notes',
+ callbackScheme: 'yourscheme', // アプリ側で登録したスキーム
+);
+final token = await oauthClient.authenticate(oauthConfig);
+```
+
+##### 両方式を同一アプリでサポートするには
+
+- iOSの`Info.plist`・Androidの`AndroidManifest.xml`で同じ`sheme`(例: `yourscheme`)を1つ登録すれば、OAuth/MiAuthで共用可能です。
+- OAuth用の`redirect.html`は、`yourscheme://oauth/callback?...`へ飛ばす実装にしておくと、MiAuthの`callback`でも同じパス表現(`yourscheme://oauth/callback`)を使い回せます。
+- Androidは以下のように`scheme`のみのマッチで十分です(`host`や`path`は任意)。
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
### API リファレンス
#### MisskeyOAuthConfig
@@ -373,26 +515,52 @@ class MisskeyOAuthConfig {
#### MisskeyOAuthClient
-Misskey認証を処理するメインクライアント。
+Misskey OAuth認証を処理するメインクラス
```dart
class MisskeyOAuthClient {
/// Misskeyサーバーで認証を実行
- Future authenticate(MisskeyOAuthConfig config);
+ Future authenticate(MisskeyOAuthConfig config);
+
+ /// OAuthサーバー情報を取得
+ Future getOAuthServerInfo(String host);
+
+ /// 保存されたアクセストークンを取得
+ Future getStoredAccessToken();
+
+ /// 保存されたトークンを削除
+ Future clearTokens();
+}
+```
+
+#### MisskeyMiAuthClient
+
+Misskey MiAuth認証を処理するメインクラス
+
+```dart
+class MisskeyMiAuthClient {
+ /// MisskeyサーバーでMiAuth認証を実行
+ Future authenticate(MisskeyMiAuthConfig config);
+
+ /// 保存されたアクセストークンを取得
+ Future getStoredAccessToken();
- /// サーバーがOAuth 2.0をサポートしているかチェック
- Future isOAuthSupported(String host);
+ /// 保存されたトークンを削除
+ Future clearTokens();
}
```
### エラーハンドリング
-ライブラリは様々なエラーシナリオに対応するカスタム例外を提供します:
+ライブラリには以下のカテゴリの例外クラスが含まれています:
+- 認証設定エラー
+- ネットワーク・接続エラー
+- OAuth・MiAuth固有のエラー
+- ユーザーキャンセル・認可失敗
+- セキュアストレージ操作エラー
+- レスポンス解析エラー
-- `MisskeyAuthException` - ベース例外クラス
-- `OAuthNotSupportedException` - サーバーがOAuth 2.0をサポートしていない場合
-- `AuthenticationFailedException` - 認証が失敗した場合
-- `TokenExchangeException` - トークン交換が失敗した場合
+詳細についてはpub.devのドキュメントを参考にして下さい
### よくあるエラー
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5a66e8a..898912f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -24,14 +24,6 @@
-
-
-
-
-
-
-
-
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 5be01ff..3670012 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -25,6 +25,17 @@
+
+
+
+
+
+
+
+
+
+
+
{
final _client = MisskeyOAuthClient();
+ final _miClient = MisskeyMiAuthClient();
+ int _currentIndex = 0;
// フォームコントローラー
final _hostController = TextEditingController();
@@ -38,12 +43,75 @@ class _AuthExamplePageState extends State {
final _scopeController = TextEditingController();
final _callbackSchemeController = TextEditingController();
+ // MiAuth 用フォーム
+ final _miAppNameController = TextEditingController();
+ final _miPermissionsController = TextEditingController();
+ final _miIconUrlController = TextEditingController();
+
// 状態
- bool _isLoading = false;
String? _accessToken;
- String? _errorMessage;
OAuthServerInfo? _serverInfo;
+ String _mapErrorToMessage(Object error) {
+ // MisskeyAuth のカスタム例外をユーザー向け日本語に整形
+ if (error is MisskeyAuthException) {
+ final details = error.details;
+ if (error is UserCancelledException) {
+ return '認証がキャンセルされました';
+ }
+ if (error is CallbackSchemeErrorException) {
+ return 'コールバックスキームの設定が正しくありません(AndroidManifest/Info.plist を確認してください)';
+ }
+ if (error is AuthorizationLaunchException) {
+ return '認証画面を起動できませんでした';
+ }
+ if (error is NetworkException) {
+ return 'ネットワークエラーが発生しました';
+ }
+ if (error is ResponseParseException) {
+ return 'サーバー応答の解析に失敗しました';
+ }
+ if (error is SecureStorageException) {
+ return 'セキュアストレージの操作に失敗しました';
+ }
+ if (error is InvalidAuthConfigException) {
+ return '認証設定が無効です';
+ }
+ if (error is ServerInfoException) {
+ return 'サーバー情報の取得に失敗しました${details != null ? ': $details' : ''}';
+ }
+ // OAuth
+ if (error is OAuthNotSupportedException) {
+ return 'このサーバーはOAuth認証に対応していません(MiAuthをご利用ください)';
+ }
+ if (error is StateMismatchException) {
+ return 'セキュリティ検証に失敗しました(state不一致)';
+ }
+ if (error is AuthorizationCodeMissingException) {
+ return '認証コードを取得できませんでした';
+ }
+ if (error is AuthorizationServerErrorException) {
+ return '認可サーバーでエラーが発生しました${details != null ? ': $details' : ''}';
+ }
+ if (error is TokenExchangeException) {
+ return 'トークン交換に失敗しました${details != null ? ': $details' : ''}';
+ }
+ // MiAuth
+ if (error is MiAuthDeniedException) {
+ return 'MiAuth がキャンセル/拒否されました';
+ }
+ if (error is MiAuthCheckFailedException) {
+ return 'MiAuth の検証に失敗しました${details != null ? ': $details' : ''}';
+ }
+ if (error is MiAuthSessionInvalidException) {
+ return 'MiAuth のセッションが無効または期限切れです${details != null ? ': $details' : ''}';
+ }
+ return error.toString();
+ }
+ // その他の例外はそのまま文字列化
+ return error.toString();
+ }
+
@override
void initState() {
super.initState();
@@ -58,6 +126,9 @@ class _AuthExamplePageState extends State {
_redirectUriController.dispose();
_scopeController.dispose();
_callbackSchemeController.dispose();
+ _miAppNameController.dispose();
+ _miPermissionsController.dispose();
+ _miIconUrlController.dispose();
super.dispose();
}
@@ -69,22 +140,36 @@ class _AuthExamplePageState extends State {
'https://librarylibrarian.github.io/misskey_auth/redirect.html';
_scopeController.text = 'read:account write:notes';
_callbackSchemeController.text = 'misskeyauth';
+
+ // MiAuth
+ _miAppNameController.text = 'Misskey Auth Example';
+ _miPermissionsController.text = 'read:account write:notes';
+ _miIconUrlController.text = '';
}
Future _loadStoredToken() async {
- final token = await _client.getStoredAccessToken();
- setState(() {
- _accessToken = token;
- });
+ try {
+ final token = await _client.getStoredAccessToken();
+ setState(() {
+ _accessToken = token;
+ });
+ } on MisskeyAuthException catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(_mapErrorToMessage(e))),
+ );
+ }
+ } catch (_) {}
}
Future _checkServerInfo() async {
setState(() {
- _isLoading = true;
_serverInfo = null;
- _errorMessage = null;
});
+ if (!mounted) return;
+ context.loaderOverlay.show();
+
try {
final host = _hostController.text.trim();
if (host.isEmpty) {
@@ -93,28 +178,39 @@ class _AuthExamplePageState extends State {
final serverInfo = await _client.getOAuthServerInfo(host);
+ if (!mounted) return;
setState(() {
_serverInfo = serverInfo;
- if (serverInfo == null) {
- _errorMessage = 'OAuth認証はサポートされていません(MiAuth認証を使用してください)';
- }
});
+
+ if (serverInfo == null && mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('OAuth認証はサポートされていません(MiAuth認証を使用してください)')),
+ );
+ }
+ } on MisskeyAuthException catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(_mapErrorToMessage(e))),
+ );
+ }
} catch (e) {
- setState(() {
- _errorMessage = 'エラー: $e';
- });
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(e.toString())),
+ );
+ }
} finally {
- setState(() {
- _isLoading = false;
- });
+ if (mounted) {
+ context.loaderOverlay.hide();
+ }
}
}
Future _startAuth() async {
- setState(() {
- _isLoading = true;
- _errorMessage = null;
- });
+ if (!mounted) return;
+ context.loaderOverlay.show();
try {
final config = MisskeyOAuthConfig(
@@ -127,7 +223,7 @@ class _AuthExamplePageState extends State {
final tokenResponse = await _client.authenticate(config);
- if (tokenResponse != null) {
+ if (tokenResponse != null && mounted) {
setState(() {
_accessToken = tokenResponse.accessToken;
});
@@ -138,31 +234,110 @@ class _AuthExamplePageState extends State {
);
}
}
+ } on MisskeyAuthException catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(_mapErrorToMessage(e))),
+ );
+ }
} catch (e) {
- setState(() {
- _errorMessage = e.toString();
- });
-
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('認証エラー: $e')),
);
}
} finally {
- setState(() {
- _isLoading = false;
- });
+ if (mounted) {
+ context.loaderOverlay.hide();
+ }
}
}
- Future _clearToken() async {
- await _client.clearTokens();
- await _loadStoredToken();
+ Future _startMiAuth() async {
+ if (!mounted) return;
+ context.loaderOverlay.show();
+
+ try {
+ final host = _hostController.text.trim();
+ if (host.isEmpty) {
+ throw Exception('ホストを入力してください');
+ }
+
+ final scheme = _callbackSchemeController.text.trim();
+ if (scheme.isEmpty) {
+ throw Exception('コールバックスキームを入力してください');
+ }
+
+ final permissions = _miPermissionsController.text
+ .split(RegExp(r"[ ,]+"))
+ .where((e) => e.isNotEmpty)
+ .toList();
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('トークンを削除しました')),
+ final config = MisskeyMiAuthConfig(
+ host: host,
+ appName: _miAppNameController.text.trim(),
+ callbackScheme: scheme,
+ permissions: permissions,
+ iconUrl: _miIconUrlController.text.trim().isEmpty
+ ? null
+ : _miIconUrlController.text.trim(),
);
+
+ final res = await _miClient.authenticate(config);
+
+ if (mounted) {
+ setState(() {
+ _accessToken = res.token;
+ });
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('MiAuth に成功しました!')),
+ );
+ }
+ }
+ } on MisskeyAuthException catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(_mapErrorToMessage(e))),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('MiAuth エラー: $e')),
+ );
+ }
+ } finally {
+ if (mounted) {
+ context.loaderOverlay.hide();
+ }
+ }
+ }
+
+ Future _clearToken() async {
+ if (!mounted) return;
+ context.loaderOverlay.show();
+
+ try {
+ await _client.clearTokens();
+ await _loadStoredToken();
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('トークンを削除しました')),
+ );
+ }
+ } on MisskeyAuthException catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(_mapErrorToMessage(e))),
+ );
+ }
+ } finally {
+ if (mounted) {
+ context.loaderOverlay.hide();
+ }
}
}
@@ -178,185 +353,271 @@ class _AuthExamplePageState extends State {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- // 保存されたトークン情報
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- '保存されたトークン',
- style:
- TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 8),
- Text(_accessToken != null
- ? '${_accessToken!.substring(0, 10)}...'
- : 'トークンなし'),
- if (_accessToken != null) ...[
- const SizedBox(height: 8),
- ElevatedButton(
- onPressed: _clearToken,
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.red,
- foregroundColor: Colors.white,
- ),
- child: const Text('トークンを削除'),
- ),
- ],
- ],
+ _buildStoredTokenCard(),
+ const SizedBox(height: 16),
+ if (_currentIndex == 0)
+ _buildOAuthForm(context)
+ else if (_currentIndex == 1)
+ _buildMiAuthForm(context)
+ else
+ _buildServerInfoTab(context),
+ // 画面内のエラーカード表示は行わず、Snackbarのみで通知
+ ],
+ ),
+ ),
+ bottomNavigationBar: NavigationBar(
+ selectedIndex: _currentIndex,
+ destinations: const [
+ NavigationDestination(icon: Icon(Icons.lock), label: 'OAuth'),
+ NavigationDestination(icon: Icon(Icons.vpn_key), label: 'MiAuth'),
+ NavigationDestination(
+ icon: Icon(Icons.info_outline), label: 'サーバー情報'),
+ ],
+ onDestinationSelected: (index) {
+ setState(() {
+ _currentIndex = index;
+ });
+ },
+ ),
+ );
+ }
+
+ Widget _buildStoredTokenCard() {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '保存されたトークン',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Text(_accessToken != null
+ ? '${_accessToken!.substring(0, 10)}...'
+ : 'トークンなし'),
+ if (_accessToken != null) ...[
+ const SizedBox(height: 8),
+ ElevatedButton(
+ onPressed: _clearToken,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.red,
+ foregroundColor: Colors.white,
),
+ child: const Text('トークンを削除'),
),
- ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+ Widget _buildOAuthForm(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'OAuth認証設定',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _callbackSchemeController,
+ decoration: const InputDecoration(
+ labelText: 'コールバックスキーム',
+ hintText: '例: misskeyauth',
+ ),
+ ),
const SizedBox(height: 16),
-
- // 認証設定フォーム
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- 'OAuth認証設定',
- style:
- TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 8),
- TextField(
- controller: _callbackSchemeController,
- decoration: const InputDecoration(
- labelText: 'コールバックスキーム',
- hintText: '例: misskeyauth',
- ),
- ),
- const SizedBox(height: 16),
- TextField(
- controller: _hostController,
- decoration: const InputDecoration(
- labelText: 'ホスト',
- hintText: '例: misskey.io',
- ),
- ),
- const SizedBox(height: 8),
- TextField(
- controller: _clientIdController,
- decoration: const InputDecoration(
- labelText: 'クライアントID (URL)',
- hintText: '例: https://example.com/my-app',
- ),
- ),
- const SizedBox(height: 8),
- TextField(
- controller: _redirectUriController,
- decoration: const InputDecoration(
- labelText: 'リダイレクトURI',
- hintText: '例: https://example.com/redirect',
- helperText: '要HTTPS',
- ),
- ),
- const SizedBox(height: 8),
- TextField(
- controller: _scopeController,
- decoration: const InputDecoration(
- labelText: 'スコープ',
- hintText: '例: read:account write:notes',
- ),
- ),
- const SizedBox(height: 16),
- Row(
- children: [
- ElevatedButton(
- onPressed: _isLoading ? null : _checkServerInfo,
- child: const Text('サーバー情報を確認'),
- ),
- const SizedBox(width: 8),
- ElevatedButton(
- onPressed: _isLoading ? null : _startAuth,
- style: ElevatedButton.styleFrom(
- backgroundColor:
- Theme.of(context).colorScheme.primary,
- foregroundColor: Colors.white,
- ),
- child: const Text('認証を開始'),
- ),
- ],
- ),
- ],
- ),
+ TextField(
+ controller: _hostController,
+ decoration: const InputDecoration(
+ labelText: 'ホスト',
+ hintText: '例: misskey.io',
),
),
-
- // サーバー情報
- if (_serverInfo != null) ...[
- const SizedBox(height: 16),
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- 'サーバー情報',
- style: TextStyle(
- fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 8),
- Text('認証エンドポイント:\n${_serverInfo!.authorizationEndpoint}'),
- const SizedBox(height: 4),
- Text('トークンエンドポイント:\n${_serverInfo!.tokenEndpoint}'),
- if (_serverInfo!.scopesSupported != null) ...[
- const SizedBox(height: 4),
- Text(
- 'サポートされているスコープ:\n${_serverInfo!.scopesSupported!.join(', ')}'),
- ],
- ],
+ const SizedBox(height: 8),
+ TextField(
+ controller: _clientIdController,
+ decoration: const InputDecoration(
+ labelText: 'クライアントID (URL)',
+ hintText: '例: https://example.com/my-app',
+ ),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _redirectUriController,
+ decoration: const InputDecoration(
+ labelText: 'リダイレクトURI',
+ hintText: '例: https://example.com/redirect',
+ helperText: '要HTTPS',
+ ),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _scopeController,
+ decoration: const InputDecoration(
+ labelText: 'スコープ',
+ hintText: '例: read:account write:notes',
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ ElevatedButton(
+ onPressed: _startAuth,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ foregroundColor: Colors.white,
),
+ child: const Text('認証を開始'),
),
- ),
- ],
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
- // エラーメッセージ
- if (_errorMessage != null) ...[
- const SizedBox(height: 16),
- Card(
- color: Colors.red[50],
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- 'エラー',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- color: Colors.red,
- ),
- ),
- const SizedBox(height: 8),
- Text(
- _errorMessage!,
- style: const TextStyle(color: Colors.red),
- ),
- ],
+ Widget _buildMiAuthForm(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'MiAuth認証設定',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _callbackSchemeController,
+ decoration: const InputDecoration(
+ labelText: 'コールバックスキーム',
+ hintText: '例: misskeyauth',
+ ),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _hostController,
+ decoration: const InputDecoration(
+ labelText: 'ホスト',
+ hintText: '例: misskey.io',
+ ),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _miAppNameController,
+ decoration: const InputDecoration(
+ labelText: 'アプリ名',
+ hintText: '例: Misskey Auth Example',
+ ),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _miPermissionsController,
+ decoration: const InputDecoration(
+ labelText: '権限(空白/カンマ区切り)',
+ hintText: '例: read:account write:notes',
+ ),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _miIconUrlController,
+ decoration: const InputDecoration(
+ labelText: 'アイコンURL(任意)',
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ ElevatedButton(
+ onPressed: _startMiAuth,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ foregroundColor: Colors.white,
),
+ child: const Text('MiAuthで認証'),
),
- ),
- ],
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
- // ローディング
- if (_isLoading) ...[
- const SizedBox(height: 16),
- const Center(
- child: CircularProgressIndicator(),
- ),
+ Widget _buildServerInfoCard() {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'サーバー情報',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ Text('認証エンドポイント:\n${_serverInfo!.authorizationEndpoint}'),
+ const SizedBox(height: 4),
+ Text('トークンエンドポイント:\n${_serverInfo!.tokenEndpoint}'),
+ if (_serverInfo!.scopesSupported != null) ...[
+ const SizedBox(height: 4),
+ Text(
+ 'サポートされているスコープ:\n${_serverInfo!.scopesSupported!.join(', ')}'),
],
],
),
),
);
}
+
+ Widget _buildServerInfoTab(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'サーバー情報の確認',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _hostController,
+ decoration: const InputDecoration(
+ labelText: 'ホスト',
+ hintText: '例: misskey.io',
+ ),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: _checkServerInfo,
+ child: const Text('サーバー情報を確認'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ if (_serverInfo != null) ...[
+ const SizedBox(height: 16),
+ _buildServerInfoCard(),
+ ],
+ ],
+ );
+ }
+
+ // 画面内のエラーカードは廃止(Snackbarのみ使用)
}
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 062ef77..d2e03ed 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -15,6 +15,7 @@ dependencies:
misskey_auth:
path: ../
http: ^1.1.0
+ loader_overlay: ^3.0.0
dev_dependencies:
flutter_test:
diff --git a/lib/misskey_auth.dart b/lib/misskey_auth.dart
index 5d6c4ed..d6f9fea 100644
--- a/lib/misskey_auth.dart
+++ b/lib/misskey_auth.dart
@@ -1,8 +1,10 @@
// Models
export 'src/models/oauth_models.dart';
+export 'src/models/miauth_models.dart';
// API
export 'src/api/misskey_oauth_client.dart';
+export 'src/api/misskey_miauth_client.dart';
// Exceptions
export 'src/exceptions/misskey_auth_exception.dart';
diff --git a/lib/src/api/misskey_miauth_client.dart b/lib/src/api/misskey_miauth_client.dart
new file mode 100644
index 0000000..0287c0b
--- /dev/null
+++ b/lib/src/api/misskey_miauth_client.dart
@@ -0,0 +1,184 @@
+import 'dart:math';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
+
+import '../models/miauth_models.dart';
+import '../exceptions/misskey_auth_exception.dart';
+
+/// Misskey の MiAuth 認証を扱うクライアント
+class MisskeyMiAuthClient {
+ final Dio _dio;
+ final FlutterSecureStorage _storage;
+
+ // ストレージキー(OAuth 実装と揃える)
+ static const _accessTokenKey = 'misskey_access_token';
+ static const _hostKey = 'misskey_host';
+
+ MisskeyMiAuthClient({
+ Dio? dio,
+ FlutterSecureStorage? storage,
+ }) : _dio = dio ?? Dio(),
+ _storage = storage ?? const FlutterSecureStorage();
+
+ /// ランダムなセッション ID を生成(URL セーフな英数字)
+ String generateSessionId({int length = 32}) {
+ const charset =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ final random = Random.secure();
+ return List.generate(length, (_) => charset[random.nextInt(charset.length)])
+ .join();
+ }
+
+ /// MiAuth 認証を開始し、成功すればアクセストークンを返す
+ Future authenticate(MisskeyMiAuthConfig config) async {
+ try {
+ // 1. セッション ID を生成
+ final sessionId = generateSessionId();
+ if (kDebugMode) {
+ print('MiAuth セッション: $sessionId');
+ }
+
+ // 2. 認証 URL を構築
+ final permissions = config.permissions.join(',');
+ final query = {
+ 'name': config.appName,
+ 'callback': config.callbackUrl,
+ if (permissions.isNotEmpty) 'permission': permissions,
+ if (config.iconUrl != null && config.iconUrl!.isNotEmpty)
+ 'icon': config.iconUrl!,
+ };
+
+ final authUri = Uri(
+ scheme: 'https',
+ host: config.host,
+ path: '/miauth/$sessionId',
+ queryParameters: query,
+ );
+
+ if (kDebugMode) {
+ print('MiAuth URL: $authUri');
+ }
+
+ // 3. ブラウザで認証ページを開く
+ late final String result;
+ try {
+ result = await FlutterWebAuth2.authenticate(
+ url: authUri.toString(),
+ callbackUrlScheme: config.callbackScheme,
+ );
+ } on PlatformException catch (e) {
+ final code = (e.code).toLowerCase();
+ if (code.contains('cancel')) {
+ throw const UserCancelledException();
+ }
+ if (e.message != null &&
+ e.message!.toLowerCase().contains('callback')) {
+ throw CallbackSchemeErrorException(
+ details: e.message, originalException: e);
+ }
+ throw AuthorizationLaunchException(
+ details: e.message, originalException: e);
+ } catch (e) {
+ if (e is MisskeyAuthException) rethrow;
+ throw AuthorizationLaunchException(details: e.toString());
+ }
+
+ if (kDebugMode) {
+ print('MiAuth コールバック URL: $result');
+ }
+
+ // 4. 許可後にチェック API を叩いてトークンを取得
+ final checkUrl = Uri(
+ scheme: 'https',
+ host: config.host,
+ path: '/api/miauth/$sessionId/check',
+ );
+
+ final response = await _dio.post(
+ checkUrl.toString(),
+ options: Options(contentType: 'application/json'),
+ data: {},
+ );
+
+ if (response.statusCode != 200) {
+ final status = response.statusCode;
+ String details = 'status=$status';
+ final data = response.data;
+ if (data is Map) {
+ final err = data['error'] ?? data['message'];
+ if (err != null) {
+ details = '$details, $err';
+ }
+ }
+ // セッション不正や期限切れなどをある程度推定
+ if (status == 404 || status == 410) {
+ throw MiAuthSessionInvalidException(details: details);
+ }
+ throw MiAuthCheckFailedException(details: details);
+ }
+
+ final body = response.data as Map;
+ final check = MiAuthCheckResponse.fromJson(body);
+
+ if (!check.ok || check.token == null || check.token!.isEmpty) {
+ throw const MiAuthDeniedException();
+ }
+
+ // 5. トークン保存
+ try {
+ await _storage.write(key: _hostKey, value: config.host);
+ await _storage.write(key: _accessTokenKey, value: check.token);
+ } on PlatformException catch (e) {
+ throw SecureStorageException(details: e.message, originalException: e);
+ } catch (e) {
+ if (e is MisskeyAuthException) rethrow;
+ throw SecureStorageException(details: e.toString());
+ }
+
+ if (kDebugMode) {
+ print('MiAuth 成功');
+ }
+ return MiAuthTokenResponse(token: check.token!, user: check.user);
+ } on MisskeyAuthException {
+ rethrow;
+ } on DioException catch (e) {
+ throw NetworkException(details: e.message, originalException: e);
+ } on PlatformException catch (e) {
+ final code = (e.code).toLowerCase();
+ if (code.contains('cancel')) {
+ throw const UserCancelledException();
+ }
+ throw AuthorizationLaunchException(
+ details: e.message, originalException: e);
+ } on FormatException catch (e) {
+ throw ResponseParseException(details: e.message, originalException: e);
+ } catch (e) {
+ if (kDebugMode) {
+ print('MiAuth エラー: $e');
+ }
+ throw MisskeyAuthException('MiAuthでエラーが発生しました', details: e.toString());
+ }
+ }
+
+ /// 保存された MiAuth トークンを取得
+ Future getStoredAccessToken() async {
+ try {
+ return _storage.read(key: _accessTokenKey);
+ } on PlatformException catch (e) {
+ throw SecureStorageException(details: e.message, originalException: e);
+ }
+ }
+
+ /// MiAuth の保存情報を削除
+ Future clearTokens() async {
+ try {
+ await _storage.deleteAll();
+ } on PlatformException catch (e) {
+ throw SecureStorageException(details: e.message, originalException: e);
+ }
+ }
+}
diff --git a/lib/src/api/misskey_oauth_client.dart b/lib/src/api/misskey_oauth_client.dart
index 74b0912..3880c4a 100644
--- a/lib/src/api/misskey_oauth_client.dart
+++ b/lib/src/api/misskey_oauth_client.dart
@@ -4,8 +4,10 @@ import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import '../models/oauth_models.dart';
+import '../exceptions/misskey_auth_exception.dart';
/// MisskeyのOAuth認証を管理するクライアント
class MisskeyOAuthClient {
@@ -34,13 +36,24 @@ class MisskeyOAuthClient {
if (response.statusCode == 200) {
return OAuthServerInfo.fromJson(response.data);
}
- return null;
+ if (response.statusCode == 404 || response.statusCode == 501) {
+ // 非対応と判断
+ return null;
+ }
+ // その他のステータスはサーバー側の問題として扱う
+ throw ServerInfoException('OAuth情報の取得に失敗しました: ${response.statusCode}');
+ } on DioException catch (e) {
+ if (e.response?.statusCode == 404 || e.response?.statusCode == 501) {
+ return null; // 非対応
+ }
+ throw NetworkException(details: e.message, originalException: e);
+ } on FormatException catch (e) {
+ throw ResponseParseException(details: e.message, originalException: e);
} catch (e) {
- // OAuth非対応の場合はnullを返す
if (kDebugMode) {
print('OAuth情報取得エラー: $e');
}
- return null;
+ throw ServerInfoException('OAuth情報の取得に失敗しました: $e');
}
}
@@ -76,7 +89,7 @@ class MisskeyOAuthClient {
}
final serverInfo = await getOAuthServerInfo(config.host);
if (serverInfo == null) {
- throw Exception('このサーバーはOAuth認証に対応していません');
+ throw OAuthNotSupportedException(config.host);
}
if (kDebugMode) {
print('認証エンドポイント: ${serverInfo.authorizationEndpoint}');
@@ -123,10 +136,30 @@ class MisskeyOAuthClient {
print('コールバックURLスキーム: $callbackUrlScheme');
}
- final result = await FlutterWebAuth2.authenticate(
- url: authUrl.toString(),
- callbackUrlScheme: callbackUrlScheme,
- );
+ late final String result;
+ try {
+ result = await FlutterWebAuth2.authenticate(
+ url: authUrl.toString(),
+ callbackUrlScheme: callbackUrlScheme,
+ );
+ } on PlatformException catch (e) {
+ // FlutterWebAuth2 の代表的なケースをマッピング
+ final code = (e.code).toLowerCase();
+ if (code.contains('cancel')) {
+ throw const UserCancelledException();
+ }
+ // コールバックスキーム不一致/未設定の可能性
+ if (e.message != null &&
+ e.message!.toLowerCase().contains('callback')) {
+ throw CallbackSchemeErrorException(
+ details: e.message, originalException: e);
+ }
+ throw AuthorizationLaunchException(
+ details: e.message, originalException: e);
+ } catch (e) {
+ if (e is MisskeyAuthException) rethrow;
+ throw AuthorizationLaunchException(details: e.toString());
+ }
if (kDebugMode) {
print('認証結果URL: $result');
@@ -134,6 +167,16 @@ class MisskeyOAuthClient {
// 5. コールバックURLからパラメータを取得
final uri = Uri.parse(result);
+ // 認可サーバーからのエラー(RFC6749)
+ final authError = uri.queryParameters['error'];
+ if (authError != null && authError.isNotEmpty) {
+ final desc = uri.queryParameters['error_description'];
+ final errMsg = desc == null || desc.isEmpty
+ ? 'error=$authError'
+ : 'error=$authError, description=$desc';
+ throw AuthorizationServerErrorException(details: errMsg);
+ }
+
final code = uri.queryParameters['code'];
final returnedState = uri.queryParameters['state'];
@@ -144,11 +187,11 @@ class MisskeyOAuthClient {
// 6. stateを検証
if (returnedState != state) {
- throw Exception('stateが一致しません');
+ throw const StateMismatchException();
}
if (code == null) {
- throw Exception('認証コードが取得できませんでした');
+ throw const AuthorizationCodeMissingException();
}
// 7. 認証コードをトークンと交換
@@ -165,22 +208,42 @@ class MisskeyOAuthClient {
);
// 8. トークンを保存
- await _saveTokens(
- host: config.host,
- accessToken: tokenResponse.accessToken,
- refreshToken: tokenResponse.refreshToken,
- expiresIn: tokenResponse.expiresIn,
- );
+ try {
+ await _saveTokens(
+ host: config.host,
+ accessToken: tokenResponse.accessToken,
+ refreshToken: tokenResponse.refreshToken,
+ expiresIn: tokenResponse.expiresIn,
+ );
+ } on PlatformException catch (e) {
+ throw SecureStorageException(details: e.message, originalException: e);
+ } catch (e) {
+ if (e is MisskeyAuthException) rethrow;
+ throw SecureStorageException(details: e.toString());
+ }
if (kDebugMode) {
print('認証成功!');
}
return tokenResponse;
+ } on MisskeyAuthException {
+ rethrow;
+ } on DioException catch (e) {
+ // ネットワーク層の例外
+ throw NetworkException(details: e.message, originalException: e);
+ } on PlatformException catch (e) {
+ final code = (e.code).toLowerCase();
+ if (code.contains('cancel')) {
+ throw const UserCancelledException();
+ }
+ throw AuthorizationLaunchException(
+ details: e.message, originalException: e);
} catch (e) {
if (kDebugMode) {
print('認証エラー: $e');
}
- rethrow;
+ // 想定外はベース例外に包む
+ throw MisskeyAuthException(e.toString());
}
}
@@ -212,14 +275,43 @@ class MisskeyOAuthClient {
if (response.statusCode == 200) {
return OAuthTokenResponse.fromJson(response.data);
}
- throw Exception('トークン交換に失敗しました: ${response.statusCode}');
- } catch (e) {
- if (e is DioException) {
- if (kDebugMode) {
- print('DioException: ${e.response?.data}');
+ final status = response.statusCode;
+ String message = 'トークン交換に失敗しました: $status';
+ // RFC準拠のエラーフィールドがあれば詳細に含める
+ final data = response.data;
+ if (data is Map) {
+ final err = data['error'];
+ final desc = data['error_description'];
+ if (err != null) {
+ message =
+ '$message (error=$err${desc != null ? ', description=$desc' : ''})';
+ }
+ }
+ throw TokenExchangeException(message);
+ } on DioException catch (e) {
+ if (kDebugMode) {
+ print('DioException: ${e.response?.data}');
+ }
+ if (e.response != null) {
+ final status = e.response?.statusCode;
+ String message = 'トークン交換に失敗しました: $status';
+ final data = e.response?.data;
+ if (data is Map) {
+ final err = data['error'];
+ final desc = data['error_description'];
+ if (err != null) {
+ message =
+ '$message (error=$err${desc != null ? ', description=$desc' : ''})';
+ }
}
+ throw TokenExchangeException(message);
}
- throw Exception('トークン交換中にエラーが発生しました: $e');
+ // レスポンスが無い=ネットワーク層の失敗
+ throw NetworkException(details: e.message, originalException: e);
+ } on FormatException catch (e) {
+ throw ResponseParseException(details: e.message, originalException: e);
+ } catch (e) {
+ throw MisskeyAuthException('トークン交換中にエラーが発生しました', details: e.toString());
}
}
@@ -246,11 +338,19 @@ class MisskeyOAuthClient {
/// 保存されたアクセストークンを取得
Future getStoredAccessToken() async {
- return await _storage.read(key: _accessTokenKey);
+ try {
+ return await _storage.read(key: _accessTokenKey);
+ } on PlatformException catch (e) {
+ throw SecureStorageException(details: e.message, originalException: e);
+ }
}
/// トークンをクリア
Future clearTokens() async {
- await _storage.deleteAll();
+ try {
+ await _storage.deleteAll();
+ } on PlatformException catch (e) {
+ throw SecureStorageException(details: e.message, originalException: e);
+ }
}
}
diff --git a/lib/src/exceptions/misskey_auth_exception.dart b/lib/src/exceptions/misskey_auth_exception.dart
index d495f07..2f3b74b 100644
--- a/lib/src/exceptions/misskey_auth_exception.dart
+++ b/lib/src/exceptions/misskey_auth_exception.dart
@@ -44,3 +44,101 @@ class TokenExchangeException extends MisskeyAuthException {
class ServerInfoException extends MisskeyAuthException {
const ServerInfoException(super.message);
}
+
+/// コールバックURLスキーム(カスタムスキーム)が未設定または不一致の場合の例外
+class CallbackSchemeErrorException extends MisskeyAuthException {
+ const CallbackSchemeErrorException(
+ {String? details, Exception? originalException})
+ : super('The callback URL scheme is not set or does not match.',
+ details: details, originalException: originalException);
+}
+
+/// 認証UI(ブラウザ等)の起動に失敗した場合の例外
+class AuthorizationLaunchException extends MisskeyAuthException {
+ const AuthorizationLaunchException(
+ {String? details, Exception? originalException})
+ : super('Failed to display the authentication screen.',
+ details: details, originalException: originalException);
+}
+
+/// ユーザーが認証をキャンセルした場合の例外
+class UserCancelledException extends MisskeyAuthException {
+ const UserCancelledException({String? details, Exception? originalException})
+ : super('The user canceled the authentication.',
+ details: details, originalException: originalException);
+}
+
+/// ネットワーク層での例外(タイムアウト/オフライン/SSL等)
+class NetworkException extends MisskeyAuthException {
+ const NetworkException({String? details, Exception? originalException})
+ : super('A network error has occurred.',
+ details: details, originalException: originalException);
+}
+
+/// サーバーレスポンスのパースに失敗した場合の例外
+class ResponseParseException extends MisskeyAuthException {
+ const ResponseParseException({String? details, Exception? originalException})
+ : super('Failed to parse the server response.',
+ details: details, originalException: originalException);
+}
+
+/// セキュアストレージの操作に失敗した場合の例外
+class SecureStorageException extends MisskeyAuthException {
+ const SecureStorageException({String? details, Exception? originalException})
+ : super('Failed to operate the secure storage.',
+ details: details, originalException: originalException);
+}
+
+/// OAuthのstate検証に失敗した場合の例外
+class StateMismatchException extends MisskeyAuthException {
+ const StateMismatchException({String? details, Exception? originalException})
+ : super('The state does not match.',
+ details: details, originalException: originalException);
+}
+
+/// OAuthの認可コードが取得できなかった場合の例外
+class AuthorizationCodeMissingException extends MisskeyAuthException {
+ const AuthorizationCodeMissingException(
+ {String? details, Exception? originalException})
+ : super('Failed to get the authorization code.',
+ details: details, originalException: originalException);
+}
+
+/// 認可サーバーがエラーを返した場合(error, error_description など)
+class AuthorizationServerErrorException extends MisskeyAuthException {
+ const AuthorizationServerErrorException(
+ {String? details, Exception? originalException})
+ : super('The authorization server returned an error.',
+ details: details, originalException: originalException);
+}
+
+/// MiAuth がユーザーによって拒否/キャンセルされた場合の例外
+class MiAuthDeniedException extends MisskeyAuthException {
+ const MiAuthDeniedException({String? details, Exception? originalException})
+ : super('MiAuth authentication was canceled/rejected.',
+ details: details, originalException: originalException);
+}
+
+/// MiAuth のチェックAPIが失敗(非200)した場合の例外
+class MiAuthCheckFailedException extends MisskeyAuthException {
+ const MiAuthCheckFailedException(
+ {String? details, Exception? originalException})
+ : super('MiAuth check failed.',
+ details: details, originalException: originalException);
+}
+
+/// MiAuth のセッションが見つからない/期限切れなどで無効な場合の例外
+class MiAuthSessionInvalidException extends MisskeyAuthException {
+ const MiAuthSessionInvalidException(
+ {String? details, Exception? originalException})
+ : super('MiAuth session is invalid or expired.',
+ details: details, originalException: originalException);
+}
+
+/// サーバーが MiAuth に対応していない可能性がある場合の例外(必要に応じて使用)
+class MiAuthNotSupportedException extends MisskeyAuthException {
+ const MiAuthNotSupportedException(
+ {String? details, Exception? originalException})
+ : super('This server may not support MiAuth.',
+ details: details, originalException: originalException);
+}
diff --git a/lib/src/models/auth_config.dart b/lib/src/models/auth_config.dart
deleted file mode 100644
index 83e8d9e..0000000
--- a/lib/src/models/auth_config.dart
+++ /dev/null
@@ -1,76 +0,0 @@
-import '../exceptions/misskey_auth_exception.dart';
-
-/// Misskey認証の設定を表すクラス
-class AuthConfig {
- /// Misskeyサーバーのホスト(例: misskey.io)
- final String host;
-
- /// アプリケーションのクライアントID
- final String clientId;
-
- /// アプリケーションのクライアントシークレット
- final String clientSecret;
-
- /// 認証完了後のリダイレクトURI
- final String redirectUri;
-
- /// 要求するスコープ(カンマ区切り)
- final String scopes;
-
- /// アプリケーション名
- final String appName;
-
- /// アプリケーションの説明
- final String? appDescription;
-
- /// アプリケーションのアイコンURL
- final String? appIconUrl;
-
- /// アプリケーションの権限
- final List? permissions;
-
- const AuthConfig({
- required this.host,
- required this.clientId,
- required this.clientSecret,
- required this.redirectUri,
- this.scopes = 'read:account,write:notes',
- required this.appName,
- this.appDescription,
- this.appIconUrl,
- this.permissions,
- });
-
- /// 設定が有効かどうかを検証する
- bool get isValid {
- return host.isNotEmpty &&
- clientId.isNotEmpty &&
- clientSecret.isNotEmpty &&
- redirectUri.isNotEmpty &&
- appName.isNotEmpty;
- }
-
- /// 設定の検証を行い、無効な場合は例外を投げる
- void validate() {
- if (!isValid) {
- throw const InvalidAuthConfigException(
- '認証設定が無効です。host、clientId、clientSecret、redirectUri、appNameは必須です。',
- );
- }
- }
-
- /// サーバーのベースURLを取得
- String get serverUrl {
- return 'https://$host';
- }
-
- /// OAuth認証エンドポイントのURLを取得
- String get oauthWellKnownUrl {
- return '$serverUrl/.well-known/oauth-authorization-server';
- }
-
- /// MiAuth認証エンドポイントのURLを取得
- String get miauthUrl {
- return '$serverUrl/miauth';
- }
-}
diff --git a/lib/src/models/auth_result.dart b/lib/src/models/auth_result.dart
deleted file mode 100644
index 2875918..0000000
--- a/lib/src/models/auth_result.dart
+++ /dev/null
@@ -1,98 +0,0 @@
-/// Misskey認証の結果を表すクラス
-class AuthResult {
- /// アクセストークン
- final String accessToken;
-
- /// リフレッシュトークン(OAuth認証の場合)
- final String? refreshToken;
-
- /// トークンの有効期限(秒)
- final int? expiresIn;
-
- /// 認証方式(oauth または miauth)
- final String authType;
-
- /// ユーザー情報
- final Map? userInfo;
-
- /// 認証が成功したかどうか
- final bool isSuccess;
-
- /// エラーメッセージ(認証が失敗した場合)
- final String? errorMessage;
-
- const AuthResult({
- required this.accessToken,
- this.refreshToken,
- this.expiresIn,
- required this.authType,
- this.userInfo,
- this.isSuccess = true,
- this.errorMessage,
- });
-
- /// 認証失敗時の結果を作成するファクトリメソッド
- factory AuthResult.failure({
- required String errorMessage,
- required String authType,
- }) {
- return AuthResult(
- accessToken: '',
- authType: authType,
- isSuccess: false,
- errorMessage: errorMessage,
- );
- }
-
- /// OAuth認証成功時の結果を作成するファクトリメソッド
- factory AuthResult.oauthSuccess({
- required String accessToken,
- String? refreshToken,
- int? expiresIn,
- Map? userInfo,
- }) {
- return AuthResult(
- accessToken: accessToken,
- refreshToken: refreshToken,
- expiresIn: expiresIn,
- authType: 'oauth',
- userInfo: userInfo,
- isSuccess: true,
- );
- }
-
- /// MiAuth認証成功時の結果を作成するファクトリメソッド
- factory AuthResult.miauthSuccess({
- required String accessToken,
- Map? userInfo,
- }) {
- return AuthResult(
- accessToken: accessToken,
- authType: 'miauth',
- userInfo: userInfo,
- isSuccess: true,
- );
- }
-
- /// OAuth認証開始時の結果を作成するファクトリメソッド
- factory AuthResult.oauthStarted({
- required String authUrl,
- required String codeVerifier,
- }) {
- return AuthResult(
- accessToken: '',
- authType: 'oauth',
- isSuccess: true,
- errorMessage: null,
- );
- }
-
- @override
- String toString() {
- if (isSuccess) {
- return 'AuthResult(success: true, type: $authType, token: ${accessToken.substring(0, 10)}...)';
- } else {
- return 'AuthResult(success: false, error: $errorMessage)';
- }
- }
-}
diff --git a/lib/src/models/miauth_models.dart b/lib/src/models/miauth_models.dart
new file mode 100644
index 0000000..626ef43
--- /dev/null
+++ b/lib/src/models/miauth_models.dart
@@ -0,0 +1,66 @@
+/// MiAuth 認証の設定を表すクラス
+class MisskeyMiAuthConfig {
+ /// Misskey サーバーのホスト(例: misskey.io)
+ final String host;
+
+ /// アプリケーション名(ユーザーに表示)
+ final String appName;
+
+ /// カスタム URL スキーム(例: misskeyauth)
+ final String callbackScheme;
+
+ /// 要求する権限一覧(例: ['read:account', 'write:notes'])
+ final List permissions;
+
+ /// アプリアイコンの URL(任意)
+ final String? iconUrl;
+
+ const MisskeyMiAuthConfig({
+ required this.host,
+ required this.appName,
+ required this.callbackScheme,
+ this.permissions = const [],
+ this.iconUrl,
+ });
+
+ /// コールバック URL(例: misskeyauth://oauth/callback)
+ String get callbackUrl {
+ // スキームのみで十分(iOS/Android ともにスキーム一致で復帰可能)
+ return '$callbackScheme://';
+ }
+}
+
+/// MiAuth チェック API のレスポンス
+class MiAuthCheckResponse {
+ final bool ok;
+ final String? token;
+ final Map? user;
+
+ const MiAuthCheckResponse({
+ required this.ok,
+ this.token,
+ this.user,
+ });
+
+ factory MiAuthCheckResponse.fromJson(Map json) {
+ return MiAuthCheckResponse(
+ ok: json['ok'] == true,
+ token: json['token'] as String?,
+ user: json['user'] as Map?,
+ );
+ }
+}
+
+/// MiAuth 成功時の結果(利用側が扱いやすい形)
+class MiAuthTokenResponse {
+ /// アクセストークン
+ final String token;
+
+ /// 付随するユーザー情報(任意)
+ final Map? user;
+
+ const MiAuthTokenResponse({
+ required this.token,
+ this.user,
+ });
+}
diff --git a/lib/src/models/misskey_server_info.dart b/lib/src/models/misskey_server_info.dart
deleted file mode 100644
index 980ef75..0000000
--- a/lib/src/models/misskey_server_info.dart
+++ /dev/null
@@ -1,69 +0,0 @@
-/// Misskeyサーバーの情報を表すクラス
-class MisskeyServerInfo {
- /// サーバーのホスト
- final String host;
-
- /// OAuth 2.0がサポートされているかどうか
- final bool supportsOAuth;
-
- /// OAuth認証エンドポイント
- final String? authorizationEndpoint;
-
- /// OAuthトークンエンドポイント
- final String? tokenEndpoint;
-
- /// サーバーのバージョン情報
- final String? version;
-
- /// サーバーの名前
- final String? name;
-
- /// サーバーの説明
- final String? description;
-
- const MisskeyServerInfo({
- required this.host,
- required this.supportsOAuth,
- this.authorizationEndpoint,
- this.tokenEndpoint,
- this.version,
- this.name,
- this.description,
- });
-
- /// OAuth認証が利用可能かどうかを判定
- bool get canUseOAuth {
- return supportsOAuth &&
- authorizationEndpoint != null &&
- tokenEndpoint != null;
- }
-
- /// サーバー情報をJSONから作成するファクトリメソッド
- factory MisskeyServerInfo.fromJson(String host, Map json) {
- final hasOAuthEndpoints = json.containsKey('authorization_endpoint') &&
- json.containsKey('token_endpoint');
-
- return MisskeyServerInfo(
- host: host,
- supportsOAuth: hasOAuthEndpoints,
- authorizationEndpoint: json['authorization_endpoint'] as String?,
- tokenEndpoint: json['token_endpoint'] as String?,
- version: json['version'] as String?,
- name: json['name'] as String?,
- description: json['description'] as String?,
- );
- }
-
- /// デフォルトのサーバー情報を作成するファクトリメソッド
- factory MisskeyServerInfo.defaultInfo(String host) {
- return MisskeyServerInfo(
- host: host,
- supportsOAuth: false,
- );
- }
-
- @override
- String toString() {
- return 'MisskeyServerInfo(host: $host, oauth: $supportsOAuth)';
- }
-}
diff --git a/pubspec.yaml b/pubspec.yaml
index e9616b0..50ed6d7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -31,7 +31,7 @@ topics:
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 0.1.1-beta
+version: 0.1.2-beta
environment:
sdk: ">=3.5.0 <4.0.0"