@tstdl/base 0.93.178 → 0.93.180

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 (207) hide show
  1. package/api/response.js +4 -3
  2. package/api/server/gateway.js +9 -3
  3. package/audit/auditor.d.ts +1 -2
  4. package/audit/drizzle/{0000_lumpy_thunderball.sql → 0000_shallow_elektra.sql} +1 -1
  5. package/audit/drizzle/meta/0000_snapshot.json +2 -2
  6. package/audit/drizzle/meta/_journal.json +2 -2
  7. package/authentication/README.md +87 -42
  8. package/authentication/authentication.api.d.ts +392 -53
  9. package/authentication/authentication.api.js +133 -28
  10. package/authentication/client/api.client.d.ts +3 -3
  11. package/authentication/client/api.client.js +4 -4
  12. package/authentication/client/authentication.service.d.ts +93 -23
  13. package/authentication/client/authentication.service.js +113 -28
  14. package/authentication/client/http-client.middleware.d.ts +1 -1
  15. package/authentication/client/http-client.middleware.js +5 -4
  16. package/authentication/client/module.d.ts +1 -1
  17. package/authentication/client/module.js +2 -2
  18. package/authentication/errors/index.d.ts +1 -1
  19. package/authentication/errors/index.js +1 -1
  20. package/authentication/errors/password-requirements.error.d.ts +5 -0
  21. package/authentication/errors/{secret-requirements.error.js → password-requirements.error.js} +2 -2
  22. package/authentication/models/authentication-password.model.d.ts +8 -0
  23. package/authentication/models/{authentication-credentials.model.js → authentication-password.model.js} +11 -17
  24. package/authentication/models/authentication-session.model.d.ts +0 -2
  25. package/authentication/models/authentication-session.model.js +1 -7
  26. package/authentication/models/authentication-totp-recovery-code.model.d.ts +6 -0
  27. package/authentication/models/authentication-totp-recovery-code.model.js +34 -0
  28. package/authentication/models/authentication-totp.model.d.ts +19 -0
  29. package/authentication/models/authentication-totp.model.js +51 -0
  30. package/authentication/models/authentication-used-totp-token.model.d.ts +5 -0
  31. package/authentication/models/authentication-used-totp-token.model.js +32 -0
  32. package/authentication/models/index.d.ts +6 -3
  33. package/authentication/models/index.js +6 -3
  34. package/authentication/models/{init-secret-reset-data.model.d.ts → init-password-reset-data.model.d.ts} +3 -3
  35. package/authentication/models/{init-secret-reset-data.model.js → init-password-reset-data.model.js} +5 -5
  36. package/authentication/models/password-check-result.model.d.ts +3 -0
  37. package/authentication/models/{secret-check-result.model.js → password-check-result.model.js} +6 -6
  38. package/authentication/models/subject.model.d.ts +0 -6
  39. package/authentication/models/subject.model.js +0 -6
  40. package/authentication/models/token.model.d.ts +16 -2
  41. package/authentication/server/authentication-ancillary.service.d.ts +6 -6
  42. package/authentication/server/authentication-ancillary.service.js +1 -1
  43. package/authentication/server/authentication-password-requirements.validator.d.ts +55 -0
  44. package/authentication/server/{authentication-secret-requirements.validator.js → authentication-password-requirements.validator.js} +22 -22
  45. package/authentication/server/authentication.api-controller.d.ts +55 -27
  46. package/authentication/server/authentication.api-controller.js +214 -39
  47. package/authentication/server/authentication.audit.d.ts +42 -5
  48. package/authentication/server/authentication.service.d.ts +182 -93
  49. package/authentication/server/authentication.service.js +628 -206
  50. package/authentication/server/drizzle/{0000_soft_tag.sql → 0000_odd_echo.sql} +59 -13
  51. package/authentication/server/drizzle/meta/0000_snapshot.json +345 -32
  52. package/authentication/server/drizzle/meta/_journal.json +2 -2
  53. package/authentication/server/helper.d.ts +16 -16
  54. package/authentication/server/helper.js +33 -34
  55. package/authentication/server/index.d.ts +1 -1
  56. package/authentication/server/index.js +1 -1
  57. package/authentication/server/module.d.ts +2 -2
  58. package/authentication/server/module.js +4 -2
  59. package/authentication/server/schemas.d.ts +11 -7
  60. package/authentication/server/schemas.js +7 -3
  61. package/authentication/tests/authentication-password-requirements.validator.test.js +29 -0
  62. package/authentication/tests/authentication.api-controller.test.js +49 -15
  63. package/authentication/tests/authentication.client-error-handling.test.js +3 -2
  64. package/authentication/tests/authentication.client-middleware.test.js +5 -5
  65. package/authentication/tests/authentication.client-service-methods.test.js +28 -14
  66. package/authentication/tests/authentication.client-service-refresh.test.js +7 -6
  67. package/authentication/tests/authentication.client-service.test.js +10 -8
  68. package/authentication/tests/authentication.service.test.js +37 -29
  69. package/authentication/tests/authentication.test-ancillary-service.d.ts +1 -1
  70. package/authentication/tests/authentication.test-ancillary-service.js +1 -1
  71. package/authentication/tests/brute-force-protection.test.js +211 -0
  72. package/authentication/tests/helper.test.js +25 -21
  73. package/authentication/tests/password-requirements.error.test.js +14 -0
  74. package/authentication/tests/remember.api.test.js +22 -14
  75. package/authentication/tests/remember.service.test.js +23 -16
  76. package/authentication/tests/subject.service.test.js +2 -2
  77. package/authentication/tests/suspended-subject.test.d.ts +1 -0
  78. package/authentication/tests/suspended-subject.test.js +120 -0
  79. package/authentication/tests/totp.enrollment.test.d.ts +1 -0
  80. package/authentication/tests/totp.enrollment.test.js +123 -0
  81. package/authentication/tests/totp.login.test.d.ts +1 -0
  82. package/authentication/tests/totp.login.test.js +213 -0
  83. package/authentication/tests/totp.recovery-codes.test.d.ts +1 -0
  84. package/authentication/tests/totp.recovery-codes.test.js +97 -0
  85. package/authentication/tests/totp.status.test.d.ts +1 -0
  86. package/authentication/tests/totp.status.test.js +72 -0
  87. package/circuit-breaker/postgres/drizzle/{0000_cooing_korath.sql → 0000_same_captain_cross.sql} +1 -1
  88. package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +2 -2
  89. package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
  90. package/cryptography/cryptography.d.ts +336 -0
  91. package/cryptography/cryptography.js +328 -0
  92. package/cryptography/index.d.ts +4 -0
  93. package/cryptography/index.js +4 -0
  94. package/{utils → cryptography}/jwt.d.ts +22 -4
  95. package/{utils → cryptography}/jwt.js +36 -18
  96. package/cryptography/module.d.ts +35 -0
  97. package/cryptography/module.js +148 -0
  98. package/cryptography/tests/cryptography.test.d.ts +1 -0
  99. package/cryptography/tests/cryptography.test.js +175 -0
  100. package/cryptography/tests/jwt.test.d.ts +1 -0
  101. package/cryptography/tests/jwt.test.js +54 -0
  102. package/cryptography/tests/modern.test.d.ts +1 -0
  103. package/cryptography/tests/modern.test.js +105 -0
  104. package/cryptography/tests/module.test.d.ts +1 -0
  105. package/cryptography/tests/module.test.js +100 -0
  106. package/cryptography/tests/totp.test.d.ts +1 -0
  107. package/cryptography/tests/totp.test.js +108 -0
  108. package/cryptography/totp.d.ts +96 -0
  109. package/cryptography/totp.js +123 -0
  110. package/document-management/server/drizzle/{0000_curious_nighthawk.sql → 0000_sharp_scream.sql} +21 -21
  111. package/document-management/server/drizzle/meta/0000_snapshot.json +22 -22
  112. package/document-management/server/drizzle/meta/_journal.json +2 -2
  113. package/document-management/server/services/document-file.service.js +1 -1
  114. package/errors/errors.localization.d.ts +2 -2
  115. package/errors/errors.localization.js +2 -2
  116. package/errors/index.d.ts +1 -0
  117. package/errors/index.js +1 -0
  118. package/errors/too-many-requests.error.d.ts +5 -0
  119. package/errors/too-many-requests.error.js +7 -0
  120. package/examples/api/authentication.js +5 -5
  121. package/examples/api/custom-authentication.js +4 -3
  122. package/file/server/mime-type.js +1 -1
  123. package/http/http-body.d.ts +1 -0
  124. package/http/http-body.js +3 -0
  125. package/image-service/imgproxy/imgproxy-image-service.d.ts +0 -1
  126. package/image-service/imgproxy/imgproxy-image-service.js +9 -27
  127. package/key-value-store/postgres/drizzle/{0000_shocking_slipstream.sql → 0000_moaning_calypso.sql} +1 -1
  128. package/key-value-store/postgres/drizzle/meta/0000_snapshot.json +2 -2
  129. package/key-value-store/postgres/drizzle/meta/_journal.json +2 -2
  130. package/lock/postgres/drizzle/{0000_busy_tattoo.sql → 0000_nappy_wraith.sql} +1 -1
  131. package/lock/postgres/drizzle/meta/0000_snapshot.json +2 -2
  132. package/lock/postgres/drizzle/meta/_journal.json +2 -2
  133. package/logger/formatters/json.js +1 -1
  134. package/logger/formatters/pretty-print.js +1 -1
  135. package/mail/drizzle/{0000_numerous_the_watchers.sql → 0000_cultured_quicksilver.sql} +2 -2
  136. package/mail/drizzle/meta/0000_snapshot.json +4 -4
  137. package/mail/drizzle/meta/_journal.json +2 -9
  138. package/notification/server/drizzle/{0000_wise_pyro.sql → 0000_new_tenebrous.sql} +6 -6
  139. package/notification/server/drizzle/meta/0000_snapshot.json +7 -7
  140. package/notification/server/drizzle/meta/_journal.json +2 -2
  141. package/notification/tests/notification-flow.test.js +1 -8
  142. package/notification/tests/notification-type.service.test.js +3 -3
  143. package/openid-connect/oidc.service.js +2 -3
  144. package/orm/data-types/common.js +1 -1
  145. package/orm/server/drizzle/schema-converter.js +9 -4
  146. package/orm/server/encryption.js +1 -1
  147. package/orm/server/module.d.ts +0 -1
  148. package/orm/server/module.js +0 -4
  149. package/orm/server/repository.d.ts +2 -1
  150. package/orm/server/repository.js +7 -10
  151. package/orm/tests/encryption.test.js +4 -6
  152. package/orm/tests/repository-extra-coverage.test.js +0 -2
  153. package/orm/tests/repository-regression.test.js +0 -3
  154. package/package.json +9 -8
  155. package/password/README.md +1 -1
  156. package/password/have-i-been-pwned.js +1 -1
  157. package/rate-limit/postgres/drizzle/{0000_watery_rage.sql → 0000_serious_sauron.sql} +1 -1
  158. package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +2 -2
  159. package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
  160. package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -1
  161. package/rate-limit/postgres/postgres-rate-limiter.js +1 -1
  162. package/rate-limit/rate-limiter.d.ts +1 -1
  163. package/rpc/tests/rpc.integration.test.js +25 -31
  164. package/supports.d.ts +1 -0
  165. package/supports.js +1 -0
  166. package/task-queue/postgres/drizzle/{0000_faithful_daimon_hellstrom.sql → 0000_dark_ronan.sql} +5 -5
  167. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +10 -10
  168. package/task-queue/postgres/drizzle/meta/_journal.json +2 -9
  169. package/task-queue/postgres/task-queue.js +2 -2
  170. package/task-queue/tests/coverage-enhancement.test.js +2 -2
  171. package/test/drizzle/{0000_natural_cannonball.sql → 0000_organic_gamora.sql} +2 -2
  172. package/test/drizzle/meta/0000_snapshot.json +3 -4
  173. package/test/drizzle/meta/_journal.json +2 -9
  174. package/testing/integration-setup.d.ts +7 -3
  175. package/testing/integration-setup.js +119 -96
  176. package/utils/alphabet.d.ts +1 -0
  177. package/utils/alphabet.js +1 -0
  178. package/utils/base32.d.ts +4 -0
  179. package/utils/base32.js +49 -0
  180. package/utils/base64.d.ts +0 -2
  181. package/utils/base64.js +6 -70
  182. package/utils/equals.d.ts +13 -3
  183. package/utils/equals.js +29 -9
  184. package/utils/index.d.ts +1 -2
  185. package/utils/index.js +1 -2
  186. package/utils/random.d.ts +1 -0
  187. package/utils/random.js +14 -8
  188. package/authentication/errors/secret-requirements.error.d.ts +0 -5
  189. package/authentication/models/authentication-credentials.model.d.ts +0 -10
  190. package/authentication/models/secret-check-result.model.d.ts +0 -3
  191. package/authentication/server/authentication-secret-requirements.validator.d.ts +0 -55
  192. package/authentication/tests/authentication-ancillary.service.test.js +0 -13
  193. package/authentication/tests/authentication-secret-requirements.validator.test.js +0 -29
  194. package/authentication/tests/secret-requirements.error.test.js +0 -14
  195. package/mail/drizzle/0001_married_tarantula.sql +0 -12
  196. package/mail/drizzle/meta/0001_snapshot.json +0 -69
  197. package/orm/server/tokens.d.ts +0 -1
  198. package/orm/server/tokens.js +0 -2
  199. package/task-queue/postgres/drizzle/0001_rapid_infant_terrible.sql +0 -16
  200. package/task-queue/postgres/drizzle/meta/0001_snapshot.json +0 -753
  201. package/test/drizzle/0001_closed_the_captain.sql +0 -2
  202. package/test/drizzle/meta/0001_snapshot.json +0 -117
  203. package/utils/cryptography.d.ts +0 -137
  204. package/utils/cryptography.js +0 -201
  205. /package/authentication/tests/{authentication-ancillary.service.test.d.ts → authentication-password-requirements.validator.test.d.ts} +0 -0
  206. /package/authentication/tests/{authentication-secret-requirements.validator.test.d.ts → brute-force-protection.test.d.ts} +0 -0
  207. /package/authentication/tests/{secret-requirements.error.test.d.ts → password-requirements.error.test.d.ts} +0 -0
@@ -19,8 +19,10 @@ describe('AuthenticationApiController Remember Functionality', () => {
19
19
  const tenantId = crypto.randomUUID();
20
20
  beforeAll(async () => {
21
21
  ({ injector, database } = await setupIntegrationTest({
22
- modules: { authentication: true, audit: true, keyValueStore: true },
23
- authenticationAncillaryService: DefaultAuthenticationAncillaryService,
22
+ modules: { authentication: true, audit: true, keyValueStore: true, signals: true, api: true },
23
+ authentication: {
24
+ ancillaryService: DefaultAuthenticationAncillaryService,
25
+ },
24
26
  }));
25
27
  authenticationService = await injector.resolveAsync(AuthenticationService);
26
28
  subjectService = await injector.resolveAsync(SubjectService);
@@ -31,13 +33,14 @@ describe('AuthenticationApiController Remember Functionality', () => {
31
33
  await injector?.dispose();
32
34
  });
33
35
  beforeEach(async () => {
34
- await clearTenantData(database, schema, ['credentials', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
36
+ await clearTenantData(database, schema, ['used_totp_tokens', 'totp_recovery_code', 'totp', 'password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
35
37
  });
36
38
  test('login with remember: true should have Expires in cookies', async () => {
37
39
  const user = await subjectService.createUser({ tenantId, email: 'api-rem@example.com', firstName: 'A', lastName: 'L' });
38
- await authenticationService.setCredentials(user, 'Pass-R3m3mb3r-2026!');
40
+ await authenticationService.setPassword(user, 'Pass-R3m3mb3r-2026!');
39
41
  const context = {
40
- parameters: { tenantId, subject: user.id, secret: 'Pass-R3m3mb3r-2026!', remember: true, data: undefined },
42
+ request: { ip: '127.0.0.1' },
43
+ parameters: { tenantId, subject: user.id, password: 'Pass-R3m3mb3r-2026!', remember: true, data: undefined },
41
44
  getAuditor: async () => auditor,
42
45
  };
43
46
  const response = await controller.login(context);
@@ -50,9 +53,10 @@ describe('AuthenticationApiController Remember Functionality', () => {
50
53
  });
51
54
  test('login with remember: false should NOT have Expires in cookies', async () => {
52
55
  const user = await subjectService.createUser({ tenantId, email: 'api-no-rem@example.com', firstName: 'A', lastName: 'L' });
53
- await authenticationService.setCredentials(user, 'Pass-R3m3mb3r-2026!');
56
+ await authenticationService.setPassword(user, 'Pass-R3m3mb3r-2026!');
54
57
  const context = {
55
- parameters: { tenantId, subject: user.id, secret: 'Pass-R3m3mb3r-2026!', remember: false, data: undefined },
58
+ request: { ip: '127.0.0.1' },
59
+ parameters: { tenantId, subject: user.id, password: 'Pass-R3m3mb3r-2026!', remember: false, data: undefined },
56
60
  getAuditor: async () => auditor,
57
61
  };
58
62
  const response = await controller.login(context);
@@ -64,18 +68,20 @@ describe('AuthenticationApiController Remember Functionality', () => {
64
68
  });
65
69
  test('refresh should propagate remember status to cookies', async () => {
66
70
  const user = await subjectService.createUser({ tenantId, email: 'api-refresh-rem@example.com', firstName: 'A', lastName: 'L' });
67
- await authenticationService.setCredentials(user, 'Pass-R3m3mb3r-2026!');
71
+ await authenticationService.setPassword(user, 'Pass-R3m3mb3r-2026!');
68
72
  // 1. Login with remember: true
69
73
  const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Pass-R3m3mb3r-2026!', undefined, auditor, true);
74
+ expect(loginResult.type).toBe('success');
70
75
  // 2. Refresh
71
76
  const context = {
72
77
  request: {
78
+ ip: '127.0.0.1',
73
79
  headers: new HttpHeaders({
74
- 'X-Refresh-Token': `Bearer ${loginResult.refreshToken}`
80
+ 'X-Refresh-Token': `Bearer ${loginResult.result.refreshToken}`,
75
81
  }),
76
82
  cookies: {
77
- tryGet: () => undefined
78
- }
83
+ tryGet: () => undefined,
84
+ },
79
85
  },
80
86
  parameters: { data: undefined },
81
87
  getAuditor: async () => auditor,
@@ -87,15 +93,17 @@ describe('AuthenticationApiController Remember Functionality', () => {
87
93
  }
88
94
  // 3. Login with remember: false
89
95
  const loginResultNoRem = await authenticationService.login({ tenantId, subject: user.id }, 'Pass-R3m3mb3r-2026!', undefined, auditor, false);
96
+ expect(loginResultNoRem.type).toBe('success');
90
97
  // 4. Refresh
91
98
  const contextNoRem = {
92
99
  request: {
100
+ ip: '127.0.0.1',
93
101
  headers: new HttpHeaders({
94
- 'X-Refresh-Token': `Bearer ${loginResultNoRem.refreshToken}`
102
+ 'X-Refresh-Token': `Bearer ${loginResultNoRem.result.refreshToken}`,
95
103
  }),
96
104
  cookies: {
97
- tryGet: () => undefined
98
- }
105
+ tryGet: () => undefined,
106
+ },
99
107
  },
100
108
  parameters: { data: undefined },
101
109
  getAuditor: async () => auditor,
@@ -2,7 +2,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest'
2
2
  import { Auditor } from '../../audit/index.js';
3
3
  import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
4
4
  import { AuthenticationService } from '../server/authentication.service.js';
5
- import { getRefreshTokenFromString } from '../server/helper.js';
6
5
  import { SubjectService } from '../server/subject.service.js';
7
6
  import { DefaultAuthenticationAncillaryService } from './authentication.test-ancillary-service.js';
8
7
  describe('AuthenticationService Remember Functionality', () => {
@@ -16,7 +15,9 @@ describe('AuthenticationService Remember Functionality', () => {
16
15
  beforeAll(async () => {
17
16
  ({ injector, database } = await setupIntegrationTest({
18
17
  modules: { authentication: true },
19
- authenticationAncillaryService: DefaultAuthenticationAncillaryService,
18
+ authentication: {
19
+ ancillaryService: DefaultAuthenticationAncillaryService,
20
+ },
20
21
  }));
21
22
  authenticationService = await injector.resolveAsync(AuthenticationService);
22
23
  subjectService = await injector.resolveAsync(SubjectService);
@@ -26,7 +27,7 @@ describe('AuthenticationService Remember Functionality', () => {
26
27
  await injector?.dispose();
27
28
  });
28
29
  beforeEach(async () => {
29
- await clearTenantData(database, schema, ['credentials', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
30
+ await clearTenantData(database, schema, ['password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
30
31
  });
31
32
  test('RefreshToken should contain remember flag', async () => {
32
33
  const user = await subjectService.createUser({
@@ -36,7 +37,7 @@ describe('AuthenticationService Remember Functionality', () => {
36
37
  lastName: 'Ember',
37
38
  });
38
39
  const tokenResult = await authenticationService.getToken(user, undefined, { remember: true });
39
- const refreshToken = await getRefreshTokenFromString(tokenResult.refreshToken, authenticationService.derivedRefreshTokenSigningSecret);
40
+ const refreshToken = await authenticationService.validateRefreshToken(tokenResult.refreshToken);
40
41
  expect(refreshToken.payload).toHaveProperty('remember', true);
41
42
  expect(tokenResult.remember).toBe(true);
42
43
  });
@@ -44,33 +45,39 @@ describe('AuthenticationService Remember Functionality', () => {
44
45
  const user = await subjectService.createUser({ tenantId, email: 'ttl@example.com', firstName: 'T', lastName: 'L' });
45
46
  const normalResult = await authenticationService.getToken(user, undefined, { remember: false });
46
47
  const rememberResult = await authenticationService.getToken(user, undefined, { remember: true });
47
- const normalToken = await getRefreshTokenFromString(normalResult.refreshToken, authenticationService.derivedRefreshTokenSigningSecret);
48
- const rememberToken = await getRefreshTokenFromString(rememberResult.refreshToken, authenticationService.derivedRefreshTokenSigningSecret);
48
+ const normalToken = await authenticationService.validateRefreshToken(normalResult.refreshToken);
49
+ const rememberToken = await authenticationService.validateRefreshToken(rememberResult.refreshToken);
49
50
  expect(rememberToken.payload.exp).toBeGreaterThan(normalToken.payload.exp);
50
51
  });
51
52
  test('login should pass remember flag to getToken', async () => {
52
53
  const user = await subjectService.createUser({ tenantId, email: 'login-rem@example.com', firstName: 'L', lastName: 'R' });
53
- await authenticationService.setCredentials(user, 'Strong-Password-2026!');
54
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
54
55
  const result = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor, true);
55
- expect(result.remember).toBe(true);
56
- const refreshToken = await getRefreshTokenFromString(result.refreshToken, authenticationService.derivedRefreshTokenSigningSecret);
56
+ expect(result.type).toBe('success');
57
+ const tokenResult = result.result;
58
+ expect(tokenResult.remember).toBe(true);
59
+ const refreshToken = await authenticationService.validateRefreshToken(tokenResult.refreshToken);
57
60
  expect(refreshToken.payload.remember).toBe(true);
58
61
  });
59
62
  test('refresh should propagate remember flag', async () => {
60
63
  const user = await subjectService.createUser({ tenantId, email: 'refresh-rem@example.com', firstName: 'R', lastName: 'R' });
61
- await authenticationService.setCredentials(user, 'Strong-Password-2026!');
64
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
62
65
  const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor, true);
63
- expect(loginResult.remember).toBe(true);
64
- const refreshResult = await authenticationService.refresh(loginResult.refreshToken, undefined, {}, auditor);
66
+ expect(loginResult.type).toBe('success');
67
+ const loginSuccessResult = loginResult.result;
68
+ expect(loginSuccessResult.remember).toBe(true);
69
+ const refreshResult = await authenticationService.refresh(loginSuccessResult.refreshToken, undefined, {}, auditor);
65
70
  expect(refreshResult.remember).toBe(true);
66
- const newRefreshToken = await getRefreshTokenFromString(refreshResult.refreshToken, authenticationService.derivedRefreshTokenSigningSecret);
71
+ const newRefreshToken = await authenticationService.validateRefreshToken(refreshResult.refreshToken);
67
72
  expect(newRefreshToken.payload.remember).toBe(true);
68
73
  // Verify it also works when not remembered
69
74
  const loginResultNoRem = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor, false);
70
- expect(loginResultNoRem.remember).toBe(false);
71
- const refreshResultNoRem = await authenticationService.refresh(loginResultNoRem.refreshToken, undefined, {}, auditor);
75
+ expect(loginResultNoRem.type).toBe('success');
76
+ const loginSuccessResultNoRem = loginResultNoRem.result;
77
+ expect(loginSuccessResultNoRem.remember).toBe(false);
78
+ const refreshResultNoRem = await authenticationService.refresh(loginSuccessResultNoRem.refreshToken, undefined, {}, auditor);
72
79
  expect(refreshResultNoRem.remember).toBe(false);
73
- const newRefreshTokenNoRem = await getRefreshTokenFromString(refreshResultNoRem.refreshToken, authenticationService.derivedRefreshTokenSigningSecret);
80
+ const newRefreshTokenNoRem = await authenticationService.validateRefreshToken(refreshResultNoRem.refreshToken);
74
81
  expect(newRefreshTokenNoRem.payload.remember).toBe(false);
75
82
  });
76
83
  });
@@ -92,11 +92,11 @@ describe('SubjectService', () => {
92
92
  test('updateUser should update user details', async () => {
93
93
  await runInInjectionContext(injector, async () => {
94
94
  const user = await subjectService.createUser({ tenantId, email: 'update@example.com', firstName: 'Old', lastName: 'Name' });
95
- await subjectService.updateUser(tenantId, user.id, { firstName: 'New', status: SubjectStatus.Inactive });
95
+ await subjectService.updateUser(tenantId, user.id, { firstName: 'New', status: SubjectStatus.Suspended });
96
96
  const updated = await subjectService.getUser(tenantId, user.id);
97
97
  expect(updated.firstName).toBe('New');
98
98
  expect(updated.lastName).toBe('Name');
99
- expect(updated.status).toBe(SubjectStatus.Inactive);
99
+ expect(updated.status).toBe(SubjectStatus.Suspended);
100
100
  });
101
101
  });
102
102
  test('updateUserEmail should update user email', async () => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest';
2
+ import { ActorType, Auditor } from '../../audit/index.js';
3
+ import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
4
+ import { SubjectStatus } from '../models/index.js';
5
+ import { AuthenticationAncillaryService } from '../server/authentication-ancillary.service.js';
6
+ import { AuthenticationService } from '../server/authentication.service.js';
7
+ import { SubjectService } from '../server/subject.service.js';
8
+ import { DefaultAuthenticationAncillaryService } from './authentication.test-ancillary-service.js';
9
+ describe('Suspended Subject Authentication', () => {
10
+ let injector;
11
+ let database;
12
+ let authenticationService;
13
+ let subjectService;
14
+ let auditor;
15
+ const schema = 'authentication';
16
+ const tenantId = crypto.randomUUID();
17
+ beforeAll(async () => {
18
+ ({ injector, database } = await setupIntegrationTest({
19
+ modules: { authentication: true },
20
+ authentication: {
21
+ ancillaryService: DefaultAuthenticationAncillaryService,
22
+ },
23
+ }));
24
+ authenticationService = await injector.resolveAsync(AuthenticationService);
25
+ subjectService = await injector.resolveAsync(SubjectService);
26
+ auditor = injector.resolve(Auditor);
27
+ });
28
+ afterAll(async () => {
29
+ await injector?.dispose();
30
+ });
31
+ beforeEach(async () => {
32
+ await clearTenantData(database, schema, ['totp_recovery_code', 'totp', 'password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
33
+ });
34
+ test('login should FAIL for suspended user', async () => {
35
+ const user = await subjectService.createUser({
36
+ tenantId,
37
+ email: 'suspended@example.com',
38
+ firstName: 'Suspended',
39
+ lastName: 'User',
40
+ status: SubjectStatus.Suspended,
41
+ });
42
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
43
+ const loginPromise = authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor.with({ actor: user.id, actorType: ActorType.Subject }));
44
+ await expect(loginPromise).rejects.toThrow('Invalid credentials.');
45
+ });
46
+ test('refresh should FAIL for suspended user', async () => {
47
+ const user = await subjectService.createUser({
48
+ tenantId,
49
+ email: 'suspended-refresh@example.com',
50
+ firstName: 'Suspended',
51
+ lastName: 'Refresh',
52
+ status: SubjectStatus.Active, // Start active to get a token
53
+ });
54
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
55
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
56
+ const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
57
+ expect(loginResult.type).toBe('success');
58
+ // Now suspend the user
59
+ await subjectService.updateUser(tenantId, user.id, { status: SubjectStatus.Suspended });
60
+ const refreshPromise = authenticationService.refresh(loginResult.result.refreshToken, undefined, {}, userAuditor);
61
+ await expect(refreshPromise).rejects.toThrow('Subject is suspended.');
62
+ });
63
+ test('impersonate should FAIL if target is suspended', async () => {
64
+ const admin = await subjectService.createUser({ tenantId, email: 'admin@example.com', firstName: 'A', lastName: 'D' });
65
+ const user = await subjectService.createUser({
66
+ tenantId,
67
+ email: 'suspended-target@example.com',
68
+ firstName: 'S',
69
+ lastName: 'T',
70
+ status: SubjectStatus.Suspended,
71
+ });
72
+ const adminAuditor = auditor.with({ actor: admin.id, actorType: ActorType.Subject });
73
+ const adminToken = await authenticationService.getToken(admin, undefined);
74
+ const impersonatePromise = authenticationService.impersonate(adminToken.token, adminToken.refreshToken, user.id, undefined, adminAuditor);
75
+ await expect(impersonatePromise).rejects.toThrow('Subject is suspended.');
76
+ });
77
+ test('changePassword should FAIL for suspended user', async () => {
78
+ const user = await subjectService.createUser({
79
+ tenantId,
80
+ email: 'suspended-change@example.com',
81
+ firstName: 'S',
82
+ lastName: 'C',
83
+ status: SubjectStatus.Suspended,
84
+ });
85
+ await authenticationService.setPassword(user, 'Old-Password-2026!');
86
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
87
+ const changePasswordPromise = authenticationService.changePassword({ tenantId, subject: user.id }, 'Old-Password-2026!', 'New-Password-2026!', userAuditor);
88
+ await expect(changePasswordPromise).rejects.toThrow('Invalid credentials.');
89
+ });
90
+ test('resetPassword should FAIL for suspended user', async () => {
91
+ const user = await subjectService.createUser({
92
+ tenantId,
93
+ email: 'suspended-reset@example.com',
94
+ firstName: 'S',
95
+ lastName: 'R',
96
+ status: SubjectStatus.Active,
97
+ });
98
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
99
+ // @ts-ignore - access private for test or use public init
100
+ const passwordResetToken = await authenticationService.createPasswordResetToken(user, Date.now() + 100000);
101
+ // Suspend after token issue
102
+ await subjectService.updateUser(tenantId, user.id, { status: SubjectStatus.Suspended });
103
+ const resetPasswordPromise = authenticationService.resetPassword(passwordResetToken.token, 'New-Password-Reset-2026!', userAuditor);
104
+ await expect(resetPasswordPromise).rejects.toThrow('Subject is suspended.');
105
+ });
106
+ test('initPasswordReset should SILENTLY IGNORE suspended user', async () => {
107
+ const user = await subjectService.createUser({
108
+ tenantId,
109
+ email: 'suspended-init-reset@example.com',
110
+ firstName: 'S',
111
+ lastName: 'IR',
112
+ status: SubjectStatus.Suspended,
113
+ });
114
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
115
+ const ancillaryService = await injector.resolveAsync(AuthenticationAncillaryService);
116
+ ancillaryService.lastResetData = undefined;
117
+ await authenticationService.initPasswordReset({ tenantId, subject: user.id }, undefined, userAuditor);
118
+ expect(ancillaryService.lastResetData).toBeUndefined();
119
+ });
120
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest';
2
+ import { ActorType, Auditor } from '../../audit/index.js';
3
+ import { importHmacKey, importKey } from '../../cryptography/index.js';
4
+ import { generateTotpToken } from '../../cryptography/totp.js';
5
+ import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
6
+ import { currentTimestampSeconds } from '../../utils/date-time.js';
7
+ import { TotpStatus } from '../models/authentication-totp.model.js';
8
+ import { AuthenticationService } from '../server/authentication.service.js';
9
+ import { SubjectService } from '../server/subject.service.js';
10
+ import { DefaultAuthenticationAncillaryService } from './authentication.test-ancillary-service.js';
11
+ describe('TOTP Enrollment', () => {
12
+ let injector;
13
+ let database;
14
+ let authenticationService;
15
+ let subjectService;
16
+ let auditor;
17
+ const schema = 'authentication';
18
+ const tenantId = crypto.randomUUID();
19
+ beforeAll(async () => {
20
+ ({ injector, database } = await setupIntegrationTest({
21
+ modules: { authentication: true },
22
+ authentication: {
23
+ ancillaryService: DefaultAuthenticationAncillaryService,
24
+ },
25
+ }));
26
+ authenticationService = await injector.resolveAsync(AuthenticationService);
27
+ subjectService = await injector.resolveAsync(SubjectService);
28
+ auditor = injector.resolve(Auditor);
29
+ });
30
+ afterAll(async () => {
31
+ await injector?.dispose();
32
+ });
33
+ beforeEach(async () => {
34
+ await clearTenantData(database, schema, ['used_totp_tokens', 'totp_recovery_code', 'totp', 'password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
35
+ });
36
+ test('initEnrollTotp should initiate enrollment', async () => {
37
+ const user = await subjectService.createUser({
38
+ tenantId,
39
+ email: 'totp-test@example.com',
40
+ firstName: 'Totp',
41
+ lastName: 'User',
42
+ });
43
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
44
+ const result = await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
45
+ expect(result.secret).toBeDefined();
46
+ expect(result.uri).toContain('otpauth://totp/');
47
+ expect(result.uri).toContain('secret=' + result.secret);
48
+ const totpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
49
+ expect(totpEntry).toBeDefined();
50
+ expect(totpEntry.status).toBe(TotpStatus.Pending);
51
+ });
52
+ test('completeEnrollTotp should activate TOTP after verification', async () => {
53
+ const user = await subjectService.createUser({
54
+ tenantId,
55
+ email: 'totp-test-complete@example.com',
56
+ firstName: 'Totp',
57
+ lastName: 'Complete',
58
+ });
59
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
60
+ const { secret } = await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
61
+ // We need to convert base32 secret back to Uint8Array for generateTotpToken if it's base32
62
+ // But the utility probably expects Uint8Array or we use the utility's own secret generation
63
+ // Actually initEnrollTotp returns base32 secret for the user to enter in app.
64
+ // For testing, let's assume we can get the raw secret from the DB or similar.
65
+ const totpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
66
+ const secretKey = await importKey('raw-secret', totpEntry.secret, { name: 'HMAC', hash: authenticationService.getTotpOptions().codeHashAlgorithm }, false, ['sign']);
67
+ const token = await generateTotpToken(secretKey, authenticationService.getTotpOptions());
68
+ const completeResult = await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
69
+ expect(completeResult.recoveryCodes).toHaveLength(10);
70
+ const activeTotpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
71
+ expect(activeTotpEntry.status).toBe(TotpStatus.Active);
72
+ });
73
+ test('disableTotp should deactivate TOTP', async () => {
74
+ const user = await subjectService.createUser({
75
+ tenantId,
76
+ email: 'totp-test-disable@example.com',
77
+ firstName: 'Totp',
78
+ lastName: 'Disable',
79
+ });
80
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
81
+ await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
82
+ const totpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
83
+ const secretKey = await importKey('raw-secret', totpEntry.secret, { name: 'HMAC', hash: authenticationService.getTotpOptions().codeHashAlgorithm }, false, ['sign']);
84
+ const token = await generateTotpToken(secretKey, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
85
+ await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
86
+ const token2 = await generateTotpToken(secretKey, authenticationService.getTotpOptions());
87
+ await authenticationService.disableTotp(tenantId, user.id, token2, userAuditor);
88
+ const disabledTotpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
89
+ expect(disabledTotpEntry).toBeUndefined();
90
+ });
91
+ test('disableTotp should fail with recovery code (decoupled)', async () => {
92
+ const user = await subjectService.createUser({
93
+ tenantId,
94
+ email: 'totp-test-disable-fail@example.com',
95
+ firstName: 'Totp',
96
+ lastName: 'DisableFail',
97
+ });
98
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
99
+ await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
100
+ const totpEntry = (await authenticationService.tryGetTotp(tenantId, user.id));
101
+ const secretKey = await importHmacKey('raw-secret', authenticationService.getTotpOptions().codeHashAlgorithm, totpEntry.secret, false);
102
+ const token = await generateTotpToken(secretKey, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
103
+ const { recoveryCodes } = await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
104
+ await expect(authenticationService.disableTotp(tenantId, user.id, recoveryCodes[0], userAuditor)).rejects.toThrow();
105
+ });
106
+ test('disableTotpWithRecoveryCode should deactivate TOTP', async () => {
107
+ const user = await subjectService.createUser({
108
+ tenantId,
109
+ email: 'totp-test-disable-recovery@example.com',
110
+ firstName: 'Totp',
111
+ lastName: 'DisableRecovery',
112
+ });
113
+ const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
114
+ await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
115
+ const totpEntry = (await authenticationService.tryGetTotp(tenantId, user.id));
116
+ const secretKey = await importHmacKey('raw-secret', authenticationService.getTotpOptions().codeHashAlgorithm, totpEntry.secret, false);
117
+ const token = await generateTotpToken(secretKey, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
118
+ const { recoveryCodes } = await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
119
+ await authenticationService.disableTotpWithRecoveryCode(tenantId, user.id, recoveryCodes[0], userAuditor);
120
+ const disabledTotpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
121
+ expect(disabledTotpEntry).toBeUndefined();
122
+ });
123
+ });
@@ -0,0 +1 @@
1
+ export {};