@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
@@ -8,6 +8,7 @@ import { Lock } from '../../lock/index.js';
8
8
  import { Logger } from '../../logger/index.js';
9
9
  import { MessageBus } from '../../message-bus/index.js';
10
10
  import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
11
+ import { currentTimestampSeconds } from '../../utils/date-time.js';
11
12
  import { timeout } from '../../utils/timing.js';
12
13
  describe('AuthenticationClientService Refresh Loop Reproduction', () => {
13
14
  let injector;
@@ -30,7 +31,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
30
31
  mockApiClient = {
31
32
  login: vi.fn(),
32
33
  refresh: vi.fn(),
33
- timestamp: vi.fn().mockResolvedValue(Math.floor(Date.now() / 1000)),
34
+ timestamp: vi.fn().mockResolvedValue(currentTimestampSeconds()),
34
35
  endSession: vi.fn().mockResolvedValue(undefined),
35
36
  };
36
37
  mockLock = {
@@ -66,7 +67,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
66
67
  });
67
68
  test('Zombie Timer: loop should wake up immediately when token changes', async () => {
68
69
  // 1. Mock a long expiration
69
- const now = Math.floor(Date.now() / 1000);
70
+ const now = currentTimestampSeconds();
70
71
  const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
71
72
  // Set in storage so initialize() (called by resolve) loads it
72
73
  globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
@@ -82,7 +83,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
82
83
  expect(mockApiClient.refresh).toHaveBeenCalled();
83
84
  });
84
85
  test('Forced Refresh Loss: forceRefreshToken should not be cleared on failure', async () => {
85
- const now = Math.floor(Date.now() / 1000);
86
+ const now = currentTimestampSeconds();
86
87
  const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
87
88
  globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
88
89
  service = injector.resolve(AuthenticationClientService);
@@ -96,7 +97,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
96
97
  expect(service.forceRefreshRequested()).toBe(true); // Should STILL be set
97
98
  });
98
99
  test('Lock Contention Backoff: should wait 5 seconds and not busy-loop', async () => {
99
- const now = Math.floor(Date.now() / 1000);
100
+ const now = currentTimestampSeconds();
100
101
  const initialToken = { iat: now - 3600, exp: now + 5, jti: 'initial' }; // Expiring soon
101
102
  globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
102
103
  // 1. Mock lock already held
@@ -111,7 +112,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
111
112
  expect(duration).toBeLessThan(500);
112
113
  });
113
114
  test('Busy Loop: should not busy loop when forceRefreshToken is set and lock is held', async () => {
114
- const now = Math.floor(Date.now() / 1000);
115
+ const now = currentTimestampSeconds();
115
116
  const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
116
117
  globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
117
118
  // Mock lock already held
@@ -131,7 +132,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
131
132
  expect(mockApiClient.timestamp).toHaveBeenCalled();
132
133
  });
133
134
  test('Cross-tab Sync: should not refresh if another tab already did (simulated via localStorage update)', async () => {
134
- const now = Math.floor(Date.now() / 1000);
135
+ const now = currentTimestampSeconds();
135
136
  const initialToken = { iat: now - 3600, exp: now + 5, jti: 'initial' }; // Expiring soon
136
137
  globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
137
138
  // 1. Mock lock behavior to simulate another tab refreshing while we wait for the lock
@@ -25,8 +25,10 @@ describe('AuthenticationClientService Integration', () => {
25
25
  clear: vi.fn(() => storage.clear()),
26
26
  };
27
27
  ({ injector, database } = await setupIntegrationTest({
28
- modules: { authentication: true, audit: true, keyValueStore: true, signals: true, api: true, webServer: true },
29
- authenticationAncillaryService: DefaultAuthenticationAncillaryService,
28
+ modules: { authentication: true, audit: true, keyValueStore: true, signals: true, api: true },
29
+ authentication: {
30
+ ancillaryService: DefaultAuthenticationAncillaryService,
31
+ },
30
32
  }));
31
33
  await runInInjectionContext(injector, async () => {
32
34
  server = injector.resolve(HttpServer);
@@ -44,11 +46,11 @@ describe('AuthenticationClientService Integration', () => {
44
46
  });
45
47
  beforeEach(async () => {
46
48
  globalThis.localStorage?.clear();
47
- await clearTenantData(database, schema, ['credentials', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
49
+ await clearTenantData(database, schema, ['password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
48
50
  });
49
51
  test('login and logout should work', async () => {
50
52
  const user = await subjectService.createUser({ tenantId, email: 'client-test@example.com', firstName: 'C', lastName: 'T' });
51
- await serverService.setCredentials(user, 'Strong-Pass-2026!');
53
+ await serverService.setPassword(user, 'Strong-Pass-2026!');
52
54
  expect(service.isLoggedIn()).toBe(false);
53
55
  await service.login({ tenantId, subject: user.id }, 'Strong-Pass-2026!');
54
56
  expect(service.isLoggedIn()).toBe(true);
@@ -58,17 +60,17 @@ describe('AuthenticationClientService Integration', () => {
58
60
  });
59
61
  test('refresh should work', async () => {
60
62
  const user = await subjectService.createUser({ tenantId, email: 'refresh-test@example.com', firstName: 'R', lastName: 'T' });
61
- await serverService.setCredentials(user, 'Strong-Pass-2026!');
63
+ await serverService.setPassword(user, 'Strong-Pass-2026!');
62
64
  await service.login({ tenantId, subject: user.id }, 'Strong-Pass-2026!');
63
65
  const initialToken = service.token()?.jti;
64
66
  await service.refresh();
65
67
  expect(service.token()?.jti).not.toBe(initialToken);
66
68
  expect(service.isLoggedIn()).toBe(true);
67
69
  });
68
- test('checkSecret should work', async () => {
69
- const result = await service.checkSecret('123');
70
+ test('checkPassword should work', async () => {
71
+ const result = await service.checkPassword('123');
70
72
  expect(result.strength).toBeLessThan(2);
71
- const strongResult = await service.checkSecret('Very-Strong-Password-2026-!@#$');
73
+ const strongResult = await service.checkPassword('Very-Strong-Password-2026-!@#$');
72
74
  expect(strongResult.strength).toBeGreaterThanOrEqual(2);
73
75
  });
74
76
  });
@@ -18,7 +18,9 @@ describe('AuthenticationService', () => {
18
18
  beforeAll(async () => {
19
19
  ({ injector, database } = await setupIntegrationTest({
20
20
  modules: { authentication: true },
21
- authenticationAncillaryService: DefaultAuthenticationAncillaryService,
21
+ authentication: {
22
+ ancillaryService: DefaultAuthenticationAncillaryService,
23
+ },
22
24
  }));
23
25
  authenticationService = await injector.resolveAsync(AuthenticationService);
24
26
  subjectService = await injector.resolveAsync(SubjectService);
@@ -29,7 +31,7 @@ describe('AuthenticationService', () => {
29
31
  await injector?.dispose();
30
32
  });
31
33
  beforeEach(async () => {
32
- await clearTenantData(database, schema, ['credentials', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
34
+ await clearTenantData(database, schema, ['password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
33
35
  });
34
36
  test('login should create a session and listSessions should return it', async () => {
35
37
  const user = await subjectService.createUser({
@@ -38,8 +40,10 @@ describe('AuthenticationService', () => {
38
40
  firstName: 'John',
39
41
  lastName: 'Doe',
40
42
  });
41
- await authenticationService.setCredentials(user, 'Strong-Password-2026!');
42
- const tokenResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor.with({ actor: user.id, actorType: ActorType.Subject }));
43
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
44
+ const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor.with({ actor: user.id, actorType: ActorType.Subject }));
45
+ expect(loginResult.type).toBe('success');
46
+ const tokenResult = loginResult.result;
43
47
  expect(tokenResult.token).toBeDefined();
44
48
  const sessions = await authenticationService.listSessions(tenantId, user.id);
45
49
  expect(sessions).toHaveLength(1);
@@ -52,7 +56,7 @@ describe('AuthenticationService', () => {
52
56
  firstName: 'John',
53
57
  lastName: 'Doe',
54
58
  });
55
- await authenticationService.setCredentials(user, 'Strong-Password-2026!');
59
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
56
60
  const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
57
61
  await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
58
62
  await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
@@ -62,7 +66,7 @@ describe('AuthenticationService', () => {
62
66
  expect(sessions.every((s) => s.end > now)).toBe(true);
63
67
  await authenticationService.invalidateAllSessions(tenantId, user.id, userAuditor);
64
68
  sessions = await authenticationService.listSessions(tenantId, user.id);
65
- expect(sessions.every((s) => s.end <= now + 1000)).toBe(true); // small buffer for test execution time
69
+ expect(sessions).toHaveLength(0);
66
70
  });
67
71
  test('getSession and tryGetSession', async () => {
68
72
  const user = await subjectService.createUser({
@@ -71,9 +75,11 @@ describe('AuthenticationService', () => {
71
75
  firstName: 'John',
72
76
  lastName: 'Doe',
73
77
  });
74
- await authenticationService.setCredentials(user, 'Strong-Password-2026!');
78
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
75
79
  const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
76
- const tokenResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
80
+ const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
81
+ expect(loginResult.type).toBe('success');
82
+ const tokenResult = loginResult.result;
77
83
  const sessionId = tokenResult.jsonToken.payload.session;
78
84
  const session = await authenticationService.getSession(sessionId);
79
85
  expect(session.id).toBe(sessionId);
@@ -84,30 +90,32 @@ describe('AuthenticationService', () => {
84
90
  });
85
91
  test('refresh should issue new token and session', async () => {
86
92
  const user = await subjectService.createUser({ tenantId, email: 'refresh@example.com', firstName: 'R', lastName: 'F' });
87
- await authenticationService.setCredentials(user, 'Strong-Password-2026!');
93
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
88
94
  const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
89
95
  const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
90
- const refreshResult = await authenticationService.refresh(loginResult.refreshToken, undefined, {}, userAuditor);
96
+ expect(loginResult.type).toBe('success');
97
+ const loginSuccessResult = loginResult.result;
98
+ const refreshResult = await authenticationService.refresh(loginSuccessResult.refreshToken, undefined, {}, userAuditor);
91
99
  expect(refreshResult.token).toBeDefined();
92
- expect(refreshResult.refreshToken).not.toBe(loginResult.refreshToken);
100
+ expect(refreshResult.refreshToken).not.toBe(loginSuccessResult.refreshToken);
93
101
  });
94
- test('changeSecret should update credentials', async () => {
102
+ test('changePassword should update password', async () => {
95
103
  const user = await subjectService.createUser({ tenantId, email: 'change@example.com', firstName: 'C', lastName: 'S' });
96
- await authenticationService.setCredentials(user, 'Old-Password-2026!');
104
+ await authenticationService.setPassword(user, 'Old-Password-2026!');
97
105
  const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
98
- await authenticationService.changeSecret({ tenantId, subject: user.id }, 'Old-Password-2026!', 'New-Password-2026!', userAuditor);
99
- const authResult = await authenticationService.authenticate({ tenantId, subject: user.id }, 'New-Password-2026!');
106
+ await authenticationService.changePassword({ tenantId, subject: user.id }, 'Old-Password-2026!', 'New-Password-2026!', userAuditor);
107
+ const authResult = await authenticationService.authenticateWithPassword({ tenantId, subject: user.id }, 'New-Password-2026!');
100
108
  expect(authResult.success).toBe(true);
101
109
  });
102
- test('checkSecret, testSecret, and validateSecret', async () => {
110
+ test('checkPassword, testPassword, and validatePassword', async () => {
103
111
  const weak = 'abc';
104
112
  const strong = 'Very-Strong-Password-2026-!@#$';
105
- expect((await authenticationService.checkSecret(weak)).strength).toBeLessThan(2);
106
- expect((await authenticationService.checkSecret(strong)).strength).toBeGreaterThanOrEqual(2);
107
- expect((await authenticationService.testSecret(weak)).success).toBe(false);
108
- expect((await authenticationService.testSecret(strong)).success).toBe(true);
109
- await expect(authenticationService.validateSecret(weak)).rejects.toThrow();
110
- await expect(authenticationService.validateSecret(strong)).resolves.not.toThrow();
113
+ expect((await authenticationService.checkPassword(weak)).strength).toBeLessThan(2);
114
+ expect((await authenticationService.checkPassword(strong)).strength).toBeGreaterThanOrEqual(2);
115
+ expect((await authenticationService.testPassword(weak)).success).toBe(false);
116
+ expect((await authenticationService.testPassword(strong)).success).toBe(true);
117
+ await expect(authenticationService.validatePassword(weak)).rejects.toThrow();
118
+ await expect(authenticationService.validatePassword(strong)).resolves.not.toThrow();
111
119
  });
112
120
  test('tryResolveSubject and resolveSubject', async () => {
113
121
  const user = await subjectService.createUser({ tenantId, email: 'resolve@example.com', firstName: 'R', lastName: 'S' });
@@ -125,16 +133,16 @@ describe('AuthenticationService', () => {
125
133
  const impersonated = await authenticationService.impersonate(adminToken.token, adminToken.refreshToken, user.id, undefined, adminAuditor);
126
134
  expect(impersonated.jsonToken.payload.subject).toBe(user.id);
127
135
  expect(impersonated.jsonToken.payload.impersonator).toBe(admin.id);
128
- const unimpersonated = await authenticationService.unimpersonate(impersonated.impersonatorRefreshToken, undefined, adminAuditor);
136
+ const unimpersonated = await authenticationService.unimpersonate(impersonated.impersonatorRefreshToken, impersonated.token, undefined, adminAuditor);
129
137
  expect(unimpersonated.jsonToken.payload.subject).toBe(admin.id);
130
138
  expect(unimpersonated.jsonToken.payload.impersonator).toBeUndefined();
131
139
  });
132
140
  test('refresh should throw on invalid token', async () => {
133
141
  await expect(authenticationService.refresh('invalid', undefined, {}, auditor)).rejects.toThrow();
134
142
  });
135
- test('login should throw on invalid credentials', async () => {
143
+ test('login should throw on invalid password', async () => {
136
144
  const user = await subjectService.createUser({ tenantId, email: 'fail@example.com', firstName: 'F', lastName: 'L' });
137
- await authenticationService.setCredentials(user, 'Very-Strong-Password-2026!');
145
+ await authenticationService.setPassword(user, 'Very-Strong-Password-2026!');
138
146
  await expect(authenticationService.login({ tenantId, subject: user.id }, 'wrong', undefined, auditor)).rejects.toThrow();
139
147
  });
140
148
  test('endSession should handle non-existent session gracefully', async () => {
@@ -143,13 +151,13 @@ describe('AuthenticationService', () => {
143
151
  test('resolveSubject should throw if not found', async () => {
144
152
  await expect(authenticationService.resolveSubject({ tenantId, subject: 'missing' })).rejects.toThrow();
145
153
  });
146
- test('secret reset flow', async () => {
154
+ test('password reset flow', async () => {
147
155
  const user = await subjectService.createUser({ tenantId, email: 'reset@example.com', firstName: 'R', lastName: 'E' });
148
156
  const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
149
- await authenticationService.initSecretReset({ tenantId, subject: user.id }, undefined, userAuditor);
157
+ await authenticationService.initPasswordReset({ tenantId, subject: user.id }, undefined, userAuditor);
150
158
  expect(ancillaryService.lastResetData).toBeDefined();
151
- await authenticationService.resetSecret(ancillaryService.lastResetData.token, 'New-Password-Reset-2026!', userAuditor);
152
- const authResult = await authenticationService.authenticate({ tenantId, subject: user.id }, 'New-Password-Reset-2026!');
159
+ await authenticationService.resetPassword(ancillaryService.lastResetData.token, 'New-Password-Reset-2026!', userAuditor);
160
+ const authResult = await authenticationService.authenticateWithPassword({ tenantId, subject: user.id }, 'New-Password-Reset-2026!');
153
161
  expect(authResult.success).toBe(true);
154
162
  });
155
163
  test('deriveSigningSecrets should work', async () => {
@@ -3,7 +3,7 @@ export declare class DefaultAuthenticationAncillaryService extends Authenticatio
3
3
  #private;
4
4
  lastResetData: any;
5
5
  getTokenPayload(): Promise<{}>;
6
- handleInitSecretReset(data: any): Promise<void>;
6
+ handleInitPasswordReset(data: any): Promise<void>;
7
7
  canImpersonate(_token: any, _subject: any, _data: any): Promise<boolean>;
8
8
  resolveSubjects(data: any): Promise<import("../index.js").Subject[]>;
9
9
  }
@@ -12,7 +12,7 @@ let DefaultAuthenticationAncillaryService = class DefaultAuthenticationAncillary
12
12
  #authenticationService;
13
13
  lastResetData;
14
14
  async getTokenPayload() { return {}; }
15
- async handleInitSecretReset(data) { this.lastResetData = data; }
15
+ async handleInitPasswordReset(data) { this.lastResetData = data; }
16
16
  async canImpersonate(_token, _subject, _data) { return true; }
17
17
  async resolveSubjects(data) {
18
18
  if (isUndefined(this.#authenticationService)) {
@@ -0,0 +1,211 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest';
2
+ import { ApiGateway } from '../../api/server/gateway.js';
3
+ import { Auditor } from '../../audit/index.js';
4
+ import { importKey } from '../../cryptography/index.js';
5
+ import { generateTotpToken } from '../../cryptography/totp.js';
6
+ import { HttpBody } from '../../http/http-body.js';
7
+ import { HttpHeaders } from '../../http/http-headers.js';
8
+ import { HttpQuery } from '../../http/http-query.js';
9
+ import { HttpServerRequest } from '../../http/server/http-server-request.js';
10
+ import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
11
+ import { currentTimestampSeconds } from '../../utils/date-time.js';
12
+ import { assert } from '../../utils/type-guards.js';
13
+ import { AuthenticationService } from '../server/authentication.service.js';
14
+ import { SubjectService } from '../server/subject.service.js';
15
+ import { DefaultAuthenticationAncillaryService } from './authentication.test-ancillary-service.js';
16
+ describe('Authentication Brute Force Protection (API Level)', () => {
17
+ let injector;
18
+ let database;
19
+ let gateway;
20
+ let subjectService;
21
+ let authenticationService;
22
+ let auditor;
23
+ const schema = 'authentication';
24
+ const tenantId = crypto.randomUUID();
25
+ beforeAll(async () => {
26
+ ({ injector, database } = await setupIntegrationTest({
27
+ modules: { authentication: true, rateLimiter: true, api: true },
28
+ authentication: {
29
+ ancillaryService: DefaultAuthenticationAncillaryService,
30
+ options: {
31
+ bruteForceProtection: {
32
+ subjectBurstCapacity: 2,
33
+ subjectRefillInterval: 10000,
34
+ ipBurstCapacity: 2,
35
+ ipRefillInterval: 10000,
36
+ },
37
+ },
38
+ },
39
+ }));
40
+ gateway = injector.resolve(ApiGateway);
41
+ subjectService = await injector.resolveAsync(SubjectService);
42
+ authenticationService = await injector.resolveAsync(AuthenticationService);
43
+ auditor = injector.resolve(Auditor);
44
+ });
45
+ afterAll(async () => {
46
+ await injector?.dispose();
47
+ });
48
+ beforeEach(async () => {
49
+ await clearTenantData(database, schema, ['used_totp_tokens', 'totp_recovery_code', 'totp', 'password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
50
+ });
51
+ async function callLogin(ip, body) {
52
+ const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
53
+ const encodedBody = new TextEncoder().encode(JSON.stringify(body));
54
+ const request = new HttpServerRequest({
55
+ url: new URL('http://localhost/api/v1/auth/token'),
56
+ method: 'POST',
57
+ headers,
58
+ query: new HttpQuery(),
59
+ ip,
60
+ body: HttpBody.from(encodedBody, headers),
61
+ });
62
+ let capturedResponse;
63
+ const respond = async (response) => { capturedResponse = response; };
64
+ const close = async () => { };
65
+ const abortSignal = new AbortController().signal;
66
+ await gateway.handleHttpServerRequestContext({ request, respond, close, abortSignal, context: {} });
67
+ return capturedResponse;
68
+ }
69
+ async function callDisableTotp(ip, token, body) {
70
+ const headers = new HttpHeaders({
71
+ 'Content-Type': 'application/json',
72
+ 'Authorization': `Bearer ${token}`
73
+ });
74
+ const encodedBody = new TextEncoder().encode(JSON.stringify(body));
75
+ const request = new HttpServerRequest({
76
+ url: new URL('http://localhost/api/v1/auth/totp/disable'),
77
+ method: 'POST',
78
+ headers,
79
+ query: new HttpQuery(),
80
+ ip,
81
+ body: HttpBody.from(encodedBody, headers),
82
+ });
83
+ let capturedResponse;
84
+ const respond = async (response) => { capturedResponse = response; };
85
+ const close = async () => { };
86
+ const abortSignal = new AbortController().signal;
87
+ await gateway.handleHttpServerRequestContext({ request, respond, close, abortSignal, context: {} });
88
+ return capturedResponse;
89
+ }
90
+ test('should limit login attempts by user', async () => {
91
+ const user = await subjectService.createUser({ tenantId, email: 'user-limit@example.com', firstName: 'John', lastName: 'Doe' });
92
+ await authenticationService.setPassword(user, 'Strong-Password-2026!');
93
+ const body = { tenantId, subject: user.id, password: 'wrong' };
94
+ // 1st attempt - Fail (InvalidCredentials)
95
+ let res = await callLogin('1.1.1.1', body);
96
+ expect(res.statusCode).toBe(401);
97
+ // 2nd attempt - Fail (InvalidCredentials)
98
+ res = await callLogin('1.1.1.2', body);
99
+ expect(res.statusCode).toBe(401);
100
+ // 3rd attempt - Should be throttled (429)
101
+ res = await callLogin('1.1.1.3', body);
102
+ expect(res.statusCode).toBe(429);
103
+ expect(res.body.json.error.name).toBe('TooManyRequestsError');
104
+ });
105
+ test('should limit login attempts by IP', async () => {
106
+ const ip = '2.2.2.2';
107
+ const user1 = await subjectService.createUser({ tenantId, email: 'ip-limit-1@example.com', firstName: 'A', lastName: 'B' });
108
+ const user2 = await subjectService.createUser({ tenantId, email: 'ip-limit-2@example.com', firstName: 'C', lastName: 'D' });
109
+ await authenticationService.setPassword(user1, 'Strong-Password-2026!');
110
+ await authenticationService.setPassword(user2, 'Strong-Password-2026!');
111
+ // Fail for user1
112
+ let res = await callLogin(ip, { tenantId, subject: user1.id, password: 'wrong' });
113
+ expect(res.statusCode).toBe(401);
114
+ // Fail for user2
115
+ res = await callLogin(ip, { tenantId, subject: user2.id, password: 'wrong' });
116
+ expect(res.statusCode).toBe(401);
117
+ // 3rd attempt from same IP - Should be throttled (429)
118
+ res = await callLogin(ip, { tenantId, subject: 'any', password: 'any' });
119
+ expect(res.statusCode).toBe(429);
120
+ });
121
+ test('should work for multiple successful logins', async () => {
122
+ const user1 = await subjectService.createUser({ tenantId, email: 'success1@example.com', firstName: 'E', lastName: 'F' });
123
+ const user2 = await subjectService.createUser({ tenantId, email: 'success2@example.com', firstName: 'E', lastName: 'F' });
124
+ const user3 = await subjectService.createUser({ tenantId, email: 'success3@example.com', firstName: 'E', lastName: 'F' });
125
+ await authenticationService.setPassword(user1, 'Strong-Password-2026!');
126
+ await authenticationService.setPassword(user2, 'Strong-Password-2026!');
127
+ await authenticationService.setPassword(user3, 'Strong-Password-2026!');
128
+ // 1st attempt
129
+ let res = await callLogin('4.4.4.1', { tenantId, subject: user1.id, password: 'Strong-Password-2026!' });
130
+ expect(res.statusCode).toBe(200);
131
+ // 2nd attempt
132
+ res = await callLogin('4.4.4.2', { tenantId, subject: user2.id, password: 'Strong-Password-2026!' });
133
+ expect(res.statusCode).toBe(200);
134
+ // 3rd attempt
135
+ res = await callLogin('4.4.4.3', { tenantId, subject: user3.id, password: 'Strong-Password-2026!' });
136
+ expect(res.statusCode).toBe(200);
137
+ });
138
+ test('should limit disableTotp attempts by user', async () => {
139
+ const password = 'Strong-Password-2026!';
140
+ const user = await subjectService.createUser({ tenantId, email: 'totp-limit@example.com', firstName: 'John', lastName: 'Doe' });
141
+ await authenticationService.setPassword(user, password);
142
+ // Enable TOTP for the user
143
+ await authenticationService.initEnrollTotp(tenantId, user.id, auditor);
144
+ const totpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
145
+ const secret = await importKey('raw-secret', totpEntry.secret, { name: 'HMAC', hash: authenticationService.getTotpOptions().codeHashAlgorithm }, false, ['sign']);
146
+ const setupToken = await generateTotpToken(secret, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
147
+ await authenticationService.completeEnrollTotp(tenantId, user.id, setupToken, auditor);
148
+ // Login to get a session token
149
+ const loginResult = await authenticationService.login({ tenantId, subject: user.id }, password, undefined, auditor);
150
+ expect(loginResult.type).toBe('totp');
151
+ assert(loginResult.type == 'totp', 'Login should have triggered TOTP');
152
+ const verifyToken = await generateTotpToken(secret, authenticationService.getTotpOptions());
153
+ const verifyResult = await authenticationService.loginVerifyTotp(loginResult.challengeToken, verifyToken, auditor);
154
+ expect(verifyResult.type).toBe('success');
155
+ assert(verifyResult.type == 'success', 'TOTP verification failed');
156
+ const sessionToken = verifyResult.result.token;
157
+ // 1st attempt - Fail (Invalid TOTP token)
158
+ let res = await callDisableTotp('1.1.1.1', sessionToken, { token: '000000' });
159
+ expect(res.statusCode).toBe(403);
160
+ // 2nd attempt - Fail (Invalid TOTP token)
161
+ res = await callDisableTotp('1.1.1.2', sessionToken, { token: '000001' });
162
+ expect(res.statusCode).toBe(403);
163
+ // 3rd attempt - Should be throttled (429)
164
+ res = await callDisableTotp('1.1.1.3', sessionToken, { token: '000002' });
165
+ expect(res.statusCode).toBe(429);
166
+ expect(res.body.json.error.name).toBe('TooManyRequestsError');
167
+ });
168
+ test('should limit disableTotp attempts by IP', async () => {
169
+ const password = 'Strong-Password-2026!';
170
+ const ip = '3.3.3.3';
171
+ // Create two users with TOTP enabled
172
+ const user1 = await subjectService.createUser({ tenantId, email: 'totp-ip-1@example.com', firstName: 'A', lastName: 'B' });
173
+ await authenticationService.setPassword(user1, password);
174
+ await authenticationService.initEnrollTotp(tenantId, user1.id, auditor);
175
+ const totpEntry1 = await authenticationService.tryGetTotp(tenantId, user1.id);
176
+ const secret1 = await importKey('raw-secret', totpEntry1.secret, { name: 'HMAC', hash: authenticationService.getTotpOptions().codeHashAlgorithm }, false, ['sign']);
177
+ const setupToken1 = await generateTotpToken(secret1, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
178
+ await authenticationService.completeEnrollTotp(tenantId, user1.id, setupToken1, auditor);
179
+ const user2 = await subjectService.createUser({ tenantId, email: 'totp-ip-2@example.com', firstName: 'C', lastName: 'D' });
180
+ await authenticationService.setPassword(user2, password);
181
+ await authenticationService.initEnrollTotp(tenantId, user2.id, auditor);
182
+ const totpEntry2 = await authenticationService.tryGetTotp(tenantId, user2.id);
183
+ const secret2 = await importKey('raw-secret', totpEntry2.secret, { name: 'HMAC', hash: authenticationService.getTotpOptions().codeHashAlgorithm }, false, ['sign']);
184
+ const setupToken2 = await generateTotpToken(secret2, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
185
+ await authenticationService.completeEnrollTotp(tenantId, user2.id, setupToken2, auditor);
186
+ // Login both users
187
+ const loginResult1 = await authenticationService.login({ tenantId, subject: user1.id }, password, undefined, auditor);
188
+ const loginResult2 = await authenticationService.login({ tenantId, subject: user2.id }, password, undefined, auditor);
189
+ expect(loginResult1.type).toBe('totp');
190
+ expect(loginResult2.type).toBe('totp');
191
+ assert(loginResult1.type == 'totp' && loginResult2.type == 'totp', 'Login should have triggered TOTP');
192
+ const verifyToken1 = await generateTotpToken(secret1, authenticationService.getTotpOptions());
193
+ const verifyToken2 = await generateTotpToken(secret2, authenticationService.getTotpOptions());
194
+ const verifyResult1 = await authenticationService.loginVerifyTotp(loginResult1.challengeToken, verifyToken1, auditor);
195
+ const verifyResult2 = await authenticationService.loginVerifyTotp(loginResult2.challengeToken, verifyToken2, auditor);
196
+ expect(verifyResult1.type).toBe('success');
197
+ expect(verifyResult2.type).toBe('success');
198
+ assert(verifyResult1.type == 'success' && verifyResult2.type == 'success', 'TOTP verification failed');
199
+ const sessionToken1 = verifyResult1.result.token;
200
+ const sessionToken2 = verifyResult2.result.token;
201
+ // Fail for user1 from IP
202
+ let res = await callDisableTotp(ip, sessionToken1, { token: '000000' });
203
+ expect(res.statusCode).toBe(403);
204
+ // Fail for user2 from same IP
205
+ res = await callDisableTotp(ip, sessionToken2, { token: '000000' });
206
+ expect(res.statusCode).toBe(403);
207
+ // 3rd attempt from same IP - Should be throttled (429)
208
+ res = await callDisableTotp(ip, sessionToken1, { token: 'any' });
209
+ expect(res.statusCode).toBe(429);
210
+ });
211
+ });
@@ -1,11 +1,15 @@
1
- import { describe, expect, test } from 'vitest';
1
+ import { beforeAll, describe, expect, test } from 'vitest';
2
+ import { createJwtTokenString, importKmacKey } from '../../cryptography/index.js';
2
3
  import { BadRequestError } from '../../errors/bad-request.error.js';
3
4
  import { InvalidTokenError } from '../../errors/invalid-token.error.js';
4
5
  import { currentTimestampSeconds } from '../../utils/date-time.js';
5
- import { createJwtTokenString } from '../../utils/jwt.js';
6
- import { getRefreshTokenFromString, getSecretResetTokenFromString, getTokenFromRequest, getTokenFromString, tryGetAuthorizationTokenStringFromRequest, tryGetTokenFromRequest } from '../server/helper.js';
6
+ import { encodeUtf8 } from '../../utils/encoding.js';
7
+ import { getPasswordResetTokenFromString, getRefreshTokenFromString, getTokenFromRequest, getTokenFromString, tryGetAuthorizationTokenStringFromRequest, tryGetTokenFromRequest } from '../server/helper.js';
7
8
  describe('authentication helper', () => {
8
- const secret = 'test-secret';
9
+ let signingKey;
10
+ beforeAll(async () => {
11
+ signingKey = await importKmacKey('raw-secret', 'KMAC256', encodeUtf8('test-secret-with-enough-length-32'));
12
+ });
9
13
  test('tryGetAuthorizationTokenStringFromRequest should extract bearer token from header', () => {
10
14
  const request = {
11
15
  headers: {
@@ -17,7 +21,7 @@ describe('authentication helper', () => {
17
21
  };
18
22
  expect(tryGetAuthorizationTokenStringFromRequest(request)).toBe('my-token');
19
23
  });
20
- test('tryGetAuthorizationTokenStringFromRequest should extract token without scheme from header', () => {
24
+ test('tryGetAuthorizationTokenStringFromRequest should throw if token without scheme from header', () => {
21
25
  const request = {
22
26
  headers: {
23
27
  tryGet: (name) => name == 'Authorization' ? 'my-token' : undefined,
@@ -26,7 +30,7 @@ describe('authentication helper', () => {
26
30
  tryGet: () => undefined,
27
31
  },
28
32
  };
29
- expect(tryGetAuthorizationTokenStringFromRequest(request)).toBe('my-token');
33
+ expect(() => tryGetAuthorizationTokenStringFromRequest(request)).toThrow(BadRequestError);
30
34
  });
31
35
  test('tryGetAuthorizationTokenStringFromRequest should extract bearer token from cookie', () => {
32
36
  const request = {
@@ -34,7 +38,7 @@ describe('authentication helper', () => {
34
38
  tryGet: () => undefined,
35
39
  },
36
40
  cookies: {
37
- tryGet: (name) => name == 'authorization' ? 'Bearer my-token' : undefined,
41
+ tryGet: (name) => name == 'token' ? 'my-token' : undefined,
38
42
  },
39
43
  };
40
44
  expect(tryGetAuthorizationTokenStringFromRequest(request)).toBe('my-token');
@@ -63,35 +67,35 @@ describe('authentication helper', () => {
63
67
  });
64
68
  test('getTokenFromString should validate token', async () => {
65
69
  const payload = { exp: currentTimestampSeconds() + 60, tenant: 't', subject: 's', jti: 'j' };
66
- const token = await createJwtTokenString({ header: { v: 1, alg: 'HS256', typ: 'JWT' }, payload }, secret);
67
- const validated = await getTokenFromString(token, 1, secret);
70
+ const token = await createJwtTokenString({ header: { v: 1, alg: 'KMAC256', typ: 'JWT' }, payload }, signingKey);
71
+ const validated = await getTokenFromString(token, 1, signingKey);
68
72
  expect(validated.payload).toEqual(payload);
69
73
  });
70
74
  test('getTokenFromString should throw on version mismatch', async () => {
71
75
  const payload = { exp: currentTimestampSeconds() + 60, tenant: 't', subject: 's', jti: 'j' };
72
- const token = await createJwtTokenString({ header: { v: 2, alg: 'HS256', typ: 'JWT' }, payload }, secret);
73
- await expect(getTokenFromString(token, 1, secret)).rejects.toThrow(InvalidTokenError);
76
+ const token = await createJwtTokenString({ header: { v: 2, alg: 'KMAC256', typ: 'JWT' }, payload }, signingKey);
77
+ await expect(getTokenFromString(token, 1, signingKey)).rejects.toThrow(InvalidTokenError);
74
78
  });
75
79
  test('getTokenFromString should throw on expired token', async () => {
76
80
  const payload = { exp: currentTimestampSeconds() - 60, tenant: 't', subject: 's', jti: 'j' };
77
- const token = await createJwtTokenString({ header: { v: 1, alg: 'HS256', typ: 'JWT' }, payload }, secret);
78
- await expect(getTokenFromString(token, 1, secret)).rejects.toThrow('Token expired');
81
+ const token = await createJwtTokenString({ header: { v: 1, alg: 'KMAC256', typ: 'JWT' }, payload }, signingKey);
82
+ await expect(getTokenFromString(token, 1, signingKey)).rejects.toThrow('Token expired');
79
83
  });
80
84
  test('getRefreshTokenFromString should validate refresh token', async () => {
81
85
  const payload = { exp: currentTimestampSeconds() + 60, tenant: 't', subject: 's', session: 'sess', secret: 'sec' };
82
- const token = await createJwtTokenString({ header: { alg: 'HS256', typ: 'JWT' }, payload }, secret);
83
- const validated = await getRefreshTokenFromString(token, secret);
86
+ const token = await createJwtTokenString({ header: { alg: 'KMAC256', typ: 'JWT' }, payload }, signingKey);
87
+ const validated = await getRefreshTokenFromString(token, signingKey);
84
88
  expect(validated.payload).toEqual(payload);
85
89
  });
86
- test('getSecretResetTokenFromString should validate secret reset token', async () => {
90
+ test('getPasswordResetTokenFromString should validate password reset token', async () => {
87
91
  const payload = { iat: currentTimestampSeconds(), exp: currentTimestampSeconds() + 60, tenant: 't', subject: 's' };
88
- const token = await createJwtTokenString({ header: { alg: 'HS256', typ: 'JWT' }, payload }, secret);
89
- const validated = await getSecretResetTokenFromString(token, secret);
92
+ const token = await createJwtTokenString({ header: { alg: 'KMAC256', typ: 'JWT' }, payload }, signingKey);
93
+ const validated = await getPasswordResetTokenFromString(token, signingKey);
90
94
  expect(validated.payload).toEqual(payload);
91
95
  });
92
96
  test('getTokenFromRequest should extract and validate token', async () => {
93
97
  const payload = { exp: currentTimestampSeconds() + 60, tenant: 't', subject: 's', jti: 'j' };
94
- const token = await createJwtTokenString({ header: { v: 1, alg: 'HS256', typ: 'JWT' }, payload }, secret);
98
+ const token = await createJwtTokenString({ header: { v: 1, alg: 'KMAC256', typ: 'JWT' }, payload }, signingKey);
95
99
  const request = {
96
100
  headers: {
97
101
  tryGet: (name) => name == 'Authorization' ? `Bearer ${token}` : undefined,
@@ -100,7 +104,7 @@ describe('authentication helper', () => {
100
104
  tryGet: () => undefined,
101
105
  },
102
106
  };
103
- const validated = await getTokenFromRequest(request, 1, secret);
107
+ const validated = await getTokenFromRequest(request, 1, signingKey);
104
108
  expect(validated.payload).toEqual(payload);
105
109
  });
106
110
  test('tryGetTokenFromRequest should return undefined if no token in request', async () => {
@@ -112,7 +116,7 @@ describe('authentication helper', () => {
112
116
  tryGet: () => undefined,
113
117
  },
114
118
  };
115
- const validated = await tryGetTokenFromRequest(request, 1, secret);
119
+ const validated = await tryGetTokenFromRequest(request, 1, signingKey);
116
120
  expect(validated).toBeUndefined();
117
121
  });
118
122
  });
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { PasswordRequirementsError } from '../errors/password-requirements.error.js';
3
+ describe('PasswordRequirementsError', () => {
4
+ it('should create an error with the given message', () => {
5
+ const message = 'Password is too weak.';
6
+ const error = new PasswordRequirementsError(message);
7
+ expect(error.message).toBe(message);
8
+ expect(error.name).toBe('PasswordRequirementsError');
9
+ });
10
+ it('should have the correct name', () => {
11
+ const error = new PasswordRequirementsError('any message');
12
+ expect(error.name).toBe('PasswordRequirementsError');
13
+ });
14
+ });