@tuturuuu/utils 0.0.2 → 0.6.0

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 (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +122 -3
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,232 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { decryptField, encryptField } from '../encryption-service';
3
+ import { generateWorkspaceKey } from './test-helpers';
4
+
5
+ describe('encryption-service', () => {
6
+ // ============================================================================
7
+ // Field Encryption/Decryption Tests
8
+ // ============================================================================
9
+ describe('field encryption/decryption', () => {
10
+ let workspaceKey: Buffer;
11
+
12
+ beforeEach(() => {
13
+ workspaceKey = generateWorkspaceKey();
14
+ });
15
+
16
+ it('should encrypt and decrypt string field correctly', () => {
17
+ const plaintext = 'Team Meeting';
18
+ const ciphertext = encryptField(plaintext, workspaceKey);
19
+ const decrypted = decryptField(ciphertext, workspaceKey);
20
+
21
+ expect(decrypted).toBe(plaintext);
22
+ });
23
+
24
+ it('should handle empty strings', () => {
25
+ const ciphertext = encryptField('', workspaceKey);
26
+ expect(ciphertext).toBe('');
27
+
28
+ const decrypted = decryptField('', workspaceKey);
29
+ expect(decrypted).toBe('');
30
+ });
31
+
32
+ it('should handle Unicode characters (Vietnamese)', () => {
33
+ const plaintext = 'Cuộc họp nhóm buổi sáng';
34
+ const ciphertext = encryptField(plaintext, workspaceKey);
35
+ const decrypted = decryptField(ciphertext, workspaceKey);
36
+
37
+ expect(decrypted).toBe(plaintext);
38
+ });
39
+
40
+ it('should handle emoji', () => {
41
+ const plaintext = '🎉 Birthday Party 🎂';
42
+ const ciphertext = encryptField(plaintext, workspaceKey);
43
+ const decrypted = decryptField(ciphertext, workspaceKey);
44
+
45
+ expect(decrypted).toBe(plaintext);
46
+ });
47
+
48
+ it('should handle long text', () => {
49
+ const plaintext =
50
+ 'This is a very long description that contains multiple sentences. '.repeat(
51
+ 100
52
+ );
53
+ const ciphertext = encryptField(plaintext, workspaceKey);
54
+ const decrypted = decryptField(ciphertext, workspaceKey);
55
+
56
+ expect(decrypted).toBe(plaintext);
57
+ });
58
+
59
+ it('should produce different ciphertext for same plaintext (random IV)', () => {
60
+ const plaintext = 'Same text';
61
+ const ciphertext1 = encryptField(plaintext, workspaceKey);
62
+ const ciphertext2 = encryptField(plaintext, workspaceKey);
63
+
64
+ expect(ciphertext1).not.toBe(ciphertext2);
65
+ });
66
+
67
+ it('should fail decryption with wrong key and return original (backward compat)', () => {
68
+ const wrongKey = generateWorkspaceKey();
69
+ const ciphertext = encryptField('Secret', workspaceKey);
70
+
71
+ // With the new try-catch, it returns original ciphertext for backward compatibility
72
+ const result = decryptField(ciphertext, wrongKey);
73
+ expect(result).toBe(ciphertext);
74
+ });
75
+
76
+ it('should fail decryption when ciphertext is tampered and return original', () => {
77
+ const plaintext = 'Sensitive Meeting Notes';
78
+ const ciphertext = encryptField(plaintext, workspaceKey);
79
+
80
+ // Tamper with a byte in the middle of the ciphertext
81
+ const midIndex = Math.floor(ciphertext.length / 2);
82
+ const originalChar = ciphertext[midIndex];
83
+ // Guarantee the new char is different from the original
84
+ const tamperedChar = originalChar === 'A' ? 'B' : 'A';
85
+ const tamperedCiphertext =
86
+ ciphertext.slice(0, midIndex) +
87
+ tamperedChar +
88
+ ciphertext.slice(midIndex + 1);
89
+
90
+ // Ensure we actually changed something
91
+ expect(tamperedCiphertext).not.toBe(ciphertext);
92
+ expect(tamperedCiphertext.length).toBe(ciphertext.length);
93
+
94
+ // With try-catch wrapper, should return original ciphertext
95
+ const result = decryptField(tamperedCiphertext, workspaceKey);
96
+ expect(result).toBe(tamperedCiphertext);
97
+ });
98
+
99
+ it('should handle special characters', () => {
100
+ const plaintext = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/\\`~';
101
+ const ciphertext = encryptField(plaintext, workspaceKey);
102
+ const decrypted = decryptField(ciphertext, workspaceKey);
103
+
104
+ expect(decrypted).toBe(plaintext);
105
+ });
106
+
107
+ it('should handle newlines and tabs', () => {
108
+ const plaintext = 'Line 1\nLine 2\tTabbed\rCarriage return';
109
+ const ciphertext = encryptField(plaintext, workspaceKey);
110
+ const decrypted = decryptField(ciphertext, workspaceKey);
111
+
112
+ expect(decrypted).toBe(plaintext);
113
+ });
114
+
115
+ it('should handle mixed languages', () => {
116
+ const plaintext = 'English 日本語 العربية עברית';
117
+ const ciphertext = encryptField(plaintext, workspaceKey);
118
+ const decrypted = decryptField(ciphertext, workspaceKey);
119
+
120
+ expect(decrypted).toBe(plaintext);
121
+ });
122
+
123
+ it('should handle null bytes', () => {
124
+ const plaintext = 'Before\x00After';
125
+ const ciphertext = encryptField(plaintext, workspaceKey);
126
+ const decrypted = decryptField(ciphertext, workspaceKey);
127
+
128
+ expect(decrypted).toBe(plaintext);
129
+ });
130
+ });
131
+
132
+ // ============================================================================
133
+ // Key Validation Tests
134
+ // ============================================================================
135
+ describe('key validation', () => {
136
+ it('should throw error for non-Buffer workspace key', () => {
137
+ expect(() => {
138
+ encryptField('test', 'not-a-buffer' as unknown as Buffer);
139
+ }).toThrow('Invalid workspaceKey: expected Buffer, got string');
140
+ });
141
+
142
+ it('should throw error for wrong key length', () => {
143
+ const shortKey = Buffer.alloc(16); // 128 bits instead of 256
144
+
145
+ expect(() => {
146
+ encryptField('test', shortKey);
147
+ }).toThrow(
148
+ 'Invalid workspaceKey: expected 32 bytes for AES-256, got 16 bytes'
149
+ );
150
+ });
151
+
152
+ it('should throw error for too long key', () => {
153
+ const longKey = Buffer.alloc(64); // 512 bits
154
+
155
+ expect(() => {
156
+ encryptField('test', longKey);
157
+ }).toThrow(
158
+ 'Invalid workspaceKey: expected 32 bytes for AES-256, got 64 bytes'
159
+ );
160
+ });
161
+
162
+ it('should accept valid 32-byte key', () => {
163
+ const validKey = generateWorkspaceKey();
164
+ expect(() => {
165
+ encryptField('test', validKey);
166
+ }).not.toThrow();
167
+ });
168
+
169
+ it('should throw for null workspace key', () => {
170
+ expect(() => {
171
+ encryptField('test', null as unknown as Buffer);
172
+ }).toThrow('Invalid workspaceKey: expected Buffer, got object');
173
+ });
174
+
175
+ it('should throw for undefined workspace key', () => {
176
+ expect(() => {
177
+ encryptField('test', undefined as unknown as Buffer);
178
+ }).toThrow('Invalid workspaceKey: expected Buffer, got undefined');
179
+ });
180
+
181
+ it('should throw for array workspace key', () => {
182
+ expect(() => {
183
+ encryptField('test', [1, 2, 3] as unknown as Buffer);
184
+ }).toThrow('Invalid workspaceKey: expected Buffer, got object');
185
+ });
186
+ });
187
+
188
+ // ============================================================================
189
+ // Decryption Error Handling Tests
190
+ // ============================================================================
191
+ describe('decryption error handling', () => {
192
+ let workspaceKey: Buffer;
193
+
194
+ beforeEach(() => {
195
+ workspaceKey = generateWorkspaceKey();
196
+ });
197
+
198
+ it('should return original for too-short ciphertext', () => {
199
+ const shortCiphertext = 'abc'; // Too short to be valid encrypted data
200
+
201
+ const result = decryptField(shortCiphertext, workspaceKey);
202
+ expect(result).toBe(shortCiphertext);
203
+ });
204
+
205
+ it('should return original for invalid base64', () => {
206
+ // Invalid base64 will decode to something short
207
+ const invalidBase64 = '!!!invalid!!!';
208
+
209
+ const result = decryptField(invalidBase64, workspaceKey);
210
+ expect(result).toBe(invalidBase64);
211
+ });
212
+
213
+ it('should return original for corrupted auth tag', () => {
214
+ const plaintext = 'Secret data';
215
+ const ciphertext = encryptField(plaintext, workspaceKey);
216
+
217
+ // Corrupt the last few characters (auth tag area)
218
+ const corrupted = `${ciphertext.slice(0, -4)}XXXX`;
219
+
220
+ const result = decryptField(corrupted, workspaceKey);
221
+ expect(result).toBe(corrupted);
222
+ });
223
+
224
+ it('should handle extremely long ciphertext gracefully', () => {
225
+ const plaintext = 'x'.repeat(100000);
226
+ const ciphertext = encryptField(plaintext, workspaceKey);
227
+ const decrypted = decryptField(ciphertext, workspaceKey);
228
+
229
+ expect(decrypted).toBe(plaintext);
230
+ });
231
+ });
232
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { generateWorkspaceKey } from '../encryption-service';
3
+
4
+ describe('encryption-service', () => {
5
+ describe('generateWorkspaceKey', () => {
6
+ it('should generate a 256-bit key', () => {
7
+ const key = generateWorkspaceKey();
8
+ expect(key).toBeInstanceOf(Buffer);
9
+ expect(key.length).toBe(32); // 256 bits = 32 bytes
10
+ });
11
+
12
+ it('should generate unique keys each time', () => {
13
+ const key1 = generateWorkspaceKey();
14
+ const key2 = generateWorkspaceKey();
15
+ expect(key1.equals(key2)).toBe(false);
16
+ });
17
+
18
+ it('should generate cryptographically random keys', () => {
19
+ // Generate multiple keys and ensure they have reasonable entropy
20
+ const keys: Buffer[] = [];
21
+ for (let i = 0; i < 10; i++) {
22
+ keys.push(generateWorkspaceKey());
23
+ }
24
+
25
+ // All keys should be unique
26
+ const uniqueKeys = new Set(keys.map((k) => k.toString('hex')));
27
+ expect(uniqueKeys.size).toBe(10);
28
+ });
29
+ });
30
+ });
@@ -0,0 +1,187 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import {
3
+ decryptCalendarEventFields,
4
+ decryptField,
5
+ decryptWorkspaceKey,
6
+ encryptCalendarEventFields,
7
+ encryptField,
8
+ encryptWorkspaceKey,
9
+ } from '../encryption-service';
10
+ import {
11
+ generateWorkspaceKey,
12
+ setupEncryptionEnv,
13
+ TEST_MASTER_KEY,
14
+ } from './test-helpers';
15
+
16
+ const SCRYPT_COVERAGE_TIMEOUT_MS = 30_000;
17
+
18
+ describe('encryption-service', () => {
19
+ setupEncryptionEnv();
20
+
21
+ // ============================================================================
22
+ // Concurrency and Performance Tests
23
+ // ============================================================================
24
+ describe('concurrency and performance', () => {
25
+ it('should handle concurrent encryptions', async () => {
26
+ const workspaceKey = generateWorkspaceKey();
27
+ const plaintexts = Array.from({ length: 100 }, (_, i) => `Message ${i}`);
28
+
29
+ const encryptions = plaintexts.map((pt) =>
30
+ Promise.resolve(encryptField(pt, workspaceKey))
31
+ );
32
+ const ciphertexts = await Promise.all(encryptions);
33
+
34
+ // All ciphertexts should be unique
35
+ const uniqueCiphertexts = new Set(ciphertexts);
36
+ expect(uniqueCiphertexts.size).toBe(100);
37
+
38
+ // All should decrypt correctly
39
+ ciphertexts.forEach((ct, i) => {
40
+ const decrypted = decryptField(ct, workspaceKey);
41
+ expect(decrypted).toBe(`Message ${i}`);
42
+ });
43
+ });
44
+
45
+ it('should handle concurrent key generations', async () => {
46
+ const generations = Array.from({ length: 100 }, () =>
47
+ Promise.resolve(generateWorkspaceKey())
48
+ );
49
+ const keys = await Promise.all(generations);
50
+
51
+ // All keys should be unique
52
+ const uniqueKeys = new Set(keys.map((k) => k.toString('hex')));
53
+ expect(uniqueKeys.size).toBe(100);
54
+
55
+ // All should be valid 32-byte keys
56
+ keys.forEach((key) => {
57
+ expect(key.length).toBe(32);
58
+ });
59
+ });
60
+
61
+ it(
62
+ 'should benefit from key derivation cache',
63
+ async () => {
64
+ const workspaceKey = generateWorkspaceKey();
65
+
66
+ // Verify cache is working: multiple calls complete without error
67
+ // and produce valid encrypted output (deterministic check instead of timing)
68
+ const encrypted1 = await encryptWorkspaceKey(
69
+ workspaceKey,
70
+ TEST_MASTER_KEY
71
+ );
72
+ const encrypted2 = await encryptWorkspaceKey(
73
+ workspaceKey,
74
+ TEST_MASTER_KEY
75
+ );
76
+
77
+ // Both should be valid base64 strings
78
+ expect(encrypted1).toMatch(/^[A-Za-z0-9+/]+=*$/);
79
+ expect(encrypted2).toMatch(/^[A-Za-z0-9+/]+=*$/);
80
+
81
+ // Each encryption produces different ciphertext (random IV)
82
+ expect(encrypted1).not.toBe(encrypted2);
83
+
84
+ // Both should decrypt to the same key
85
+ const decrypted1 = await decryptWorkspaceKey(
86
+ encrypted1,
87
+ TEST_MASTER_KEY
88
+ );
89
+ const decrypted2 = await decryptWorkspaceKey(
90
+ encrypted2,
91
+ TEST_MASTER_KEY
92
+ );
93
+ expect(decrypted1.equals(workspaceKey)).toBe(true);
94
+ expect(decrypted2.equals(workspaceKey)).toBe(true);
95
+ },
96
+ SCRYPT_COVERAGE_TIMEOUT_MS
97
+ );
98
+ });
99
+
100
+ // ============================================================================
101
+ // Edge Cases and Boundary Tests
102
+ // ============================================================================
103
+ describe('edge cases', () => {
104
+ let workspaceKey: Buffer;
105
+
106
+ beforeEach(() => {
107
+ workspaceKey = generateWorkspaceKey();
108
+ });
109
+
110
+ it('should handle maximum safe string length', () => {
111
+ // Test with a reasonably large string (1MB)
112
+ const largePlaintext = 'a'.repeat(1024 * 1024);
113
+ const ciphertext = encryptField(largePlaintext, workspaceKey);
114
+ const decrypted = decryptField(ciphertext, workspaceKey);
115
+
116
+ expect(decrypted).toBe(largePlaintext);
117
+ });
118
+
119
+ it('should handle single character', () => {
120
+ const plaintext = 'x';
121
+ const ciphertext = encryptField(plaintext, workspaceKey);
122
+ const decrypted = decryptField(ciphertext, workspaceKey);
123
+
124
+ expect(decrypted).toBe(plaintext);
125
+ });
126
+
127
+ it('should handle only whitespace', () => {
128
+ const plaintext = ' \t\n ';
129
+ const ciphertext = encryptField(plaintext, workspaceKey);
130
+ const decrypted = decryptField(ciphertext, workspaceKey);
131
+
132
+ expect(decrypted).toBe(plaintext);
133
+ });
134
+
135
+ it('should handle extremely long location', () => {
136
+ const event = {
137
+ title: 'Meeting',
138
+ description: 'Desc',
139
+ location: 'Room '.repeat(10000),
140
+ };
141
+
142
+ const encrypted = encryptCalendarEventFields(event, workspaceKey);
143
+ const decrypted = decryptCalendarEventFields(encrypted, workspaceKey);
144
+
145
+ expect(decrypted.location).toBe(event.location);
146
+ });
147
+
148
+ it('should handle HTML content', () => {
149
+ const plaintext = '<script>alert("xss")</script><b>Bold</b>';
150
+ const ciphertext = encryptField(plaintext, workspaceKey);
151
+ const decrypted = decryptField(ciphertext, workspaceKey);
152
+
153
+ expect(decrypted).toBe(plaintext);
154
+ });
155
+
156
+ it('should handle JSON content', () => {
157
+ const plaintext = JSON.stringify({
158
+ key: 'value',
159
+ nested: { array: [1, 2, 3] },
160
+ });
161
+ const ciphertext = encryptField(plaintext, workspaceKey);
162
+ const decrypted = decryptField(ciphertext, workspaceKey);
163
+
164
+ expect(decrypted).toBe(plaintext);
165
+ expect(JSON.parse(decrypted)).toEqual({
166
+ key: 'value',
167
+ nested: { array: [1, 2, 3] },
168
+ });
169
+ });
170
+
171
+ it('should handle URL content', () => {
172
+ const plaintext = 'https://example.com/path?query=value&other=123#hash';
173
+ const ciphertext = encryptField(plaintext, workspaceKey);
174
+ const decrypted = decryptField(ciphertext, workspaceKey);
175
+
176
+ expect(decrypted).toBe(plaintext);
177
+ });
178
+
179
+ it('should handle markdown content', () => {
180
+ const plaintext = '# Heading\n\n**bold** _italic_ `code`\n\n- list item';
181
+ const ciphertext = encryptField(plaintext, workspaceKey);
182
+ const decrypted = decryptField(ciphertext, workspaceKey);
183
+
184
+ expect(decrypted).toBe(plaintext);
185
+ });
186
+ });
187
+ });
@@ -0,0 +1,22 @@
1
+ import { afterEach, beforeEach } from 'vitest';
2
+ import { generateWorkspaceKey } from '../encryption-service';
3
+
4
+ export const TEST_MASTER_KEY = 'test-master-key-for-unit-testing-only';
5
+
6
+ export function setupEncryptionEnv() {
7
+ let originalEnv: string | undefined;
8
+
9
+ beforeEach(() => {
10
+ originalEnv = process.env.ENCRYPTION_MASTER_KEY;
11
+ });
12
+
13
+ afterEach(() => {
14
+ if (originalEnv !== undefined) {
15
+ process.env.ENCRYPTION_MASTER_KEY = originalEnv;
16
+ } else {
17
+ delete process.env.ENCRYPTION_MASTER_KEY;
18
+ }
19
+ });
20
+ }
21
+
22
+ export { generateWorkspaceKey };
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ decryptWorkspaceKey,
4
+ encryptWorkspaceKey,
5
+ } from '../encryption-service';
6
+ import {
7
+ generateWorkspaceKey,
8
+ setupEncryptionEnv,
9
+ TEST_MASTER_KEY,
10
+ } from './test-helpers';
11
+
12
+ const SCRYPT_COVERAGE_TIMEOUT_MS = 30_000;
13
+
14
+ describe('encryption-service', () => {
15
+ setupEncryptionEnv();
16
+
17
+ describe('workspace key encryption/decryption', () => {
18
+ it(
19
+ 'should encrypt and decrypt workspace key correctly',
20
+ async () => {
21
+ const originalKey = generateWorkspaceKey();
22
+ const encryptedKey = await encryptWorkspaceKey(
23
+ originalKey,
24
+ TEST_MASTER_KEY
25
+ );
26
+
27
+ expect(encryptedKey).toBeTruthy();
28
+ expect(typeof encryptedKey).toBe('string');
29
+
30
+ const decryptedKey = await decryptWorkspaceKey(
31
+ encryptedKey,
32
+ TEST_MASTER_KEY
33
+ );
34
+ expect(decryptedKey.equals(originalKey)).toBe(true);
35
+ },
36
+ SCRYPT_COVERAGE_TIMEOUT_MS
37
+ );
38
+
39
+ it(
40
+ 'should produce different ciphertext for same key (random IV)',
41
+ async () => {
42
+ const key = generateWorkspaceKey();
43
+ const encrypted1 = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
44
+ const encrypted2 = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
45
+
46
+ expect(encrypted1).not.toBe(encrypted2);
47
+ },
48
+ SCRYPT_COVERAGE_TIMEOUT_MS
49
+ );
50
+
51
+ it(
52
+ 'should fail decryption with wrong master key',
53
+ async () => {
54
+ const key = generateWorkspaceKey();
55
+ const encryptedKey = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
56
+
57
+ await expect(
58
+ decryptWorkspaceKey(encryptedKey, 'wrong-master-key')
59
+ ).rejects.toThrow();
60
+ },
61
+ SCRYPT_COVERAGE_TIMEOUT_MS
62
+ );
63
+
64
+ it(
65
+ 'should handle different master key lengths',
66
+ async () => {
67
+ const key = generateWorkspaceKey();
68
+
69
+ // Short master key
70
+ const shortMasterKey = 'short';
71
+ const encrypted1 = await encryptWorkspaceKey(key, shortMasterKey);
72
+ const decrypted1 = await decryptWorkspaceKey(
73
+ encrypted1,
74
+ shortMasterKey
75
+ );
76
+ expect(decrypted1.equals(key)).toBe(true);
77
+
78
+ // Long master key
79
+ const longMasterKey = 'a'.repeat(256);
80
+ const encrypted2 = await encryptWorkspaceKey(key, longMasterKey);
81
+ const decrypted2 = await decryptWorkspaceKey(encrypted2, longMasterKey);
82
+ expect(decrypted2.equals(key)).toBe(true);
83
+ },
84
+ SCRYPT_COVERAGE_TIMEOUT_MS
85
+ );
86
+
87
+ it(
88
+ 'should produce valid base64 encoded output',
89
+ async () => {
90
+ const key = generateWorkspaceKey();
91
+ const encrypted = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
92
+
93
+ // Should be valid base64
94
+ expect(() => Buffer.from(encrypted, 'base64')).not.toThrow();
95
+
96
+ // Decoded buffer should have minimum length (IV + ciphertext + authTag)
97
+ const decoded = Buffer.from(encrypted, 'base64');
98
+ expect(decoded.length).toBeGreaterThanOrEqual(12 + 32 + 16); // IV + key + tag
99
+ },
100
+ SCRYPT_COVERAGE_TIMEOUT_MS
101
+ );
102
+
103
+ it(
104
+ 'should fail on corrupted encrypted key',
105
+ async () => {
106
+ const key = generateWorkspaceKey();
107
+ const encrypted = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
108
+
109
+ // Corrupt the encrypted data by modifying the auth tag (last 16 bytes)
110
+ // This guarantees decryption will fail because AES-GCM uses auth tag for integrity
111
+ const decoded = Buffer.from(encrypted, 'base64');
112
+ // Flip bits in the auth tag (last 16 bytes) - decoded is guaranteed to have min size
113
+ const lastIdx = decoded.length - 1;
114
+ const secondLastIdx = decoded.length - 2;
115
+ decoded.writeUInt8(decoded.readUInt8(lastIdx) ^ 0xff, lastIdx);
116
+ decoded.writeUInt8(
117
+ decoded.readUInt8(secondLastIdx) ^ 0xff,
118
+ secondLastIdx
119
+ );
120
+ const corrupted = decoded.toString('base64');
121
+
122
+ await expect(
123
+ decryptWorkspaceKey(corrupted, TEST_MASTER_KEY)
124
+ ).rejects.toThrow();
125
+ },
126
+ SCRYPT_COVERAGE_TIMEOUT_MS
127
+ );
128
+ });
129
+ });