@siran/auth-core 0.9.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/COMPOSITE_AUTH_SERVICE.md +104 -0
  3. package/package.json +1 -1
  4. package/src/application/policies/account-already-exists.policy.ts +23 -0
  5. package/src/application/policies/magic-link-token.policy.ts +23 -0
  6. package/src/application/policies/oauth-code.policy.ts +23 -0
  7. package/src/application/policies/oauth-provider.policy.ts +29 -0
  8. package/src/application/policies/otp-code-format.policy.ts +23 -0
  9. package/src/application/policies/otp-code.policy.ts +23 -0
  10. package/src/application/policies/otp-identifier.policy.ts +23 -0
  11. package/src/application/policies/password-identifier.policy.ts +23 -0
  12. package/src/application/policies/password-min-length.policy.ts +29 -0
  13. package/src/application/policies/password-present.policy.ts +23 -0
  14. package/src/application/policies/password-strength.policy.ts +27 -0
  15. package/src/application/ports/auth-preference-service.port.ts +5 -0
  16. package/src/application/ports/auth-service.port.ts +53 -14
  17. package/src/application/ports/user-existence.port.ts +3 -0
  18. package/src/application/services/composite-auth.service.ts +96 -0
  19. package/src/auth-engine.factory.ts +35 -10
  20. package/src/index.ts +9 -0
  21. package/tests/application/policies/magic-link-token.policy.test.ts +74 -0
  22. package/tests/application/policies/oauth-code.policy.test.ts +79 -0
  23. package/tests/application/policies/oauth-provider.policy.test.ts +76 -0
  24. package/tests/application/policies/otp-code-format.policy.test.ts +90 -0
  25. package/tests/application/policies/otp-code.policy.test.ts +55 -0
  26. package/tests/application/policies/otp-identifier.policy.test.ts +55 -0
  27. package/tests/application/policies/password-identifier.policy.test.ts +55 -0
  28. package/tests/application/policies/password-min-length.policy.test.ts +66 -0
  29. package/tests/application/policies/password-present.policy.test.ts +55 -0
  30. package/tests/application/policies/password-strength.policy.test.ts +66 -0
  31. package/tests/application/ports/auth-service.port.mock.ts +0 -2
  32. package/tests/application/services/composite-auth.mock.ts +18 -0
  33. package/tests/application/services/composite-auth.test.ts +162 -0
  34. package/tsconfig.lib.json +3 -2
  35. package/src/application/services/account-already-exists.service.ts +0 -20
package/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ ## 0.14.0 (2026-01-25)
2
+
3
+ ### 🚀 Features
4
+
5
+ - update index.ts to export new application ports ([de8d09e](https://github.com/narisraz/auth/commit/de8d09e))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Naris Razafimahatratra
10
+
11
+ ## 0.13.0 (2026-01-25)
12
+
13
+ ### 🚀 Features
14
+
15
+ - add comprehensive tests for authentication policies ([5a4b9ff](https://github.com/narisraz/auth/commit/5a4b9ff))
16
+
17
+ ### ❤️ Thank You
18
+
19
+ - Naris Razafimahatratra
20
+
21
+ ## 0.12.0 (2026-01-25)
22
+
23
+ ### 🚀 Features
24
+
25
+ - integrate UserExistencePort for account existence checks ([47fcde5](https://github.com/narisraz/auth/commit/47fcde5))
26
+
27
+ ### ❤️ Thank You
28
+
29
+ - Naris Razafimahatratra
30
+
31
+ ## 0.11.0 (2026-01-25)
32
+
33
+ ### 🚀 Features
34
+
35
+ - enhance authentication framework with CompositeAuthService and policies ([ac9fa89](https://github.com/narisraz/auth/commit/ac9fa89))
36
+
37
+ ### ❤️ Thank You
38
+
39
+ - Naris Razafimahatratra
40
+
41
+ ## 0.10.0 (2026-01-24)
42
+
43
+ This was a version bump only for @siran/auth-core to align it with other projects, there were no code changes.
44
+
1
45
  ## 0.9.0 (2026-01-24)
2
46
 
3
47
  ### 🚀 Features
@@ -0,0 +1,104 @@
1
+ # CompositeAuthService Usage
2
+
3
+ The `CompositeAuthService` allows you to configure different adapters for each authentication method type with an immutable configuration.
4
+
5
+ ## Configuration
6
+
7
+ ```typescript
8
+ import { CompositeAuthService } from '@siran/auth-core';
9
+ import { PasswordAdapter } from '@siran/password-adapter';
10
+ import { OAuthAdapter } from '@siran/oauth-adapter';
11
+
12
+ const authService = new CompositeAuthService({
13
+ password: new PasswordAdapter(),
14
+ oauth: new OAuthAdapter(),
15
+ // otp: new OTPAdapter(), // Optional
16
+ // magic_link: new MagicLinkAdapter(), // Optional
17
+ });
18
+ ```
19
+
20
+ ## Supported Methods
21
+
22
+ - `password`: For username/password authentication
23
+ - `oauth`: For OAuth provider authentication (Google, GitHub, Apple, Facebook)
24
+ - `otp`: For one-time password authentication
25
+ - `magic_link`: For magic link authentication
26
+
27
+ ## Error Handling
28
+
29
+ The service provides specific error codes for different failure scenarios:
30
+
31
+ ### Method/Payload Errors
32
+
33
+ - `ADAPTER_UNAVAILABLE`: No adapter configured for the auth method
34
+ - `INVALID_METHOD_PAYLOAD`: Malformed or incomplete auth method data
35
+ - `UNSUPPORTED_OAUTH_PROVIDER`: OAuth provider not in supported list
36
+
37
+ ### Authentication Errors
38
+
39
+ - `INVALID_CREDENTIALS`: Username/password incorrect or invalid credentials
40
+
41
+ ### Account Status Errors
42
+
43
+ - `ACCOUNT_DISABLED`: Account is disabled
44
+ - `ACCOUNT_NOT_VERIFIED`: Account email not verified
45
+ - `ACCOUNT_LOCKED`: Account permanently locked
46
+ - `ACCOUNT_LOCKED_TEMPORARILY`: Account temporarily locked
47
+
48
+ ### Policy Errors
49
+
50
+ - `PRE_REGISTER_POLICY_VIOLATED`: Pre-registration policy violated
51
+ - `RATE_LIMIT_EXCEEDED`: Too many authentication attempts
52
+
53
+ ### System Errors
54
+
55
+ - `OAUTH_PROVIDER_DOWN`: OAuth provider service unavailable
56
+ - `NETWORK_ERROR`: Network connectivity issues
57
+ - `INTERNAL_ERROR`: Unexpected server error
58
+
59
+ ## Usage Example
60
+
61
+ ```typescript
62
+ // Password authentication
63
+ const passwordResult = await authService.authenticate({
64
+ type: 'password',
65
+ identifier: 'user@example.com',
66
+ password: 'password123',
67
+ });
68
+
69
+ // OAuth authentication
70
+ const oauthResult = await authService.authenticate({
71
+ type: 'oauth',
72
+ provider: 'google',
73
+ code: 'auth-code',
74
+ });
75
+
76
+ // If a method is not configured, it returns a specific error:
77
+ const magicLinkResult = await authService.authenticate({
78
+ type: 'magic_link',
79
+ token: 'token',
80
+ });
81
+
82
+ console.log(magicLinkResult.ok); // false
83
+ console.log(magicLinkResult.error); // 'ADAPTER_UNAVAILABLE'
84
+
85
+ // Invalid payloads are caught early:
86
+ const invalidPassword = await authService.authenticate({
87
+ type: 'password',
88
+ identifier: 'test@example.com',
89
+ password: 'short', // Too short
90
+ });
91
+
92
+ console.log(invalidPassword.error); // 'INVALID_CREDENTIALS'
93
+ ```
94
+
95
+ ## Configuration Access
96
+
97
+ The service provides read-only access to the configuration:
98
+
99
+ ```typescript
100
+ const config = authService.getMethodConfig();
101
+ // Returns: { password: PasswordAdapter, oauth: OAuthAdapter }
102
+ ```
103
+
104
+ Note: The configuration is immutable once created. No runtime adapter management is supported.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siran/auth-core",
3
- "version": "0.9.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -0,0 +1,23 @@
1
+
2
+ import { AuthMethod, AuthMethodOtp, AuthMethodPassword } from "@application/ports/auth-service.port.js";
3
+ import { PreRegisterPolicy } from "@application/ports/pre-register-policy.port.js";
4
+ import { UserExistencePort } from "@application/ports/user-existence.port.js";
5
+
6
+ export class AccountAlreadyExistsPolicy implements PreRegisterPolicy {
7
+ constructor(
8
+ private readonly userExistence: UserExistencePort,
9
+ private readonly enabled: boolean,
10
+ ) { }
11
+
12
+ code: string = 'ACCOUNT_ALREADY_EXISTS';
13
+
14
+ async check(method: AuthMethod): Promise<boolean> {
15
+ const identifier = (method as AuthMethodPassword | AuthMethodOtp).identifier;
16
+ if (!identifier) return false;
17
+ return this.userExistence.userExistsByIdentifier(identifier);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'password' || method.type === 'otp';
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodMagicLink,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class MagicLinkTokenPolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'MISSING_MAGIC_LINK_TOKEN';
11
+
12
+ private checkMagicLink(method: AuthMethodMagicLink): boolean {
13
+ return !!method.token && method.token.length >= 10;
14
+ }
15
+
16
+ async check(method: AuthMethod): Promise<boolean> {
17
+ return this.checkMagicLink(method as AuthMethodMagicLink);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'magic_link';
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodOAuth,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class OAuthCodePolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'MISSING_OAUTH_CODE';
11
+
12
+ private checkOAuth(method: AuthMethodOAuth): boolean {
13
+ return !!method.code && method.code.length >= 5;
14
+ }
15
+
16
+ async check(method: AuthMethod): Promise<boolean> {
17
+ return this.checkOAuth(method as AuthMethodOAuth);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'oauth';
22
+ }
23
+ }
@@ -0,0 +1,29 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodOAuth,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class OAuthProviderPolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'UNSUPPORTED_OAUTH_PROVIDER';
11
+
12
+ private checkOAuth(method: AuthMethodOAuth): boolean {
13
+ const supportedProviders = [
14
+ 'google',
15
+ 'github',
16
+ 'apple',
17
+ 'facebook',
18
+ ] as const;
19
+ return supportedProviders.includes(method.provider);
20
+ }
21
+
22
+ async check(method: AuthMethod): Promise<boolean> {
23
+ return this.checkOAuth(method as AuthMethodOAuth);
24
+ }
25
+
26
+ async isActive(method: AuthMethod): Promise<boolean> {
27
+ return this.enabled && method.type === 'oauth';
28
+ }
29
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodOtp,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class OtpCodeFormatPolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'INVALID_OTP_FORMAT';
11
+
12
+ private checkOtp(method: AuthMethodOtp): boolean {
13
+ return /^\d{6}$/.test(method.code || '');
14
+ }
15
+
16
+ async check(method: AuthMethod): Promise<boolean> {
17
+ return this.checkOtp(method as AuthMethodOtp);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'otp';
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodOtp,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class OtpCodePolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'MISSING_OTP_CODE';
11
+
12
+ private checkOtp(method: AuthMethodOtp): boolean {
13
+ return !!method.code;
14
+ }
15
+
16
+ async check(method: AuthMethod): Promise<boolean> {
17
+ return this.checkOtp(method as AuthMethodOtp);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'otp';
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodOtp,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class OtpIdentifierPolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'MISSING_OTP_IDENTIFIER';
11
+
12
+ private checkOtp(method: AuthMethodOtp): boolean {
13
+ return !!method.identifier;
14
+ }
15
+
16
+ async check(method: AuthMethod): Promise<boolean> {
17
+ return this.checkOtp(method as AuthMethodOtp);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'otp';
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodPassword,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class PasswordIdentifierPolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'MISSING_PASSWORD_IDENTIFIER';
11
+
12
+ private checkPassword(method: AuthMethodPassword): boolean {
13
+ return !!method.identifier;
14
+ }
15
+
16
+ async check(method: AuthMethod): Promise<boolean> {
17
+ return this.checkPassword(method as AuthMethodPassword);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'password';
22
+ }
23
+ }
@@ -0,0 +1,29 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodPassword,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class PasswordMinLengthPolicy implements PreRegisterPolicy {
8
+ constructor(
9
+ private readonly enabled: boolean,
10
+ private readonly minLength: number = 8
11
+ ) {}
12
+
13
+ code: string = 'PASSWORD_TOO_SHORT';
14
+
15
+ private checkPassword(method: AuthMethodPassword): boolean {
16
+ return method.password?.length >= this.minLength;
17
+ }
18
+
19
+ async check(method: AuthMethod): Promise<boolean> {
20
+ if (method.type === 'password') {
21
+ return this.checkPassword(method);
22
+ }
23
+ return true;
24
+ }
25
+
26
+ async isActive(method: AuthMethod): Promise<boolean> {
27
+ return this.enabled && method.type === 'password';
28
+ }
29
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ AuthMethod,
3
+ AuthMethodPassword,
4
+ } from '@application/ports/auth-service.port.js';
5
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
6
+
7
+ export class PasswordPresentPolicy implements PreRegisterPolicy {
8
+ constructor(private readonly enabled: boolean) { }
9
+
10
+ code: string = 'MISSING_PASSWORD';
11
+
12
+ private checkPassword(method: AuthMethodPassword): boolean {
13
+ return !!method.password;
14
+ }
15
+
16
+ async check(method: AuthMethod): Promise<boolean> {
17
+ return this.checkPassword(method as AuthMethodPassword);
18
+ }
19
+
20
+ async isActive(method: AuthMethod): Promise<boolean> {
21
+ return this.enabled && method.type === 'password';
22
+ }
23
+ }
@@ -0,0 +1,27 @@
1
+ import type {
2
+ AuthErrorCode,
3
+ AuthMethod,
4
+ AuthMethodPassword,
5
+ } from '@application/ports/auth-service.port.js';
6
+ import type { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
7
+
8
+ export class PasswordStrengthPolicy implements PreRegisterPolicy {
9
+ code: AuthErrorCode = 'WEAK_PASSWORD';
10
+
11
+ constructor(
12
+ private readonly enabled: boolean,
13
+ private readonly minLength: number = 8
14
+ ) { }
15
+
16
+ private checkPassword(method: AuthMethodPassword): boolean {
17
+ return method.password?.length >= this.minLength;
18
+ }
19
+
20
+ async check(method: AuthMethod): Promise<boolean> {
21
+ return this.checkPassword(method as AuthMethodPassword);
22
+ }
23
+
24
+ async isActive(method: AuthMethod): Promise<boolean> {
25
+ return this.enabled && method.type === 'password';
26
+ }
27
+ }
@@ -1,6 +1,11 @@
1
1
  export type AuthPreferences = {
2
2
  allowLoginWithNotVerifiedAccount: boolean;
3
3
  allowDuplicatedAccount: boolean;
4
+ allowWeakPassword: boolean;
5
+ allowMissingPasswordIdentifier: boolean;
6
+ allowMissingPassword: boolean;
7
+ allowMissingOtpIdentifier: boolean;
8
+ allowMissingOtpCode: boolean;
4
9
  };
5
10
 
6
11
  export interface AuthPreferencesServicePort {
@@ -1,20 +1,62 @@
1
- import type { UserAccount } from "@domain/user-account.js";
1
+ import type { UserAccount } from '@domain/user-account.js';
2
+
3
+ export type AuthMethodPassword = {
4
+ type: 'password';
5
+ identifier: string;
6
+ password: string;
7
+ };
8
+ export type AuthMethodOtp = { type: 'otp'; identifier: string; code: string };
9
+ export type AuthMethodMagicLink = { type: 'magic_link'; token: string };
10
+ export type AuthMethodOAuth = {
11
+ type: 'oauth';
12
+ provider: 'google' | 'github' | 'apple' | 'facebook';
13
+ code: string;
14
+ };
2
15
 
3
16
  export type AuthMethod =
4
- | { type: "password"; identifier: string; password: string }
5
- | { type: "otp"; identifier: string; code: string }
6
- | { type: "magic_link"; token: string }
7
- | { type: "oauth"; provider: "google" | "github" | "apple" | "facebook"; code: string };
17
+ | AuthMethodPassword
18
+ | AuthMethodOtp
19
+ | AuthMethodMagicLink
20
+ | AuthMethodOAuth;
8
21
 
9
22
  export type AuthErrorCode =
10
- | "INVALID_CREDENTIALS"
11
- | "ACCOUNT_DISABLED"
12
- | "ACCOUNT_NOT_VERIFIED"
13
- | "ACCOUNT_LOCKED"
14
- | "PRE_REGISTER_POLICY_VIOLATED";
23
+ // Method/Payload specific errors
24
+ | 'ADAPTER_UNAVAILABLE' // No adapter configured for this auth method
25
+ | 'INVALID_CREDENTIALS' // Username/password incorrect
26
+ | 'INVALID_METHOD_PAYLOAD' // Malformed auth method data
27
+ | 'UNSUPPORTED_OAUTH_PROVIDER' // OAuth provider not supported
28
+
29
+ // Account status errors
30
+ | 'ACCOUNT_DISABLED' // Account is disabled
31
+ | 'ACCOUNT_NOT_VERIFIED' // Account email not verified
32
+ | 'ACCOUNT_LOCKED' // Account temporarily locked
33
+ | 'ACCOUNT_LOCKED_TEMPORARILY' // Account temporarily locked
34
+
35
+ // Policy errors
36
+ | 'PRE_REGISTER_POLICY_VIOLATED' // Pre-registration policy violated
37
+ | 'RATE_LIMIT_EXCEEDED' // Too many attempts
38
+ | 'WEAK_PASSWORD' // Password doesn't meet strength requirements
39
+ | 'ACCOUNT_ALREADY_EXISTS' // Account already exists
40
+ | 'MISSING_PASSWORD_IDENTIFIER' // Password identifier missing
41
+ | 'MISSING_PASSWORD' // Password missing
42
+ | 'PASSWORD_TOO_SHORT' // Password too short
43
+ | 'MISSING_OTP_IDENTIFIER' // OTP identifier missing
44
+ | 'MISSING_OTP_CODE' // OTP code missing
45
+ | 'INVALID_OTP_FORMAT' // OTP code format invalid
46
+ | 'MISSING_MAGIC_LINK_TOKEN' // Magic link token missing
47
+ | 'MISSING_OAUTH_CODE' // OAuth code missing
48
+
49
+ // System errors
50
+ | 'OAUTH_PROVIDER_DOWN' // OAuth provider unavailable
51
+ | 'NETWORK_ERROR' // Network connectivity issues
52
+ | 'INTERNAL_ERROR'; // Unexpected server error;
15
53
 
16
54
  export type AuthResultSuccess = { ok: true; user: UserAccount };
17
- export type AuthResultFailure = { ok: false; error: AuthErrorCode; violatedPolicies: string[] };
55
+ export type AuthResultFailure = {
56
+ ok: false;
57
+ error: AuthErrorCode;
58
+ violatedPolicies: string[];
59
+ };
18
60
 
19
61
  export type AuthResult = AuthResultSuccess | AuthResultFailure;
20
62
 
@@ -22,7 +64,4 @@ export interface AuthServicePort {
22
64
  authenticate(method: AuthMethod): Promise<AuthResult>;
23
65
  register(method: AuthMethod): Promise<AuthResult>;
24
66
  logout(): Promise<void>;
25
- userExists(method: AuthMethod): Promise<boolean>;
26
67
  }
27
-
28
-
@@ -0,0 +1,3 @@
1
+ export interface UserExistencePort {
2
+ userExistsByIdentifier(identifier: string): Promise<boolean>;
3
+ }
@@ -0,0 +1,96 @@
1
+ import type {
2
+ AuthMethod,
3
+ AuthResult,
4
+ AuthServicePort,
5
+ } from '@application/ports/auth-service.port.js';
6
+
7
+ export type AuthMethodConfig = Partial<
8
+ Record<AuthMethod['type'], AuthServicePort>
9
+ >;
10
+
11
+ export class CompositeAuthService implements AuthServicePort {
12
+ private readonly methodConfig: AuthMethodConfig;
13
+ private readonly adapters: AuthServicePort[];
14
+
15
+ constructor(config: AuthMethodConfig) {
16
+ this.methodConfig = config;
17
+ // Extract all adapters from config for operations like logout
18
+ this.adapters = Object.values(config).filter(
19
+ (adapter): adapter is AuthServicePort => adapter !== undefined
20
+ );
21
+ }
22
+
23
+ async authenticate(method: AuthMethod): Promise<AuthResult> {
24
+ const adapter = this.getAdapterForMethod(method);
25
+
26
+ if (!adapter) {
27
+ return {
28
+ ok: false,
29
+ error: 'ADAPTER_UNAVAILABLE',
30
+ violatedPolicies: [],
31
+ };
32
+ }
33
+
34
+ try {
35
+ return await adapter.authenticate(method);
36
+ } catch (error) {
37
+ console.error('Authentication error in adapter:', error);
38
+ return {
39
+ ok: false,
40
+ error: 'NETWORK_ERROR',
41
+ violatedPolicies: [],
42
+ };
43
+ }
44
+ }
45
+
46
+ async register(method: AuthMethod): Promise<AuthResult> {
47
+ const adapter = this.getAdapterForMethod(method);
48
+
49
+ if (!adapter) {
50
+ return {
51
+ ok: false,
52
+ error: 'ADAPTER_UNAVAILABLE',
53
+ violatedPolicies: [],
54
+ };
55
+ }
56
+
57
+ try {
58
+ return await adapter.register(method);
59
+ } catch (error) {
60
+ console.error('Registration error in adapter:', error);
61
+ return {
62
+ ok: false,
63
+ error: 'NETWORK_ERROR',
64
+ violatedPolicies: [],
65
+ };
66
+ }
67
+ }
68
+
69
+ async logout(): Promise<void> {
70
+ // Try to logout from all adapters
71
+ const logoutPromises = this.adapters.map(async (adapter) => {
72
+ try {
73
+ await adapter.logout();
74
+ } catch (error) {
75
+ console.error('Logout error in adapter:', error);
76
+ // Continue with other adapters even if one fails
77
+ }
78
+ });
79
+
80
+ await Promise.allSettled(logoutPromises);
81
+ }
82
+
83
+ /**
84
+ * Get adapter for specific auth method
85
+ */
86
+ private getAdapterForMethod(method: AuthMethod): AuthServicePort | undefined {
87
+ return this.methodConfig[method.type];
88
+ }
89
+
90
+ /**
91
+ * Get method configuration (read-only)
92
+ */
93
+ getMethodConfig(): AuthMethodConfig {
94
+ return { ...this.methodConfig };
95
+ }
96
+ }