@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
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { runInInjectionContext } from '../../injector/inject.js';
3
+ import { Injector } from '../../injector/injector.js';
4
+ import { importKey } from '../cryptography.js';
5
+ import { configureSecrets, injectDerivedBytes, injectDerivedCryptoKey, masterSecretAlgorithm, masterSecretKeyUsages } from '../module.js';
6
+ describe('Secrets Module', () => {
7
+ test('configure and derive bytes', async () => {
8
+ const injector = new Injector('test');
9
+ const key = 'my-super-secret-key';
10
+ configureSecrets({ injector, key });
11
+ await runInInjectionContext(injector, async () => {
12
+ const derived = injectDerivedBytes('test-info', 32);
13
+ const sameDerived = injectDerivedBytes('test-info', 32);
14
+ const differentDerived = injectDerivedBytes('other-info', 32);
15
+ const bytes = await derived.getBytes();
16
+ expect(bytes.byteLength).toBe(32);
17
+ const sameBytes = await sameDerived.getBytes();
18
+ expect(new Uint8Array(bytes)).toEqual(new Uint8Array(sameBytes));
19
+ const differentBytes = await differentDerived.getBytes();
20
+ expect(new Uint8Array(bytes)).not.toEqual(new Uint8Array(differentBytes));
21
+ });
22
+ });
23
+ test('derive crypto key', async () => {
24
+ const injector = new Injector('test');
25
+ const key = 'my-super-secret-key';
26
+ configureSecrets({ injector, key });
27
+ await runInInjectionContext(injector, async () => {
28
+ const derived = injectDerivedCryptoKey('test-info', { name: 'HMAC', hash: 'SHA-256' }, ['sign']);
29
+ const cryptoKey = await derived.getKey();
30
+ expect(cryptoKey.type).toBe('secret');
31
+ expect(cryptoKey.algorithm.name).toBe('HMAC');
32
+ expect(cryptoKey.usages).toContain('sign');
33
+ });
34
+ });
35
+ test('fail when not configured', async () => {
36
+ const injector = new Injector('test');
37
+ await runInInjectionContext(injector, async () => {
38
+ // getBaseKeyFromInjector should throw if not configured
39
+ expect(() => injectDerivedBytes('test-info', 32)).toThrow('No secret key configured');
40
+ });
41
+ });
42
+ test('fail when requesting bytes from key-configured derived key', async () => {
43
+ const injector = new Injector('test');
44
+ configureSecrets({ injector, key: 'secret' });
45
+ await runInInjectionContext(injector, async () => {
46
+ const derived = injectDerivedCryptoKey('info', { name: 'HMAC', hash: 'SHA-256' }, ['sign']);
47
+ await expect(derived.getBytes()).rejects.toThrow('This key was not configured to derive bytes');
48
+ });
49
+ });
50
+ test('fail when requesting key from byte-configured derived key', async () => {
51
+ const injector = new Injector('test');
52
+ configureSecrets({ injector, key: 'secret' });
53
+ await runInInjectionContext(injector, async () => {
54
+ const derived = injectDerivedBytes('info', 32);
55
+ await expect(derived.getKey()).rejects.toThrow('This key was not configured to derive a CryptoKey');
56
+ });
57
+ });
58
+ test('hierarchical injector configuration', async () => {
59
+ const parentInjector = new Injector('parent');
60
+ const childInjector = parentInjector.fork('child');
61
+ const key = 'parent-key';
62
+ configureSecrets({ injector: parentInjector, key });
63
+ await runInInjectionContext(childInjector, async () => {
64
+ const derived = injectDerivedBytes('info', 32);
65
+ const bytes = await derived.getBytes();
66
+ expect(bytes.byteLength).toBe(32);
67
+ });
68
+ });
69
+ test('global injector configuration', async () => {
70
+ const key = 'global-key';
71
+ configureSecrets({ key }); // Registers on global Injector
72
+ const injector = new Injector('test');
73
+ await runInInjectionContext(injector, async () => {
74
+ const derived = injectDerivedBytes('info', 32);
75
+ const bytes = await derived.getBytes();
76
+ expect(bytes.byteLength).toBe(32);
77
+ });
78
+ });
79
+ test('configureSecrets with BufferSource', async () => {
80
+ const injector = new Injector('test');
81
+ const key = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6]);
82
+ configureSecrets({ injector, key });
83
+ await runInInjectionContext(injector, async () => {
84
+ const derived = injectDerivedBytes(new Uint8Array([1, 2, 3]), 32, { salt: new Uint8Array([4, 5, 6]) });
85
+ const bytes = await derived.getBytes();
86
+ expect(bytes.byteLength).toBe(32);
87
+ });
88
+ });
89
+ test('configureSecrets with CryptoKey', async () => {
90
+ const injector = new Injector('test');
91
+ const keyMaterial = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6]);
92
+ const cryptoKey = await importKey('raw-secret', keyMaterial, masterSecretAlgorithm, false, masterSecretKeyUsages);
93
+ configureSecrets({ injector, key: cryptoKey });
94
+ await runInInjectionContext(injector, async () => {
95
+ const derived = injectDerivedBytes('', 32, { salt: 'my-salt' });
96
+ const bytes = await derived.getBytes();
97
+ expect(bytes.byteLength).toBe(32);
98
+ });
99
+ });
100
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { importKey } from '../cryptography.js';
3
+ import { encodeTotpSecret, generateTotpRecoveryCodes, generateTotpSecret, generateTotpToken, generateTotpUri, hashTotpRecoveryCode, verifyTotpRecoveryCode, verifyTotpToken } from '../totp.js';
4
+ const totpOptions = {
5
+ codeHashAlgorithm: 'SHA-1',
6
+ recoveryCodeHashOptions: {
7
+ algorithm: { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations: 10 },
8
+ length: 64,
9
+ },
10
+ };
11
+ describe('TOTP Utilities', () => {
12
+ test('should generate a secret of appropriate length', () => {
13
+ expect(generateTotpSecret('SHA-1').byteLength).toBe(20);
14
+ expect(generateTotpSecret('SHA-256').byteLength).toBe(32);
15
+ expect(generateTotpSecret('SHA-512').byteLength).toBe(64);
16
+ });
17
+ test('should generate a valid TOTP token', async () => {
18
+ const secretData = new Uint8Array(new ArrayBuffer(20));
19
+ secretData.fill(1);
20
+ const secret = await importKey('raw-secret', secretData, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
21
+ const token = await generateTotpToken(secret, { ...totpOptions, timestamp: 1000000000 });
22
+ expect(token).toMatch(/^\d{6}$/);
23
+ });
24
+ test('should verify a valid TOTP token', async () => {
25
+ const secretData = new Uint8Array(new ArrayBuffer(20));
26
+ secretData.fill(1);
27
+ const secret = await importKey('raw-secret', secretData, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
28
+ const timestamp = 1000000000;
29
+ const token = await generateTotpToken(secret, { ...totpOptions, timestamp });
30
+ const isValid = await verifyTotpToken(secret, token, { ...totpOptions, timestamp });
31
+ expect(isValid).toBe(true);
32
+ });
33
+ test('should fail to verify an invalid TOTP token', async () => {
34
+ const secretData = new Uint8Array(new ArrayBuffer(20));
35
+ secretData.fill(1);
36
+ const secret = await importKey('raw-secret', secretData, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
37
+ const isValid = await verifyTotpToken(secret, '000000', { ...totpOptions, timestamp: 1000000000 });
38
+ expect(isValid).toBe(false);
39
+ });
40
+ test('should generate a valid otpauth URI', () => {
41
+ const secret = new Uint8Array(new ArrayBuffer(20));
42
+ secret.fill(1);
43
+ const encodedSecret = encodeTotpSecret(secret);
44
+ const uri = generateTotpUri(encodedSecret, 'user@example.com', 'MyApp', totpOptions);
45
+ expect(uri).toBe('otpauth://totp/MyApp:user%40example.com?secret=AEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIB&issuer=MyApp&algorithm=SHA1&digits=6&period=30');
46
+ });
47
+ test('should generate a valid otpauth URI with SHA-256', () => {
48
+ const secret = new Uint8Array(new ArrayBuffer(32));
49
+ secret.fill(1);
50
+ const encodedSecret = encodeTotpSecret(secret);
51
+ const uri = generateTotpUri(encodedSecret, 'user@example.com', 'MyApp', { ...totpOptions, codeHashAlgorithm: 'SHA-256' });
52
+ expect(uri).toBe('otpauth://totp/MyApp:user%40example.com?secret=AEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQ&issuer=MyApp&algorithm=SHA256&digits=6&period=30');
53
+ });
54
+ test('should verify a valid TOTP token with SHA-256', async () => {
55
+ const secretData = new Uint8Array(new ArrayBuffer(32));
56
+ secretData.fill(1);
57
+ const secret = await importKey('raw-secret', secretData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
58
+ const timestamp = 1000000000;
59
+ const options = { ...totpOptions, timestamp, codeHashAlgorithm: 'SHA-256' };
60
+ const token = await generateTotpToken(secret, options);
61
+ const isValid = await verifyTotpToken(secret, token, options);
62
+ expect(isValid).toBe(true);
63
+ });
64
+ test('should verify a valid TOTP token with SHA-512', async () => {
65
+ const secretData = new Uint8Array(new ArrayBuffer(64));
66
+ secretData.fill(1);
67
+ const secret = await importKey('raw-secret', secretData, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign']);
68
+ const timestamp = 1000000000;
69
+ const options = { ...totpOptions, timestamp, codeHashAlgorithm: 'SHA-512' };
70
+ const token = await generateTotpToken(secret, options);
71
+ const isValid = await verifyTotpToken(secret, token, options);
72
+ expect(isValid).toBe(true);
73
+ });
74
+ test('should generate recovery codes', () => {
75
+ const codes = generateTotpRecoveryCodes(10, 8);
76
+ expect(codes.length).toBe(10);
77
+ for (const code of codes) {
78
+ expect(code).toMatch(/^[A-Z2-7]{8}$/);
79
+ }
80
+ });
81
+ test('should hash and verify a recovery code', async () => {
82
+ const code = 'ABCDEFGH';
83
+ const salt = new Uint8Array(new ArrayBuffer(16));
84
+ salt.fill(1);
85
+ const options = { algorithm: { name: 'PBKDF2', hash: 'SHA-256', salt, iterations: 10 }, length: 64 };
86
+ const hash = await hashTotpRecoveryCode(code, options);
87
+ const isValid = await verifyTotpRecoveryCode(code, hash, options);
88
+ expect(isValid).toBe(true);
89
+ });
90
+ test('should fail to verify an invalid recovery code', async () => {
91
+ const code = 'ABCDEFGH';
92
+ const salt = new Uint8Array(new ArrayBuffer(16));
93
+ salt.fill(1);
94
+ const options = { algorithm: { name: 'PBKDF2', hash: 'SHA-256', salt, iterations: 10 }, length: 64 };
95
+ const hash = await hashTotpRecoveryCode(code, options);
96
+ const isValid = await verifyTotpRecoveryCode('WRONGONE', hash, options);
97
+ expect(isValid).toBe(false);
98
+ });
99
+ test('should hash and verify a recovery code with Argon2id', async () => {
100
+ const code = 'ABCDEFGH';
101
+ const salt = new Uint8Array(new ArrayBuffer(16));
102
+ salt.fill(1);
103
+ const options = { algorithm: { name: 'Argon2id', parallelism: 1, memory: 1024, passes: 1, nonce: salt }, length: 64 };
104
+ const hash = await hashTotpRecoveryCode(code, options);
105
+ const isValid = await verifyTotpRecoveryCode(code, hash, options);
106
+ expect(isValid).toBe(true);
107
+ });
108
+ });
@@ -0,0 +1,96 @@
1
+ /** biome-ignore-all lint/suspicious/noBitwiseOperators: ok */
2
+ import type { TypedOmit } from '../types/types.js';
3
+ import { type DeriveAlgorithm } from './cryptography.js';
4
+ export type TotpHashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-512';
5
+ export type TotpOptions = {
6
+ codeHashAlgorithm: TotpHashAlgorithm;
7
+ recoveryCodeHashOptions: TotpRecoveryCodeHashingOptions;
8
+ /**
9
+ * The timestamp to use for token generation/verification in seconds.
10
+ * Defaults to the current time.
11
+ */
12
+ timestamp?: number;
13
+ /**
14
+ * The time step in seconds.
15
+ * Defaults to 30.
16
+ */
17
+ period?: number;
18
+ /**
19
+ * The number of digits in the token.
20
+ * Defaults to 6.
21
+ */
22
+ digits?: number;
23
+ /**
24
+ * The number of previous/next steps to check during verification.
25
+ * Defaults to 1 (checks current, -1, and +1 step).
26
+ */
27
+ window?: number;
28
+ };
29
+ export type TotpRecoveryCodeHashingOptions = {
30
+ /**
31
+ * The hashing algorithm to use.
32
+ */
33
+ algorithm: Exclude<DeriveAlgorithm, string>;
34
+ /**
35
+ * Length of the resulting hash in bytes.
36
+ */
37
+ length: number;
38
+ };
39
+ /**
40
+ * Generates a random TOTP secret.
41
+ * @param algorithm The TOTP algorithm to use for determining the secret length.
42
+ * @returns The generated secret.
43
+ */
44
+ export declare function generateTotpSecret(algorithm: TotpHashAlgorithm): Uint8Array<ArrayBuffer>;
45
+ /**
46
+ * Generates a TOTP token for a given secret.
47
+ * @param secret The TOTP secret.
48
+ * @param options TOTP configuration options.
49
+ * @returns The generated token as a string.
50
+ */
51
+ export declare function generateTotpToken(secret: CryptoKey, options: TypedOmit<TotpOptions, 'recoveryCodeHashOptions'>): Promise<string>;
52
+ /**
53
+ * Verifies a TOTP token against a secret.
54
+ * @param secret The TOTP secret.
55
+ * @param token The token to verify.
56
+ * @param options TOTP configuration options.
57
+ * @returns True if the token is valid, false otherwise.
58
+ */
59
+ export declare function verifyTotpToken(secret: CryptoKey, token: string, options: TypedOmit<TotpOptions, 'recoveryCodeHashOptions'>): Promise<boolean>;
60
+ /**
61
+ * Encodes a TOTP secret to Base32.
62
+ * @param secret The TOTP secret.
63
+ * @returns The Base32 encoded secret.
64
+ */
65
+ export declare function encodeTotpSecret(secret: Uint8Array<ArrayBuffer>): string;
66
+ /**
67
+ * Generates an otpauth URI for enrollment.
68
+ * @param encodedSecret The Base32 encoded TOTP secret.
69
+ * @param accountName The name of the account (e.g., user email).
70
+ * @param issuer The name of the application or service.
71
+ * @param options TOTP configuration options.
72
+ * @returns The formatted otpauth URI.
73
+ */
74
+ export declare function generateTotpUri(encodedSecret: string, accountName: string, issuer: string, options: TypedOmit<TotpOptions, 'recoveryCodeHashOptions'>): string;
75
+ /**
76
+ * Generates a set of random recovery codes.
77
+ * @param count The number of codes to generate. Defaults to 10.
78
+ * @param length The length of each code. Defaults to 8.
79
+ * @returns An array of generated recovery codes.
80
+ */
81
+ export declare function generateTotpRecoveryCodes(count?: number, length?: number): string[];
82
+ /**
83
+ * Hashes a recovery code.
84
+ * @param code The recovery code to hash.
85
+ * @param options Configuration for hashing.
86
+ * @returns The hashed recovery code.
87
+ */
88
+ export declare function hashTotpRecoveryCode(code: string, options: TotpRecoveryCodeHashingOptions): Promise<Uint8Array<ArrayBuffer>>;
89
+ /**
90
+ * Verifies a recovery code against its hash.
91
+ * @param codeOrHash The recovery code (string) or a pre-computed hash to verify.
92
+ * @param hash The expected hash.
93
+ * @param options Configuration for hashing.
94
+ * @returns True if the code matches the hash, false otherwise.
95
+ */
96
+ export declare function verifyTotpRecoveryCode(codeOrHash: string | Uint8Array<ArrayBuffer>, hash: Uint8Array<ArrayBuffer>, options: TotpRecoveryCodeHashingOptions): Promise<boolean>;
@@ -0,0 +1,123 @@
1
+ /** biome-ignore-all lint/suspicious/noBitwiseOperators: ok */
2
+ import { Alphabet } from '../utils/alphabet.js';
3
+ import { encodeBase32 } from '../utils/base32.js';
4
+ import { currentTimestampSeconds } from '../utils/date-time.js';
5
+ import { encodeUtf8 } from '../utils/encoding.js';
6
+ import { timingSafeBinaryEquals, timingSafeStringEquals } from '../utils/equals.js';
7
+ import { getRandomBytes, getRandomString } from '../utils/random.js';
8
+ import { isString } from '../utils/type-guards.js';
9
+ import { deriveBytes, importKey, sign } from './cryptography.js';
10
+ const totpSecretLengthMap = {
11
+ 'SHA-1': 20,
12
+ 'SHA-256': 32,
13
+ 'SHA-512': 64,
14
+ };
15
+ /**
16
+ * Generates a random TOTP secret.
17
+ * @param algorithm The TOTP algorithm to use for determining the secret length.
18
+ * @returns The generated secret.
19
+ */
20
+ export function generateTotpSecret(algorithm) {
21
+ const length = totpSecretLengthMap[algorithm];
22
+ return getRandomBytes(length);
23
+ }
24
+ /**
25
+ * Generates a TOTP token for a given secret.
26
+ * @param secret The TOTP secret.
27
+ * @param options TOTP configuration options.
28
+ * @returns The generated token as a string.
29
+ */
30
+ export async function generateTotpToken(secret, options) {
31
+ const { codeHashAlgorithm, timestamp = currentTimestampSeconds(), period = 30, digits = 6 } = options;
32
+ const counter = Math.floor(timestamp / period);
33
+ const counterBuffer = new Uint8Array(8);
34
+ const counterView = new DataView(counterBuffer.buffer);
35
+ // Set 64-bit big-endian counter
36
+ counterView.setUint32(0, Math.floor(counter / 0x100000000), false);
37
+ counterView.setUint32(4, counter % 0x100000000, false);
38
+ const hmacResult = await sign({ name: 'HMAC', hash: codeHashAlgorithm }, secret, counterBuffer).toBuffer();
39
+ const hash = new Uint8Array(hmacResult);
40
+ const offset = hash[hash.length - 1] & 0xf;
41
+ const binary = ((hash[offset] & 0x7f) << 24)
42
+ | ((hash[offset + 1] & 0xff) << 16)
43
+ | ((hash[offset + 2] & 0xff) << 8)
44
+ | (hash[offset + 3] & 0xff);
45
+ const otp = binary % (10 ** digits);
46
+ return otp.toString().padStart(digits, '0');
47
+ }
48
+ /**
49
+ * Verifies a TOTP token against a secret.
50
+ * @param secret The TOTP secret.
51
+ * @param token The token to verify.
52
+ * @param options TOTP configuration options.
53
+ * @returns True if the token is valid, false otherwise.
54
+ */
55
+ export async function verifyTotpToken(secret, token, options) {
56
+ const { timestamp = currentTimestampSeconds(), period = 30, window = 1 } = options;
57
+ for (let i = -window; i <= window; i++) {
58
+ const stepTimestamp = timestamp + (i * period);
59
+ const expectedToken = await generateTotpToken(secret, { ...options, timestamp: stepTimestamp });
60
+ if (timingSafeStringEquals(token, expectedToken)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ /**
67
+ * Encodes a TOTP secret to Base32.
68
+ * @param secret The TOTP secret.
69
+ * @returns The Base32 encoded secret.
70
+ */
71
+ export function encodeTotpSecret(secret) {
72
+ return encodeBase32(secret, false);
73
+ }
74
+ /**
75
+ * Generates an otpauth URI for enrollment.
76
+ * @param encodedSecret The Base32 encoded TOTP secret.
77
+ * @param accountName The name of the account (e.g., user email).
78
+ * @param issuer The name of the application or service.
79
+ * @param options TOTP configuration options.
80
+ * @returns The formatted otpauth URI.
81
+ */
82
+ export function generateTotpUri(encodedSecret, accountName, issuer, options) {
83
+ const { digits = 6, period = 30, codeHashAlgorithm } = options;
84
+ const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}`;
85
+ const normalizedAlgorithm = codeHashAlgorithm.replace('-', '');
86
+ return `otpauth://totp/${label}?secret=${encodedSecret}&issuer=${encodeURIComponent(issuer)}&algorithm=${normalizedAlgorithm}&digits=${digits}&period=${period}`;
87
+ }
88
+ /**
89
+ * Generates a set of random recovery codes.
90
+ * @param count The number of codes to generate. Defaults to 10.
91
+ * @param length The length of each code. Defaults to 8.
92
+ * @returns An array of generated recovery codes.
93
+ */
94
+ export function generateTotpRecoveryCodes(count = 10, length = 8) {
95
+ const codes = [];
96
+ for (let i = 0; i < count; i++) {
97
+ codes.push(getRandomString(length, Alphabet.Base32));
98
+ }
99
+ return codes;
100
+ }
101
+ /**
102
+ * Hashes a recovery code.
103
+ * @param code The recovery code to hash.
104
+ * @param options Configuration for hashing.
105
+ * @returns The hashed recovery code.
106
+ */
107
+ export async function hashTotpRecoveryCode(code, options) {
108
+ const { length, algorithm } = options;
109
+ const keyMaterial = encodeUtf8(code);
110
+ const key = await importKey('raw-secret', keyMaterial, algorithm, false, ['deriveBits']);
111
+ return await deriveBytes(algorithm, key, length);
112
+ }
113
+ /**
114
+ * Verifies a recovery code against its hash.
115
+ * @param codeOrHash The recovery code (string) or a pre-computed hash to verify.
116
+ * @param hash The expected hash.
117
+ * @param options Configuration for hashing.
118
+ * @returns True if the code matches the hash, false otherwise.
119
+ */
120
+ export async function verifyTotpRecoveryCode(codeOrHash, hash, options) {
121
+ const computedHash = isString(codeOrHash) ? await hashTotpRecoveryCode(codeOrHash, options) : codeOrHash;
122
+ return timingSafeBinaryEquals(computedHash, hash);
123
+ }
@@ -8,7 +8,7 @@ CREATE TYPE "document_management"."workflow_fail_reason" AS ENUM('no-suitable-co
8
8
  CREATE TYPE "document_management"."workflow_state" AS ENUM('pending', 'running', 'review', 'completed', 'error', 'failed');--> statement-breakpoint
9
9
  CREATE TYPE "document_management"."workflow_step" AS ENUM('content-extraction', 'classification', 'data-extraction', 'assignment', 'validation');--> statement-breakpoint
10
10
  CREATE TABLE "document_management"."document" (
11
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
11
+ "id" uuid DEFAULT uuidv7() NOT NULL,
12
12
  "tenant_id" uuid NOT NULL,
13
13
  "type_id" uuid,
14
14
  "title" text,
@@ -31,7 +31,7 @@ CREATE TABLE "document_management"."document" (
31
31
  );
32
32
  --> statement-breakpoint
33
33
  CREATE TABLE "document_management"."assignment_scope" (
34
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
34
+ "id" uuid DEFAULT uuidv7() NOT NULL,
35
35
  "tenant_id" uuid NOT NULL,
36
36
  "task_id" uuid NOT NULL,
37
37
  "collection_id" uuid NOT NULL,
@@ -45,7 +45,7 @@ CREATE TABLE "document_management"."assignment_scope" (
45
45
  );
46
46
  --> statement-breakpoint
47
47
  CREATE TABLE "document_management"."assignment_task" (
48
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
48
+ "id" uuid DEFAULT uuidv7() NOT NULL,
49
49
  "tenant_id" uuid NOT NULL,
50
50
  "document_id" uuid NOT NULL,
51
51
  "target" "document_management"."assignment_target" NOT NULL,
@@ -59,7 +59,7 @@ CREATE TABLE "document_management"."assignment_task" (
59
59
  );
60
60
  --> statement-breakpoint
61
61
  CREATE TABLE "document_management"."category" (
62
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
62
+ "id" uuid DEFAULT uuidv7() NOT NULL,
63
63
  "tenant_id" uuid NOT NULL,
64
64
  "parent_id" uuid,
65
65
  "label" text NOT NULL,
@@ -75,7 +75,7 @@ CREATE TABLE "document_management"."category" (
75
75
  );
76
76
  --> statement-breakpoint
77
77
  CREATE TABLE "document_management"."collection" (
78
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
78
+ "id" uuid DEFAULT uuidv7() NOT NULL,
79
79
  "tenant_id" uuid NOT NULL,
80
80
  "parent_id" uuid,
81
81
  "revision" integer NOT NULL,
@@ -87,7 +87,7 @@ CREATE TABLE "document_management"."collection" (
87
87
  );
88
88
  --> statement-breakpoint
89
89
  CREATE TABLE "document_management"."collection_assignment" (
90
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
90
+ "id" uuid DEFAULT uuidv7() NOT NULL,
91
91
  "tenant_id" uuid NOT NULL,
92
92
  "collection_id" uuid NOT NULL,
93
93
  "document_id" uuid NOT NULL,
@@ -102,7 +102,7 @@ CREATE TABLE "document_management"."collection_assignment" (
102
102
  );
103
103
  --> statement-breakpoint
104
104
  CREATE TABLE "document_management"."property" (
105
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
105
+ "id" uuid DEFAULT uuidv7() NOT NULL,
106
106
  "tenant_id" uuid NOT NULL,
107
107
  "label" text NOT NULL,
108
108
  "key" text,
@@ -118,7 +118,7 @@ CREATE TABLE "document_management"."property" (
118
118
  );
119
119
  --> statement-breakpoint
120
120
  CREATE TABLE "document_management"."property_value" (
121
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
121
+ "id" uuid DEFAULT uuidv7() NOT NULL,
122
122
  "tenant_id" uuid NOT NULL,
123
123
  "document_id" uuid NOT NULL,
124
124
  "property_id" uuid NOT NULL,
@@ -138,7 +138,7 @@ CREATE TABLE "document_management"."property_value" (
138
138
  );
139
139
  --> statement-breakpoint
140
140
  CREATE TABLE "document_management"."request" (
141
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
141
+ "id" uuid DEFAULT uuidv7() NOT NULL,
142
142
  "tenant_id" uuid NOT NULL,
143
143
  "type_id" uuid,
144
144
  "document_id" uuid,
@@ -154,7 +154,7 @@ CREATE TABLE "document_management"."request" (
154
154
  );
155
155
  --> statement-breakpoint
156
156
  CREATE TABLE "document_management"."request_collection_assignment" (
157
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
157
+ "id" uuid DEFAULT uuidv7() NOT NULL,
158
158
  "tenant_id" uuid NOT NULL,
159
159
  "request_id" uuid NOT NULL,
160
160
  "collection_id" uuid NOT NULL,
@@ -168,7 +168,7 @@ CREATE TABLE "document_management"."request_collection_assignment" (
168
168
  );
169
169
  --> statement-breakpoint
170
170
  CREATE TABLE "document_management"."request_template" (
171
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
171
+ "id" uuid DEFAULT uuidv7() NOT NULL,
172
172
  "tenant_id" uuid NOT NULL,
173
173
  "requests_template_id" uuid NOT NULL,
174
174
  "type_id" uuid NOT NULL,
@@ -182,7 +182,7 @@ CREATE TABLE "document_management"."request_template" (
182
182
  );
183
183
  --> statement-breakpoint
184
184
  CREATE TABLE "document_management"."requests_template" (
185
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
185
+ "id" uuid DEFAULT uuidv7() NOT NULL,
186
186
  "tenant_id" uuid NOT NULL,
187
187
  "label" text NOT NULL,
188
188
  "description" text,
@@ -195,7 +195,7 @@ CREATE TABLE "document_management"."requests_template" (
195
195
  );
196
196
  --> statement-breakpoint
197
197
  CREATE TABLE "document_management"."tag" (
198
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
198
+ "id" uuid DEFAULT uuidv7() NOT NULL,
199
199
  "tenant_id" uuid NOT NULL,
200
200
  "label" text NOT NULL,
201
201
  "revision" integer NOT NULL,
@@ -208,7 +208,7 @@ CREATE TABLE "document_management"."tag" (
208
208
  );
209
209
  --> statement-breakpoint
210
210
  CREATE TABLE "document_management"."tag_assignment" (
211
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
211
+ "id" uuid DEFAULT uuidv7() NOT NULL,
212
212
  "tenant_id" uuid NOT NULL,
213
213
  "document_id" uuid NOT NULL,
214
214
  "tag_id" uuid NOT NULL,
@@ -222,7 +222,7 @@ CREATE TABLE "document_management"."tag_assignment" (
222
222
  );
223
223
  --> statement-breakpoint
224
224
  CREATE TABLE "document_management"."type" (
225
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
225
+ "id" uuid DEFAULT uuidv7() NOT NULL,
226
226
  "tenant_id" uuid NOT NULL,
227
227
  "category_id" uuid NOT NULL,
228
228
  "label" text NOT NULL,
@@ -238,7 +238,7 @@ CREATE TABLE "document_management"."type" (
238
238
  );
239
239
  --> statement-breakpoint
240
240
  CREATE TABLE "document_management"."type_property" (
241
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
241
+ "id" uuid DEFAULT uuidv7() NOT NULL,
242
242
  "tenant_id" uuid NOT NULL,
243
243
  "type_id" uuid NOT NULL,
244
244
  "property_id" uuid NOT NULL,
@@ -252,7 +252,7 @@ CREATE TABLE "document_management"."type_property" (
252
252
  );
253
253
  --> statement-breakpoint
254
254
  CREATE TABLE "document_management"."document_type_validation" (
255
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
255
+ "id" uuid DEFAULT uuidv7() NOT NULL,
256
256
  "tenant_id" uuid NOT NULL,
257
257
  "type_id" uuid NOT NULL,
258
258
  "validation_id" uuid NOT NULL,
@@ -266,7 +266,7 @@ CREATE TABLE "document_management"."document_type_validation" (
266
266
  );
267
267
  --> statement-breakpoint
268
268
  CREATE TABLE "document_management"."validation_definition" (
269
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
269
+ "id" uuid DEFAULT uuidv7() NOT NULL,
270
270
  "tenant_id" uuid NOT NULL,
271
271
  "identifier" text NOT NULL,
272
272
  "label" text NOT NULL,
@@ -281,7 +281,7 @@ CREATE TABLE "document_management"."validation_definition" (
281
281
  );
282
282
  --> statement-breakpoint
283
283
  CREATE TABLE "document_management"."validation_execution" (
284
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
284
+ "id" uuid DEFAULT uuidv7() NOT NULL,
285
285
  "tenant_id" uuid NOT NULL,
286
286
  "workflow_id" uuid NOT NULL,
287
287
  "definition_id" uuid NOT NULL,
@@ -300,7 +300,7 @@ CREATE TABLE "document_management"."validation_execution" (
300
300
  );
301
301
  --> statement-breakpoint
302
302
  CREATE TABLE "document_management"."validation_execution_related_document" (
303
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
303
+ "id" uuid DEFAULT uuidv7() NOT NULL,
304
304
  "tenant_id" uuid NOT NULL,
305
305
  "execution_id" uuid NOT NULL,
306
306
  "document_id" uuid NOT NULL,
@@ -314,7 +314,7 @@ CREATE TABLE "document_management"."validation_execution_related_document" (
314
314
  );
315
315
  --> statement-breakpoint
316
316
  CREATE TABLE "document_management"."workflow" (
317
- "id" uuid DEFAULT gen_random_uuid() NOT NULL,
317
+ "id" uuid DEFAULT uuidv7() NOT NULL,
318
318
  "tenant_id" uuid NOT NULL,
319
319
  "document_id" uuid NOT NULL,
320
320
  "step" "document_management"."workflow_step" NOT NULL,