Skip to content
This repository was archived by the owner on Aug 5, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/at_auth/lib/at_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ export 'src/enroll/at_enrollment_base.dart';
export 'src/enroll/at_enrollment_response.dart';
// The abstract class contains fields related to enrollment request
export 'src/enroll/base_enrollment_request.dart';

/// The class contains fields to submit enrollment request for APKAM keys which generate keys for
/// an application with restricted access to the namespaces.
export 'src/enroll/enrollment_request.dart';

/// This class serves as the entity responsible for either approving or denying an enrollment request
export 'src/enroll/enrollment_request_decision.dart';

/// The class stores enrollment request details. It notifies the approving app upon receiving a
/// request from the requesting app, for approval or denial.
export 'src/enroll/enrollment_server_response.dart';
export 'src/exception/at_auth_exceptions.dart';
export 'src/keys/at_auth_keys.dart';
export 'src/onboard/at_onboarding_request.dart';
export 'src/onboard/at_onboarding_response.dart';
export 'src/registrar/registrar.dart';

/// Global constant to access [AtAuthInterface].
///
Expand Down
4 changes: 4 additions & 0 deletions packages/at_auth/lib/src/registrar/registrar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export './registrar_service_base.dart';
export './registrar_service_impl.dart';
export './registrar_exception.dart';
export './registrar_validate_person_response.dart';
12 changes: 12 additions & 0 deletions packages/at_auth/lib/src/registrar/registrar_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class RegistrarException implements Exception {
RegistrarException({required this.error, required this.message});

/// Error message. Internal use only.
final String error;

/// Helpful message to display to end-user.
final String message;

@override
String toString() => 'RegistrarException - Error: $error, Message: $message';
}
29 changes: 29 additions & 0 deletions packages/at_auth/lib/src/registrar/registrar_service_base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'registrar_validate_person_response.dart';

abstract interface class RegistrarServiceBase {
/// Gets a free atSign from the registrar.
Future<String> getFreeAtSign();

/// This request is used to register an atSign by assigning it to an email address.
/// This request accepts an [atSign] and an [email] address.
/// A one-time password will be sent to the email address provided.
Future<void> registerPerson({required String atSign, required String email});

/// This request is used to validate the person registering for an atSign by verifying the one-time password that was
/// sent to the email address provided. The one-time password is valid for 15 minutes.
Future<ValidatePersonResponse> validatePerson(
{required String atSign, required String email, required String otp});

/// This request is used to check whether the person attempting to activate an atSign is its rightful owner.
/// The request takes an atSign and sends a one-time password to the email address and/or phone number associated
/// with that atSign.
Future<void> authenticateAtSign({required String atSign});

/// This request is used to check whether the person attempting to activate an atSign is its rightful owner.
/// The request takes an atSign and a one-time password then provides the cramkey once verified.
Future<String> authenticateAtSignAndActivate(
{required String atSign, required String otp});

/// Weblink to registrar service.
String get registrarUrlSite;
}
267 changes: 267 additions & 0 deletions packages/at_auth/lib/src/registrar/registrar_service_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import 'dart:convert';
import 'dart:io';

import 'package:at_auth/src/registrar/registrar_service_base.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';

import 'registrar_validate_person_response.dart';
import 'registrar_exception.dart';

class RegistrarServiceImpl implements RegistrarServiceBase {
RegistrarServiceImpl.dev({
required this.apiKey,
int version = 3,
bool bypassCertificate = true,
this.maxRetries = 3,
this.retryDelayMs = 2000,
}) : _httpClient =
IOClient(_createHttpClient(bypassCertificate: bypassCertificate)),
_apiPath = '/api/app/v$version',
rootDomain = 'my.atsign.wtf',
weblink = 'https://atsign.wtf',
assert(version >= 1 && version <= 3, 'Version must be between 1 and 3'),
assert(maxRetries > 0, 'Max retries must be greater than 0'),
assert(retryDelayMs > 0, 'Retry delay must be greater than 0');

RegistrarServiceImpl.prod({
required this.apiKey,
int version = 3,
this.maxRetries = 3,
this.retryDelayMs = 2000,
}) : _httpClient = IOClient(_createHttpClient(bypassCertificate: false)),
_apiPath = '/api/app/v$version',
rootDomain = 'my.atsign.com',
weblink = 'https://atsign.com',
assert(version >= 1 && version <= 3, 'Version must be between 1 and 3'),
assert(maxRetries > 0, 'Max retries must be greater than 0'),
assert(retryDelayMs > 0, 'Retry delay must be greater than 0');

RegistrarServiceImpl.custom({
required this.apiKey,
required this.rootDomain,
required this.weblink,
required bool bypassCertificate,
int version = 3,
this.maxRetries = 3,
this.retryDelayMs = 2000,
IOClient? httpClient,
}) : _httpClient = httpClient ??
IOClient(_createHttpClient(bypassCertificate: bypassCertificate)),
_apiPath = '/api/app/v$version',
assert(version >= 1 && version <= 3, 'Version must be between 1 and 3'),
assert(maxRetries > 0, 'Max retries must be greater than 0'),
assert(retryDelayMs > 0, 'Retry delay must be greater than 0');

final IOClient _httpClient;
final String rootDomain;
final String _apiPath;
final int maxRetries;
final int retryDelayMs;
final String apiKey;
final String weblink;

/// Creates an `HttpClient` with a certificate bypass in non-production environments.
static HttpClient _createHttpClient({required bool bypassCertificate}) {
final client = HttpClient();
if (bypassCertificate) {
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
return client;
}

/// Sends a POST request to the registrar API with retry logic.
Future<http.Response> _postRequest(
String path, Map<String, String?>? data) async {
final url = Uri.https(rootDomain, '$_apiPath/$path');
final body = data != null ? json.encode(data) : null;

return _retryRequest(() async {
final response = await _httpClient.post(
url,
body: body,
headers: {
'Authorization': apiKey,
'Content-Type': 'application/json',
},
);

if (response.statusCode >= 200 && response.statusCode < 300) {
return response;
}
throw RegistrarException(
error: 'Request failed: ${response.statusCode} - ${response.body}',
message: 'Request failed with status code ${response.statusCode}',
);
});
}

/// Sends a GET request to the registrar API with retry logic.
Future<http.Response> _getRequest(String path) async {
final url = Uri.https(rootDomain, '$_apiPath/$path');

return _retryRequest(() async {
final response = await _httpClient.get(
url,
headers: {
'Authorization': apiKey,
'Content-Type': 'application/json',
},
);

if (response.statusCode >= 200 && response.statusCode < 300) {
return response;
}
throw RegistrarException(
error: '${response.statusCode} - ${response.body}',
message: 'Request failed with status code ${response.statusCode}',
);
});
}

/// Retries a function `_maxRetries` times with a delay.
Future<T> _retryRequest<T>(Future<T> Function() request) async {
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
return await request();
} catch (e) {
// Would be cleaner to move this logic outside the for loop but
// keeping this logic so that the error can be reported in exception.
if (attempt == maxRetries - 1) {
final message = e is RegistrarException ? e.message : e.toString();
throw RegistrarException(
error: 'Request failed after $maxRetries attempts: $message',
message: message,
);
}
await Future.delayed(Duration(milliseconds: retryDelayMs));
}
}
throw Exception('Unexpected error in _retryRequest');
}

@override
Future<String> getFreeAtSign() async {
final response = await _getRequest('get-free-atsign/');

final body = jsonDecode(response.body);
if (body is Map<String, dynamic> && body['data'] is Map<String, dynamic>) {
final data = body['data'] as Map<String, dynamic>;
if (data['atsign'] is String) {
return data['atsign'] as String;
}
}
throw RegistrarException(
error: 'Invalid response format: $body',
message: 'Invalid response format',
);
}

@override
Future<void> registerPerson(
{required String atSign, required String email}) async {
final response = await _postRequest('register-person/', {
'atsign': atSign,
'email': email,
});

final body = jsonDecode(response.body);
if (body is Map<String, dynamic> && body['message'] is String) {
if (body['message'] == 'Sent Successfully') {
return;
} else {
throw RegistrarException(
error: body['message'],
message: body['message'],
);
}
}
throw RegistrarException(
error: 'Invalid response format: $body',
message: 'Invalid response format',
);
}

@override
Future<ValidatePersonResponse> validatePerson({
required String atSign,
required String email,
required String otp,
}) async {
final response = await _postRequest('validate-person/', {
'atsign': atSign,
'email': email,
'otp': otp,
});

final body = jsonDecode(response.body);

if (body is Map<String, dynamic>) {
return ValidatePersonResponse.fromJson(body);
}

throw RegistrarException(
error: 'Invalid response format: $body',
message: 'Invalid response format',
);
}

@override
Future<void> authenticateAtSign({required String atSign}) async {
final response = await _postRequest('authenticate/atsign', {
'atsign': atSign,
});

final body = jsonDecode(response.body);

if (body is Map<String, dynamic>) {
final message = body['message'];
if (message == 'Sent Successfully') {
return;
} else {
throw RegistrarException(
error: 'authenticate/atsign failed: $message',
message: message,
);
}
}

throw RegistrarException(
error: 'Invalid response format: $body',
message: 'Invalid response format',
);
}

@override
Future<String> authenticateAtSignAndActivate(
{required String atSign, required String otp}) async {
final response = await _postRequest('authenticate/atsign/activate', {
'atsign': atSign,
'otp': otp,
});

final body = jsonDecode(response.body);

if (body is Map<String, dynamic>) {
final cramKey = (body['cramkey'] as String?)?.split(':')[1];
if (cramKey != null) {
return cramKey;
} else {
final message = body['message'] as String;
throw RegistrarException(
error: 'authenticate/atsign/activate failed: $message',
message: message,
);
}
}

throw RegistrarException(
error: 'Invalid response format: $body',
message: 'Invalid response format',
);
}

@override
String get registrarUrlSite => weblink;
}
Loading