@siran/auth-core 0.9.0 → 0.14.1

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
@@ -0,0 +1,55 @@
1
+ import { PasswordPresentPolicy } from '@application/policies/password-present.policy.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('PasswordPresentPolicy', () => {
5
+ it('should return false when policy is disabled', async () => {
6
+ const policy = new PasswordPresentPolicy(false);
7
+ const method = {
8
+ type: 'password' as const,
9
+ identifier: 'test@example.com',
10
+ password: '',
11
+ };
12
+
13
+ expect(await policy.isActive(method)).toBe(false);
14
+ });
15
+
16
+ it('should return true when policy is enabled and password is present', async () => {
17
+ const policy = new PasswordPresentPolicy(true);
18
+ const method = {
19
+ type: 'password' as const,
20
+ identifier: 'test@example.com',
21
+ password: 'password123',
22
+ };
23
+
24
+ expect(await policy.isActive(method)).toBe(true);
25
+ expect(await policy.check(method)).toBe(true);
26
+ });
27
+
28
+ it('should return false when policy is enabled but password is missing', async () => {
29
+ const policy = new PasswordPresentPolicy(true);
30
+ const method = {
31
+ type: 'password' as const,
32
+ identifier: 'test@example.com',
33
+ password: '',
34
+ };
35
+
36
+ expect(await policy.isActive(method)).toBe(true);
37
+ expect(await policy.check(method)).toBe(false);
38
+ });
39
+
40
+ it('should return false for non-password methods', async () => {
41
+ const policy = new PasswordPresentPolicy(true);
42
+ const otpMethod = {
43
+ type: 'otp' as const,
44
+ identifier: 'test@example.com',
45
+ code: '123456',
46
+ };
47
+
48
+ expect(await policy.isActive(otpMethod)).toBe(false);
49
+ });
50
+
51
+ it('should have correct error code', () => {
52
+ const policy = new PasswordPresentPolicy(true);
53
+ expect(policy.code).toBe('MISSING_PASSWORD');
54
+ });
55
+ });
@@ -0,0 +1,66 @@
1
+ import { PasswordStrengthPolicy } from '@application/policies/password-strength.policy.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('PasswordStrengthPolicy', () => {
5
+ it('should return false when policy is disabled', async () => {
6
+ const policy = new PasswordStrengthPolicy(false);
7
+ const method = {
8
+ type: 'password' as const,
9
+ identifier: 'test@example.com',
10
+ password: 'weak',
11
+ };
12
+
13
+ expect(await policy.isActive(method)).toBe(false);
14
+ });
15
+
16
+ it('should return true when policy is enabled and password meets minimum length', async () => {
17
+ const policy = new PasswordStrengthPolicy(true, 6);
18
+ const method = {
19
+ type: 'password' as const,
20
+ identifier: 'test@example.com',
21
+ password: 'strong123',
22
+ };
23
+
24
+ expect(await policy.isActive(method)).toBe(true);
25
+ expect(await policy.check(method)).toBe(true);
26
+ });
27
+
28
+ it('should return false when policy is enabled but password is too short', async () => {
29
+ const policy = new PasswordStrengthPolicy(true, 12);
30
+ const method = {
31
+ type: 'password' as const,
32
+ identifier: 'test@example.com',
33
+ password: 'short',
34
+ };
35
+
36
+ expect(await policy.isActive(method)).toBe(true);
37
+ expect(await policy.check(method)).toBe(false);
38
+ });
39
+
40
+ it('should use default minimum length of 8', async () => {
41
+ const policy = new PasswordStrengthPolicy(true);
42
+ const method = {
43
+ type: 'password' as const,
44
+ identifier: 'test@example.com',
45
+ password: '1234567', // 7 chars
46
+ };
47
+
48
+ expect(await policy.check(method)).toBe(false);
49
+ });
50
+
51
+ it('should return true for non-password methods', async () => {
52
+ const policy = new PasswordStrengthPolicy(true, 8);
53
+ const otpMethod = {
54
+ type: 'otp' as const,
55
+ identifier: 'test@example.com',
56
+ code: '123456',
57
+ };
58
+
59
+ expect(await policy.isActive(otpMethod)).toBe(false);
60
+ });
61
+
62
+ it('should have correct error code', () => {
63
+ const policy = new PasswordStrengthPolicy(true, 8);
64
+ expect(policy.code).toBe('WEAK_PASSWORD');
65
+ });
66
+ });
@@ -8,7 +8,6 @@ interface AuthServiceMock {
8
8
  authenticate?: (method: AuthMethod) => Promise<AuthResult>;
9
9
  register?: (method: AuthMethod) => Promise<AuthResult>;
10
10
  logout?: () => Promise<void>;
11
- userExists?: (method: AuthMethod) => Promise<boolean>;
12
11
  }
13
12
 
14
13
  export function createAuthServiceMock(authService?: AuthServiceMock): AuthServicePort {
@@ -16,6 +15,5 @@ export function createAuthServiceMock(authService?: AuthServiceMock): AuthServic
16
15
  authenticate: authService?.authenticate ?? vi.fn().mockResolvedValue({ ok: true, user: activeUser }),
17
16
  logout: authService?.logout ?? vi.fn().mockResolvedValue(undefined),
18
17
  register: authService?.register ?? vi.fn().mockResolvedValue({ ok: true, user: activeUser }),
19
- userExists: authService?.userExists ?? vi.fn().mockResolvedValue(false),
20
18
  };
21
19
  }
@@ -0,0 +1,18 @@
1
+ import { AuthServicePort } from '@application/ports/auth-service.port.js';
2
+ import { vi } from 'vitest';
3
+
4
+ export function createPasswordAdapterMock(): AuthServicePort {
5
+ return {
6
+ authenticate: vi.fn(),
7
+ register: vi.fn(),
8
+ logout: vi.fn(),
9
+ };
10
+ }
11
+
12
+ export function createOAuthAdapterMock(): AuthServicePort {
13
+ return {
14
+ authenticate: vi.fn(),
15
+ register: vi.fn(),
16
+ logout: vi.fn(),
17
+ };
18
+ }
@@ -0,0 +1,162 @@
1
+ import type { AuthServicePort } from '@application/ports/auth-service.port.js';
2
+ import { CompositeAuthService } from '@application/services/composite-auth.service.js';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+ import {
5
+ createOAuthAdapterMock,
6
+ createPasswordAdapterMock,
7
+ } from '../services/composite-auth.mock.js';
8
+
9
+ describe('CompositeAuthService', () => {
10
+ let compositeAuthService: CompositeAuthService;
11
+ let mockPasswordAdapter: AuthServicePort;
12
+ let mockOAuthAdapter: AuthServicePort;
13
+
14
+ beforeEach(() => {
15
+ mockPasswordAdapter = createPasswordAdapterMock();
16
+ mockOAuthAdapter = createOAuthAdapterMock();
17
+
18
+ compositeAuthService = new CompositeAuthService({
19
+ password: mockPasswordAdapter,
20
+ oauth: mockOAuthAdapter,
21
+ });
22
+ });
23
+
24
+ it('should create composite service with method-based config', () => {
25
+ expect(compositeAuthService).toBeDefined();
26
+ expect(compositeAuthService.getMethodConfig()).toEqual({
27
+ password: mockPasswordAdapter,
28
+ oauth: mockOAuthAdapter,
29
+ });
30
+ });
31
+
32
+ it('should authenticate with password adapter', async () => {
33
+ const mockUser = {
34
+ id: 'test-id',
35
+ displayName: 'Test User',
36
+ status: 'active' as const,
37
+ isVerified: true,
38
+ };
39
+
40
+ (mockPasswordAdapter.authenticate as any).mockResolvedValue({
41
+ ok: true,
42
+ user: mockUser,
43
+ });
44
+
45
+ const result = await compositeAuthService.authenticate({
46
+ type: 'password',
47
+ identifier: 'test@example.com',
48
+ password: 'password',
49
+ });
50
+
51
+ expect(result.ok).toBe(true);
52
+ if (result.ok) {
53
+ expect(result.user).toEqual(mockUser);
54
+ }
55
+ expect(mockPasswordAdapter.authenticate).toHaveBeenCalled();
56
+ expect(mockOAuthAdapter.authenticate).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it('should authenticate with oauth adapter', async () => {
60
+ const mockUser = {
61
+ id: 'test-id',
62
+ displayName: 'Test User',
63
+ status: 'active' as const,
64
+ isVerified: true,
65
+ };
66
+
67
+ (mockOAuthAdapter.authenticate as any).mockResolvedValue({
68
+ ok: true,
69
+ user: mockUser,
70
+ });
71
+
72
+ const result = await compositeAuthService.authenticate({
73
+ type: 'oauth',
74
+ provider: 'google',
75
+ code: 'auth-code',
76
+ });
77
+
78
+ expect(result.ok).toBe(true);
79
+ if (result.ok) {
80
+ expect(result.user).toEqual(mockUser);
81
+ }
82
+ expect(mockOAuthAdapter.authenticate).toHaveBeenCalled();
83
+ expect(mockPasswordAdapter.authenticate).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it('should validate magic link token format', async () => {
87
+ const result = await compositeAuthService.authenticate({
88
+ type: 'magic_link',
89
+ token: 'short', // Too short
90
+ });
91
+
92
+ expect(result.ok).toBe(false);
93
+ if (!result.ok) {
94
+ expect(result.error).toBe('ADAPTER_UNAVAILABLE'); // No magic_link adapter configured
95
+ }
96
+ });
97
+
98
+ it('should return error for unsupported auth method', async () => {
99
+ const result = await compositeAuthService.authenticate({
100
+ type: 'unknown_method' as any, // Unsupported method type
101
+ token: 'magic-token',
102
+ });
103
+
104
+ expect(result.ok).toBe(false);
105
+ if (!result.ok) {
106
+ expect(result.error).toBe('ADAPTER_UNAVAILABLE');
107
+ }
108
+ });
109
+
110
+ it('should handle register with correct adapter', async () => {
111
+ const mockUser = {
112
+ id: 'test-id',
113
+ displayName: 'Test User',
114
+ status: 'active' as const,
115
+ isVerified: true,
116
+ };
117
+
118
+ (mockPasswordAdapter.register as any).mockResolvedValue({
119
+ ok: true,
120
+ user: mockUser,
121
+ });
122
+
123
+ const result = await compositeAuthService.register({
124
+ type: 'password',
125
+ identifier: 'test@example.com',
126
+ password: 'password',
127
+ });
128
+
129
+ expect(result.ok).toBe(true);
130
+ if (result.ok) {
131
+ expect(result.user).toEqual(mockUser);
132
+ }
133
+ expect(mockPasswordAdapter.register).toHaveBeenCalled();
134
+ });
135
+
136
+ it('should validate OTP code format', async () => {
137
+ const result = await compositeAuthService.authenticate({
138
+ type: 'otp',
139
+ identifier: 'test@example.com',
140
+ code: '123', // Too short
141
+ });
142
+
143
+ expect(result.ok).toBe(false);
144
+ if (!result.ok) {
145
+ expect(result.error).toBe('ADAPTER_UNAVAILABLE'); // No OTP adapter configured
146
+ }
147
+ });
148
+
149
+ it('should demonstrate exact requested syntax', () => {
150
+ // This is exactly how the user wants to use it:
151
+ const engine = new CompositeAuthService({
152
+ password: createPasswordAdapterMock(),
153
+ oauth: createOAuthAdapterMock(),
154
+ });
155
+
156
+ expect(engine).toBeDefined();
157
+ expect(engine.getMethodConfig()).toEqual({
158
+ password: expect.any(Object),
159
+ oauth: expect.any(Object),
160
+ });
161
+ });
162
+ });
package/tsconfig.lib.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "@domain/*": ["src/domain/*"],
9
9
  "@application/*": ["src/application/*"],
10
10
  "@infrastructure/*": ["src/infrastructure/*"],
11
- "@root/*": ["src/*"],
11
+ "@root/*": ["src/*"]
12
12
  },
13
13
  "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
14
14
  "emitDeclarationOnly": true,
@@ -29,6 +29,7 @@
29
29
  "src/**/*.test.js",
30
30
  "src/**/*.spec.js",
31
31
  "src/**/*.test.jsx",
32
- "src/**/*.spec.jsx"
32
+ "src/**/*.spec.jsx",
33
+ "src/**/*.example.ts"
33
34
  ]
34
35
  }
@@ -1,20 +0,0 @@
1
-
2
- import { AuthMethod, AuthServicePort } from "@application/ports/auth-service.port.js";
3
- import { PreRegisterPolicy } from "@application/ports/pre-register-policy.port.js";
4
-
5
- export class AccountAlreadyExistsPolicy implements PreRegisterPolicy {
6
- constructor(
7
- private readonly authService: AuthServicePort,
8
- private readonly enabled: boolean,
9
- ) { }
10
-
11
- code: string = 'ACCOUNT_ALREADY_EXISTS';
12
-
13
- async check(method: AuthMethod): Promise<boolean> {
14
- return this.authService.userExists(method);
15
- }
16
-
17
- async isActive(method: AuthMethod): Promise<boolean> {
18
- return this.enabled;
19
- }
20
- }