@rineex/auth-core 0.0.2 → 0.0.3

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 (106) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/eslint.config.mjs +1 -0
  3. package/package.json +5 -1
  4. package/src/application/mfa/events/challenge-issue-observability.event.ts +18 -0
  5. package/src/application/mfa/events/session-started-observability.event.ts +18 -0
  6. package/src/application/mfa/events/verification-failed-observability.event.ts +14 -0
  7. package/src/application/mfa/events/verification-succeeded-observibility.event.ts +12 -0
  8. package/src/application/mfa/issue-mfa-challenge.application-service.ts +75 -0
  9. package/src/application/mfa/start-mfa-session.application-service.ts +90 -0
  10. package/src/application/mfa/verify-mfa.application-service.ts +61 -0
  11. package/src/application/services/auth-orchestrator.service.ts +77 -0
  12. package/src/application/services/oauth-authorize.service.ts +12 -0
  13. package/src/domain/{aggregates → identity/aggregates}/authentication-attempt.aggregate.ts +43 -26
  14. package/src/domain/identity/aggregates/index.ts +1 -0
  15. package/src/domain/identity/entities/identity.entity.ts +126 -0
  16. package/src/domain/identity/entities/index.ts +1 -0
  17. package/src/domain/identity/events/index.ts +3 -0
  18. package/src/domain/identity/index.ts +4 -0
  19. package/src/domain/identity/value-objects/__tests__/auth-attempt-id.vo.spec.ts +42 -0
  20. package/src/domain/identity/value-objects/__tests__/auth-factor.vo.spec.ts +39 -0
  21. package/src/domain/identity/value-objects/__tests__/auth-method.vo.spec.ts +0 -0
  22. package/src/domain/{value-objects → identity/value-objects}/auth-attempt-id.vo.ts +4 -0
  23. package/src/domain/identity/value-objects/auth-factor.vo.ts +17 -0
  24. package/src/domain/identity/value-objects/auth-method.vo.ts +34 -0
  25. package/src/domain/identity/value-objects/auth-policy.vo.ts +19 -0
  26. package/src/domain/{value-objects → identity/value-objects}/identity-id.vo.ts +4 -0
  27. package/src/domain/identity/value-objects/identity-provider.vo.ts +13 -0
  28. package/src/domain/identity/value-objects/index.ts +8 -0
  29. package/src/domain/identity/value-objects/risk-signal.vo.ts +17 -0
  30. package/src/domain/index.ts +5 -0
  31. package/src/domain/mfa/aggregates/mfa-session.aggregate.ts +84 -0
  32. package/src/domain/mfa/entities/mfa-challenge.entity.ts +70 -0
  33. package/src/domain/mfa/types/mfa-challenge-registry.ts +21 -0
  34. package/src/domain/mfa/value-objects/mfa-challenge-id.vo.ts +19 -0
  35. package/src/domain/mfa/value-objects/mfa-challenge-status.vo.ts +31 -0
  36. package/src/domain/mfa/value-objects/mfa-session-id.vo.ts +19 -0
  37. package/src/domain/mfa/violations/mfa-active-challenge-exists.violation.ts +10 -0
  38. package/src/domain/mfa/violations/mfa-already-verified.violation.ts +10 -0
  39. package/src/domain/mfa/violations/mfa-attempts-exceeded.violation.ts +17 -0
  40. package/src/domain/mfa/violations/mfa-expired.violation.ts +10 -0
  41. package/src/domain/oauth/aggregates/oauth-authorization.aggregate.ts +106 -0
  42. package/src/domain/oauth/aggregates/oauth-authorize.service.ts +0 -0
  43. package/src/domain/oauth/entities/oauth-authorization.entity.ts +50 -0
  44. package/src/domain/oauth/value-objects/authorization-code-id.vo.ts +9 -0
  45. package/src/domain/oauth/value-objects/authorization-code.vo.ts +18 -0
  46. package/src/domain/oauth/value-objects/client-id.vo.ts +9 -0
  47. package/src/domain/oauth/value-objects/code-challenge-method.vo.ts +15 -0
  48. package/src/domain/oauth/value-objects/code-challenge.vo.ts +24 -0
  49. package/src/domain/oauth/value-objects/oauth-authorization-id.vo.ts +19 -0
  50. package/src/domain/oauth/value-objects/oauth-provider.vo.ts +15 -0
  51. package/src/domain/oauth/value-objects/pkce.vo.ts +29 -0
  52. package/src/domain/oauth/value-objects/redirect-uri.vo.ts +19 -0
  53. package/src/domain/oauth/value-objects/scope-set.vo.ts +37 -0
  54. package/src/domain/oauth/violations/authorization-already-used.violation.ts +10 -0
  55. package/src/domain/oauth/violations/authorization-expired.violation.ts +10 -0
  56. package/src/domain/oauth/violations/consent-required.violation.ts +10 -0
  57. package/src/domain/oauth/violations/invalid-authorization-code.violation.ts +12 -0
  58. package/src/domain/oauth/violations/invalid-oauth-provider.violation.ts +13 -0
  59. package/src/domain/oauth/violations/invalid-pkce.violation.ts +12 -0
  60. package/src/domain/oauth/violations/invalid-redirect-uri.violation.ts +10 -0
  61. package/src/domain/policy/contracts/auth-policy-context.ts +27 -0
  62. package/src/domain/policy/contracts/auth-policy-decision.ts +7 -0
  63. package/src/domain/policy/contracts/auth-policy.ts +17 -0
  64. package/src/domain/policy/contracts/index.ts +3 -0
  65. package/src/domain/policy/engine/auth-policy-engine.ts +41 -0
  66. package/src/domain/policy/index.ts +2 -0
  67. package/src/domain/session/entities/session.entity.ts +82 -0
  68. package/src/domain/session/value-objects/session-id.vo.ts +10 -0
  69. package/src/domain/token/aggregates/token.aggregate.ts +34 -0
  70. package/src/domain/token/value-objects/auth-token.vo.ts +29 -0
  71. package/src/domain/token/value-objects/session-token.vo.ts +14 -0
  72. package/src/domain/violations/auth-domain.violation.ts +9 -0
  73. package/src/domain/violations/invalid-auth-token.violation.ts +13 -0
  74. package/src/domain/violations/invalid-scope.violation.ts +10 -0
  75. package/src/domain/violations/invalid-session.violation.ts +13 -0
  76. package/src/index.ts +3 -1
  77. package/src/ports/inbound/auth-method.port.ts +1 -1
  78. package/src/ports/inbound/index.ts +2 -0
  79. package/src/ports/inbound/start-auth.command.ts +28 -0
  80. package/src/ports/index.ts +2 -0
  81. package/src/ports/log/log.port.ts +25 -0
  82. package/src/ports/mfa/mfa-clock.port.ts +11 -0
  83. package/src/ports/mfa/mfa-session-id-generator.port.ts +15 -0
  84. package/src/ports/mfa/mfa-session-repository.port.ts +31 -0
  85. package/src/ports/observability/observability-event.port.ts +16 -0
  86. package/src/ports/outbound/authentication-attempt.repository.port.ts +1 -1
  87. package/src/ports/outbound/domain-event-publisher.port.ts +13 -0
  88. package/src/ports/outbound/index.ts +2 -0
  89. package/src/ports/outbound/session.repository.port.ts +9 -0
  90. package/src/ports/repositories/oauth-authorization.repository.ts +21 -0
  91. package/src/ports/repositories/token.repository.ts +11 -0
  92. package/src/types/auth-factor.type.ts +10 -0
  93. package/src/types/auth-method.type.ts +20 -0
  94. package/src/types/auth-policy.type.ts +16 -0
  95. package/src/types/identity-provider.type.ts +8 -0
  96. package/src/types/index.ts +6 -0
  97. package/src/types/observability-event.ts +33 -0
  98. package/src/types/risk-signal.type.ts +11 -0
  99. package/src/utils/default-if-blank.util.ts +46 -0
  100. package/tsconfig.json +4 -1
  101. package/src/domain/entities/identity.entity.ts +0 -13
  102. package/src/domain/value-objects/auth-method.vo.ts +0 -21
  103. /package/src/domain/{events → identity/events}/authentication-failed.event.ts +0 -0
  104. /package/src/domain/{events → identity/events}/authentication-started.event.ts +0 -0
  105. /package/src/domain/{events → identity/events}/authentication-succeeded.event.ts +0 -0
  106. /package/src/domain/{value-objects → identity/value-objects}/auth-status.vo.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @rineex/auth-core
2
2
 
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ [[`b1e8e3a`](https://github.com/rineex/core/commit/b1e8e3a4e02644118af82b8f068259d3e5bb2f24)]:
9
+ - @rineex/ddd@1.5.2
10
+
3
11
  ## 0.0.2
4
12
 
5
13
  ### Patch Changes
package/eslint.config.mjs CHANGED
@@ -7,6 +7,7 @@ export default [
7
7
 
8
8
  {
9
9
  rules: {
10
+ '@typescript-eslint/class-literal-property-style': ['off'],
10
11
  '@typescript-eslint/consistent-type-definitions': ['off'],
11
12
  '@typescript-eslint/consistent-type-imports': 'off',
12
13
  '@typescript-eslint/no-extraneous-class': 'off',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rineex/auth-core",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Authentication Core package for Rineex core modules",
5
5
  "author": "Rineex Team",
6
6
  "main": "./dist/index.js",
@@ -22,6 +22,8 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@changesets/cli": "2.29.8",
25
+ "@types/lodash.isempty": "4.4.9",
26
+ "@types/lodash.isnil": "4.0.9",
25
27
  "@types/node": "24.10.4",
26
28
  "@vitest/ui": "4.0.16",
27
29
  "tslib": "2.8.1",
@@ -40,6 +42,8 @@
40
42
  },
41
43
  "license": "Apache-2.0",
42
44
  "dependencies": {
45
+ "lodash.isempty": "4.4.0",
46
+ "lodash.isnil": "4.0.0",
43
47
  "@rineex/ddd": "1.5.1"
44
48
  },
45
49
  "scripts": {
@@ -0,0 +1,18 @@
1
+ import { ObservabilityEvent } from '@/types/observability-event';
2
+
3
+ export class ChallengeIssueObservabilityEvent extends ObservabilityEvent {
4
+ constructor(params: {
5
+ sessionId: string;
6
+ challengeType: string;
7
+ success?: boolean;
8
+ }) {
9
+ super({
10
+ payload: {
11
+ challengeType: params.challengeType,
12
+ sessionId: params.sessionId,
13
+ success: params.success,
14
+ },
15
+ name: 'authentication.mfa.challenge.issued',
16
+ });
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ import { ObservabilityEvent } from '@/types/observability-event';
2
+
3
+ export class SessionStartedObservabilityEvent extends ObservabilityEvent {
4
+ constructor(params: {
5
+ sessionId: string;
6
+ identityId: string;
7
+ reused?: boolean;
8
+ }) {
9
+ super({
10
+ payload: {
11
+ reused: params.reused ?? false,
12
+ identityId: params.identityId,
13
+ sessionId: params.sessionId,
14
+ },
15
+ name: 'authentication.mfa.session.started',
16
+ });
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import { ObservabilityEvent } from '@/types/observability-event';
2
+
3
+ export class VerificationFailedObservabilityEvent extends ObservabilityEvent {
4
+ constructor(sessionId: string, reasonCode: string) {
5
+ super({
6
+ payload: {
7
+ reasonCode,
8
+ sessionId,
9
+ },
10
+ name: 'authentication.mfa.verification_failed',
11
+ occurredAt: new Date(),
12
+ });
13
+ }
14
+ }
@@ -0,0 +1,12 @@
1
+ import { ObservabilityEvent } from '@/types/observability-event';
2
+
3
+ export class VerificationSucceededObservabilityEvent extends ObservabilityEvent {
4
+ constructor(params: { sessionId: string }) {
5
+ super({
6
+ payload: {
7
+ sessionId: params.sessionId,
8
+ },
9
+ name: 'authentication.mfa.verification.succeeded',
10
+ });
11
+ }
12
+ }
@@ -0,0 +1,75 @@
1
+ import { ObservabilityEventPort } from '@/ports/observability/observability-event.port';
2
+ import { MfaSessionRepository } from '@/ports/mfa/mfa-session-repository.port';
3
+ import { MfaSessionId } from '@/domain/mfa/value-objects/mfa-session-id.vo';
4
+ import { MFAChallenge } from '@/domain/mfa/entities/mfa-challenge.entity';
5
+ import { ApplicationServicePort, Result } from '@rineex/ddd';
6
+ import { MfaClock } from '@/ports/mfa/mfa-clock.port';
7
+ import { LoggerPort } from '@/ports/log/log.port';
8
+
9
+ import { ChallengeIssueObservabilityEvent } from './events/challenge-issue-observability.event';
10
+
11
+ type IssueMfaChallengeInput = {
12
+ sessionId: MfaSessionId;
13
+ challenge: MFAChallenge;
14
+ };
15
+
16
+ type IssueMfaChallengeOutput = Result<void, never>;
17
+
18
+ export class IssueMfaChallengeApplicationService implements ApplicationServicePort<
19
+ IssueMfaChallengeInput,
20
+ IssueMfaChallengeOutput
21
+ > {
22
+ constructor(
23
+ private readonly repository: MfaSessionRepository,
24
+ private readonly clock: MfaClock,
25
+ private readonly logger: LoggerPort,
26
+ private readonly events: ObservabilityEventPort,
27
+ ) {}
28
+
29
+ async execute({
30
+ challenge,
31
+ sessionId,
32
+ }: IssueMfaChallengeInput): Promise<IssueMfaChallengeOutput> {
33
+ try {
34
+ const session = await this.repository.findById(sessionId);
35
+
36
+ if (!session) {
37
+ this.logger.warn('MFA session not found', { sessionId });
38
+ throw new Error('MFA session not found'); // app-layer error
39
+ }
40
+
41
+ session.issueChallenge(challenge, this.clock.now());
42
+
43
+ await this.repository.save(session);
44
+
45
+ this.logger.info('MFA challenge issued', {
46
+ sessionId: sessionId.toString(),
47
+ challengeType: challenge,
48
+ });
49
+
50
+ this.events.emit(
51
+ new ChallengeIssueObservabilityEvent({
52
+ challengeType: challenge.props.challengeType,
53
+ sessionId: sessionId.toString(),
54
+ success: true,
55
+ }),
56
+ );
57
+
58
+ return Result.ok(undefined);
59
+ } catch (error) {
60
+ this.events.emit(
61
+ new ChallengeIssueObservabilityEvent({
62
+ challengeType: challenge.props.challengeType,
63
+ sessionId: sessionId.toString(),
64
+ success: false,
65
+ }),
66
+ );
67
+
68
+ this.logger.error('Error issuing MFA challenge', {
69
+ sessionId: sessionId.toString(),
70
+ error,
71
+ });
72
+ return Result.fail(error as never);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,90 @@
1
+ import { ObservabilityEventPort } from '@/ports/observability/observability-event.port';
2
+ import { MfaSessionIdGenerator } from '@/ports/mfa/mfa-session-id-generator.port';
3
+ import { MfaSessionRepository } from '@/ports/mfa/mfa-session-repository.port';
4
+ import { MFASession } from '@/domain/mfa/aggregates/mfa-session.aggregate';
5
+ import { ApplicationServicePort, Result } from '@rineex/ddd';
6
+ import { LoggerPort } from '@/ports/log/log.port';
7
+ import { IdentityId } from '@/index';
8
+
9
+ import { VerificationFailedObservabilityEvent } from './events/verification-failed-observability.event';
10
+ import { SessionStartedObservabilityEvent } from './events/session-started-observability.event';
11
+
12
+ type StartMFASessionInput = {
13
+ identityId: IdentityId;
14
+ maxAttempts: number;
15
+ };
16
+
17
+ type StartMFASessionOutput = Result<MFASession, never>;
18
+
19
+ export class StartMfaSessionApplicationService implements ApplicationServicePort<
20
+ StartMFASessionInput,
21
+ StartMFASessionOutput
22
+ > {
23
+ constructor(
24
+ private readonly repository: MfaSessionRepository,
25
+ private readonly idGenerator: MfaSessionIdGenerator,
26
+ private readonly logger: LoggerPort,
27
+ private readonly events: ObservabilityEventPort,
28
+ ) {}
29
+
30
+ async execute(args: StartMFASessionInput): Promise<StartMFASessionOutput> {
31
+ try {
32
+ const existing = await this.repository.findActiveByIdentity(
33
+ args.identityId,
34
+ );
35
+
36
+ if (existing) {
37
+ this.logger.info('MFA session reused', {
38
+ identityId: args.identityId.toString(),
39
+ });
40
+
41
+ this.events.emit(
42
+ new SessionStartedObservabilityEvent({
43
+ identityId: args.identityId.toString(),
44
+ sessionId: existing.id.toString(),
45
+ reused: true,
46
+ }),
47
+ );
48
+
49
+ return Result.ok(existing);
50
+ }
51
+
52
+ const session = new MFASession({
53
+ id: this.idGenerator.generate(),
54
+ maxAttempts: args.maxAttempts,
55
+ identityId: args.identityId,
56
+ attemptsUsed: 0,
57
+ challenges: [],
58
+ });
59
+
60
+ await this.repository.save(session);
61
+
62
+ this.logger.info('MFA session started', {
63
+ identityId: args.identityId.toString(),
64
+ sessionId: session.id.toString(),
65
+ });
66
+
67
+ this.events.emit(
68
+ new SessionStartedObservabilityEvent({
69
+ identityId: args.identityId.toString(),
70
+ sessionId: session.id.toString(),
71
+ reused: false,
72
+ }),
73
+ );
74
+
75
+ return Result.ok(session);
76
+ } catch (violation) {
77
+ this.events.emit(
78
+ new VerificationFailedObservabilityEvent(
79
+ args.identityId.toString(),
80
+ violation instanceof Error
81
+ ? violation.message
82
+ : 'START_MFA_SESSION_ERROR',
83
+ ),
84
+ );
85
+
86
+ this.logger.error('Error starting MFA session', { violation, ...args });
87
+ return Result.fail(violation as never);
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,61 @@
1
+ import { ObservabilityEventPort } from '@/ports/observability/observability-event.port';
2
+ import { MfaSessionRepository } from '@/ports/mfa/mfa-session-repository.port';
3
+ import { MfaSessionId } from '@/domain/mfa/value-objects/mfa-session-id.vo';
4
+ import { ApplicationServicePort, Result } from '@rineex/ddd';
5
+ import { MfaClock } from '@/ports/mfa/mfa-clock.port';
6
+ import { LoggerPort } from '@/ports/log/log.port';
7
+
8
+ import { VerificationSucceededObservabilityEvent } from './events/verification-succeeded-observibility.event';
9
+ import { VerificationFailedObservabilityEvent } from './events/verification-failed-observability.event';
10
+
11
+ type VerifyMFAInput = {
12
+ sessionId: MfaSessionId;
13
+ };
14
+
15
+ type VerifyMFAOutput = Result<void, never>;
16
+
17
+ export class VerifyMfaApplicationService implements ApplicationServicePort<
18
+ VerifyMFAInput,
19
+ VerifyMFAOutput
20
+ > {
21
+ constructor(
22
+ private readonly repository: MfaSessionRepository,
23
+ private readonly clock: MfaClock,
24
+ private readonly events: ObservabilityEventPort,
25
+
26
+ private readonly logger: LoggerPort,
27
+ ) {}
28
+
29
+ async execute({ sessionId }: VerifyMFAInput): Promise<VerifyMFAOutput> {
30
+ try {
31
+ const session = await this.repository.findById(sessionId);
32
+
33
+ if (!session) {
34
+ throw new Error('MFA session not found');
35
+ }
36
+
37
+ session.markAttempt();
38
+ session.verify(this.clock.now());
39
+
40
+ await this.repository.save(session);
41
+
42
+ this.events.emit(
43
+ new VerificationSucceededObservabilityEvent({
44
+ sessionId: session.id.toString(),
45
+ }),
46
+ );
47
+
48
+ return Result.ok(undefined);
49
+ } catch (error) {
50
+ this.events.emit(
51
+ new VerificationFailedObservabilityEvent(
52
+ sessionId.toString(),
53
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
54
+ (error as any).code ?? 'UNKNOWN',
55
+ ),
56
+ );
57
+ this.logger?.error('Error verifying MFA session', { sessionId, error });
58
+ return Result.fail(error as never);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,77 @@
1
+ import { AuthenticationAttemptRepositoryPort } from '@/ports/outbound/authentication-attempt.repository.port';
2
+ import { DomainEventPublisherPort } from '@/ports/outbound/domain-event-publisher.port';
3
+ import { StartAuthenticationCommand } from '@/ports/inbound/start-auth.command';
4
+ import { AuthAttemptId, AuthenticationAttempt, AuthMethod } from '@/domain';
5
+ import { AuthMethodPort } from '@/ports/inbound/auth-method.port';
6
+ import { AuthMethodName } from '@/types/auth-method.type';
7
+ import { ApplicationServicePort } from '@rineex/ddd';
8
+
9
+ /**
10
+ * Application service responsible for orchestrating authentication flows.
11
+ *
12
+ * This service:
13
+ * - Coordinates domain objects
14
+ * - Delegates authentication to method modules
15
+ * - Remains stateless and deterministic
16
+ *
17
+ * It is safe to use in:
18
+ * - HTTP servers
19
+ * - Workers
20
+ * - Serverless
21
+ * - CLI tools
22
+ */
23
+ export class AuthOrchestratorService implements ApplicationServicePort<
24
+ StartAuthenticationCommand,
25
+ AuthAttemptId
26
+ > {
27
+ constructor(
28
+ private readonly authMethods: readonly AuthMethodPort[],
29
+ private readonly attemptRepository: AuthenticationAttemptRepositoryPort,
30
+ private readonly eventPublisher: DomainEventPublisherPort,
31
+ private readonly idGenerator: () => AuthAttemptId,
32
+ ) {}
33
+
34
+ /**
35
+ * Starts a new authentication flow.
36
+ *
37
+ * @throws Error if method is not registered
38
+ */
39
+ async execute({
40
+ identityId,
41
+ context,
42
+ method,
43
+ }: StartAuthenticationCommand): Promise<AuthAttemptId> {
44
+ const authMethod = this.resolveAuthMethod(method);
45
+
46
+ const attemptId = this.idGenerator();
47
+
48
+ const attempt = AuthenticationAttempt.start(
49
+ attemptId,
50
+ AuthMethod.create(method),
51
+ identityId,
52
+ );
53
+
54
+ await this.attemptRepository.save(attempt);
55
+
56
+ await this.eventPublisher.publish(attempt.pullDomainEvents());
57
+
58
+ await authMethod.authenticate(context);
59
+
60
+ return attemptId;
61
+ }
62
+
63
+ /**
64
+ * Resolves an authentication method implementation.
65
+ *
66
+ * Fail-fast behavior is intentional for security reasons.
67
+ */
68
+ private resolveAuthMethod(method: AuthMethodName): AuthMethodPort {
69
+ const resolved = this.authMethods.find(m => m.method.is(method));
70
+
71
+ if (!resolved) {
72
+ throw new Error(`Authentication method not registered: ${method}`);
73
+ }
74
+
75
+ return resolved;
76
+ }
77
+ }
@@ -0,0 +1,12 @@
1
+ import { ApplicationServicePort } from '@rineex/ddd';
2
+
3
+ export class OAuthAuthorizeService implements ApplicationServicePort<any, any> {
4
+ constructor(
5
+ private readonly authorizationRepository: AuthorizationRepository,
6
+ private readonly clientRepository: ClientRepository,
7
+ ) {}
8
+
9
+ async execute(command: any): Promise<any> {
10
+ // Implementation goes here
11
+ }
12
+ }
@@ -1,4 +1,4 @@
1
- import { AggregateRoot } from '@rineex/ddd';
1
+ import { AggregateRoot, CreateEntityProps } from '@rineex/ddd';
2
2
 
3
3
  import { AuthenticationSucceededEvent } from '../events/authentication-succeeded.event';
4
4
  import { AuthenticationStartedEvent } from '../events/authentication-started.event';
@@ -8,11 +8,11 @@ import { AuthStatus } from '../value-objects/auth-status.vo';
8
8
  import { AuthMethod } from '../value-objects/auth-method.vo';
9
9
  import { IdentityId } from '../value-objects/identity-id.vo';
10
10
 
11
- type Props = {
11
+ interface AuthenticationAttemptProps extends CreateEntityProps<AuthAttemptId> {
12
12
  status: AuthStatus;
13
13
  method: AuthMethod;
14
14
  identityId?: IdentityId;
15
- };
15
+ }
16
16
 
17
17
  /**
18
18
  * Aggregate Root representing a single authentication attempt.
@@ -27,18 +27,38 @@ type Props = {
27
27
  * - Talk to infrastructure
28
28
  * - Know about HTTP or sessions
29
29
  */
30
- export class AuthenticationAttempt extends AggregateRoot<Props> {
31
- private constructor(id: AuthAttemptId, props: Props) {
30
+ export class AuthenticationAttempt extends AggregateRoot<AuthAttemptId> {
31
+ /**
32
+ * Current status of the authentication attempt.
33
+ */
34
+ private _status: AuthStatus;
35
+ /**
36
+ * Method used for authentication.
37
+ */
38
+ private _method: AuthMethod;
39
+ /**
40
+ * Optional identity being authenticated.
41
+ */
42
+ private _identityId?: IdentityId;
43
+
44
+ private constructor({ createdAt, id, ...props }: AuthenticationAttemptProps) {
32
45
  super({
33
- createdAt: new Date(),
34
- props,
46
+ createdAt,
35
47
  id,
36
48
  });
49
+ this._status = props.status;
50
+ this._method = props.method;
51
+ this._identityId = props.identityId;
37
52
  }
38
53
 
39
- // This "casts" the ID for this specific class only
40
- public override get id(): AuthAttemptId {
41
- return super.id as AuthAttemptId;
54
+ toObject(): Record<string, unknown> {
55
+ return {
56
+ identityId: this._identityId.getValue(),
57
+ createdAt: this.createdAt,
58
+ id: this.id.getValue(),
59
+ status: this._status,
60
+ method: this._method,
61
+ };
42
62
  }
43
63
 
44
64
  /**
@@ -49,10 +69,11 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
49
69
  method: AuthMethod,
50
70
  identityId?: IdentityId,
51
71
  ): AuthenticationAttempt {
52
- const attempt = new AuthenticationAttempt(id, {
72
+ const attempt = new AuthenticationAttempt({
53
73
  status: AuthStatus.create('PENDING'),
54
74
  identityId,
55
75
  method,
76
+ id,
56
77
  });
57
78
 
58
79
  attempt.addEvent(new AuthenticationStartedEvent(id, method));
@@ -66,18 +87,15 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
66
87
  * @throws Error if already completed
67
88
  */
68
89
  succeed(): void {
69
- if (this.props.status.isNot('PENDING')) {
90
+ if (this._status.isNot('PENDING')) {
70
91
  throw new Error('Authentication attempt already completed');
71
92
  }
72
93
 
73
- this.updateProps(props => ({
74
- ...props,
75
- status: AuthStatus.create('SUCCEEDED'),
76
- }));
77
-
78
- const ev = new AuthenticationSucceededEvent(this.id);
94
+ this.mutate(draft => {
95
+ draft._status = AuthStatus.create('SUCCEEDED');
96
+ });
79
97
 
80
- this.addEvent(ev);
98
+ this.addEvent(new AuthenticationSucceededEvent(this.id));
81
99
  }
82
100
 
83
101
  /**
@@ -86,7 +104,7 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
86
104
  * @throws Error if already completed
87
105
  */
88
106
  fail(reason: string): void {
89
- if (this.props.status.isNot('PENDING')) {
107
+ if (this._status.isNot('PENDING')) {
90
108
  throw new Error('Authentication attempt already completed');
91
109
  }
92
110
 
@@ -94,10 +112,9 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
94
112
  throw new Error('Failure reason must be provided');
95
113
  }
96
114
 
97
- this.updateProps(props => ({
98
- ...props,
99
- status: AuthStatus.create('FAILED'),
100
- }));
115
+ this.mutate(draft => {
116
+ draft._status = AuthStatus.create('FAILED');
117
+ });
101
118
 
102
119
  this.addEvent(new AuthenticationFailedEvent(this.id));
103
120
  }
@@ -108,11 +125,11 @@ export class AuthenticationAttempt extends AggregateRoot<Props> {
108
125
  * Called automatically before domain events are added.
109
126
  */
110
127
  validate(): void {
111
- if (!this.props.method) {
128
+ if (!this._method) {
112
129
  throw new Error('AuthenticationAttempt must have a method');
113
130
  }
114
131
 
115
- if (!this.props.status) {
132
+ if (!this._status) {
116
133
  throw new Error('AuthenticationAttempt must have a status');
117
134
  }
118
135
  }
@@ -0,0 +1 @@
1
+ export * from './authentication-attempt.aggregate';