@tuturuuu/utils 0.0.3 → 0.6.1

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 +313 -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 +120 -1
  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,343 @@
1
+ /**
2
+ * Encryption service for transparent encryption at rest
3
+ *
4
+ * Uses AES-256-GCM for symmetric encryption of sensitive calendar event fields.
5
+ * Workspace keys are encrypted with a master key from environment variables.
6
+ *
7
+ * Security Model:
8
+ * - Master key: stored in ENCRYPTION_MASTER_KEY environment variable
9
+ * - Workspace keys: AES-256 keys, unique per workspace, encrypted with master key
10
+ * - Field encryption: AES-GCM with random IV per encryption
11
+ */
12
+
13
+ import { gcm } from '@noble/ciphers/aes.js';
14
+ import { scryptAsync } from '@noble/hashes/scrypt.js';
15
+ import { randomBytes } from '@noble/hashes/utils.js';
16
+ import type { EncryptedCalendarEventFields, EncryptedField } from './types';
17
+
18
+ // Encryption constants
19
+ const KEY_LENGTH = 32; // 256 bits
20
+ const IV_LENGTH = 12; // 96 bits for GCM
21
+ const AUTH_TAG_LENGTH = 16; // 128 bits
22
+
23
+ // Scrypt parameters (explicit for documentation and security auditing)
24
+ //
25
+ // SECURITY RATIONALE:
26
+ // N=16384 (2^14), r=8, p=1 are used for key derivation from the master key.
27
+ // OWASP recommends N>=2^17 for PASSWORD HASHING, but this is KEY DERIVATION:
28
+ // - The master key is a high-entropy secret from environment variables
29
+ // - Key derivation doesn't need the same protection against brute-force as password hashing
30
+ // - Lower cost reduces latency for encryption/decryption operations
31
+ // - The threat model assumes the master key is not guessed (it's a 256-bit secret)
32
+ //
33
+ // NOTE: Re-evaluate cost parameters periodically. Consider:
34
+ // - Increasing N if server performance allows
35
+ // - Using per-workspace salt instead of fixed salt for additional isolation
36
+ //
37
+ const SCRYPT_COST = 16384;
38
+ const SCRYPT_BLOCK_SIZE = 8;
39
+ const SCRYPT_PARALLELISM = 1;
40
+ const SCRYPT_KEY_LENGTH = 32; // 256 bits
41
+ const SCRYPT_SALT = 'workspace-key-salt'; // Fixed salt (per-workspace salt is a future enhancement)
42
+
43
+ /**
44
+ * Maximum number of derived keys to cache.
45
+ * Prevents unbounded memory growth in long-running processes.
46
+ */
47
+ const MAX_CACHE_SIZE = 100;
48
+
49
+ // Cache for derived keys to avoid repeated scrypt calls
50
+ // Key: masterKey, Value: derived key buffer
51
+ const derivedKeyCache = new Map<string, Buffer>();
52
+
53
+ /**
54
+ * Derive a key from master key using scrypt (async, non-blocking)
55
+ * Results are cached to avoid repeated derivation.
56
+ * Cache is bounded to MAX_CACHE_SIZE entries using FIFO eviction.
57
+ */
58
+ async function getDerivedKey(masterKey: string): Promise<Buffer> {
59
+ const cached = derivedKeyCache.get(masterKey);
60
+ if (cached) {
61
+ return cached;
62
+ }
63
+
64
+ // Use @noble/hashes/scrypt instead of node:crypto for Vercel Edge compatibility
65
+ const derivedKeyArray = await scryptAsync(masterKey, SCRYPT_SALT, {
66
+ N: SCRYPT_COST,
67
+ r: SCRYPT_BLOCK_SIZE,
68
+ p: SCRYPT_PARALLELISM,
69
+ dkLen: SCRYPT_KEY_LENGTH,
70
+ });
71
+
72
+ const derivedKey = Buffer.from(derivedKeyArray);
73
+
74
+ // Evict oldest entry if cache is at capacity (FIFO eviction)
75
+ if (derivedKeyCache.size >= MAX_CACHE_SIZE) {
76
+ const oldestKey = derivedKeyCache.keys().next().value;
77
+ if (oldestKey !== undefined) {
78
+ derivedKeyCache.delete(oldestKey);
79
+ }
80
+ }
81
+ derivedKeyCache.set(masterKey, derivedKey);
82
+
83
+ return derivedKey;
84
+ }
85
+
86
+ /**
87
+ * Generate a new workspace encryption key (256-bit)
88
+ */
89
+ export function generateWorkspaceKey(): Buffer {
90
+ return Buffer.from(randomBytes(KEY_LENGTH));
91
+ }
92
+
93
+ /**
94
+ * Encrypt a workspace key with the master key
95
+ * @param workspaceKey - The workspace key to encrypt
96
+ * @param masterKey - The master key (from environment variable)
97
+ * @returns Base64-encoded encrypted key
98
+ */
99
+ export async function encryptWorkspaceKey(
100
+ workspaceKey: Buffer,
101
+ masterKey: string
102
+ ): Promise<string> {
103
+ // Derive a consistent key from the master key string (async, cached)
104
+ const derivedKey = await getDerivedKey(masterKey);
105
+ const iv = randomBytes(IV_LENGTH);
106
+
107
+ const cipher = gcm(new Uint8Array(derivedKey), new Uint8Array(iv));
108
+ const encryptedAndTag = cipher.encrypt(new Uint8Array(workspaceKey));
109
+
110
+ // Format: iv + encrypted + authTag
111
+ return Buffer.concat([
112
+ Buffer.from(iv),
113
+ Buffer.from(encryptedAndTag),
114
+ ]).toString('base64');
115
+ }
116
+
117
+ /**
118
+ * Decrypt a workspace key with the master key
119
+ * @param encryptedKey - Base64-encoded encrypted key
120
+ * @param masterKey - The master key (from environment variable)
121
+ * @returns The decrypted workspace key
122
+ */
123
+ export async function decryptWorkspaceKey(
124
+ encryptedKey: string,
125
+ masterKey: string
126
+ ): Promise<Buffer> {
127
+ const derivedKey = await getDerivedKey(masterKey);
128
+ const data = Buffer.from(encryptedKey, 'base64');
129
+
130
+ const iv = data.subarray(0, IV_LENGTH);
131
+ const encryptedAndTag = data.subarray(IV_LENGTH);
132
+
133
+ const decipher = gcm(new Uint8Array(derivedKey), new Uint8Array(iv));
134
+ const decrypted = decipher.decrypt(new Uint8Array(encryptedAndTag));
135
+
136
+ return Buffer.from(decrypted);
137
+ }
138
+
139
+ /**
140
+ * Encrypt a single field value
141
+ * @param plaintext - The plaintext value to encrypt
142
+ * @param workspaceKey - The decrypted workspace key
143
+ * @returns Base64-encoded ciphertext (iv + encrypted + authTag)
144
+ */
145
+ export function encryptField(
146
+ plaintext: string,
147
+ workspaceKey: Buffer
148
+ ): EncryptedField {
149
+ if (!plaintext) {
150
+ return ''; // Don't encrypt empty strings
151
+ }
152
+
153
+ // Validate workspace key before using it
154
+ if (!Buffer.isBuffer(workspaceKey)) {
155
+ throw new Error(
156
+ `Invalid workspaceKey: expected Buffer, got ${typeof workspaceKey}`
157
+ );
158
+ }
159
+ if (workspaceKey.length !== KEY_LENGTH) {
160
+ throw new Error(
161
+ `Invalid workspaceKey: expected ${KEY_LENGTH} bytes for AES-256, got ${workspaceKey.length} bytes`
162
+ );
163
+ }
164
+
165
+ const iv = randomBytes(IV_LENGTH);
166
+ const cipher = gcm(new Uint8Array(workspaceKey), new Uint8Array(iv));
167
+
168
+ const textBytes = new TextEncoder().encode(plaintext);
169
+ const encryptedAndTag = cipher.encrypt(new Uint8Array(textBytes));
170
+
171
+ return Buffer.concat([
172
+ Buffer.from(iv),
173
+ Buffer.from(encryptedAndTag),
174
+ ]).toString('base64');
175
+ }
176
+
177
+ /**
178
+ * Decrypt a single field value
179
+ * @param ciphertext - Base64-encoded ciphertext
180
+ * @param workspaceKey - The decrypted workspace key
181
+ * @returns The decrypted plaintext
182
+ */
183
+ export function decryptField(
184
+ ciphertext: EncryptedField,
185
+ workspaceKey: Buffer
186
+ ): string {
187
+ if (!ciphertext) {
188
+ return ''; // Handle empty strings
189
+ }
190
+
191
+ const data = Buffer.from(ciphertext, 'base64');
192
+
193
+ if (data.length < IV_LENGTH + AUTH_TAG_LENGTH) {
194
+ // Data too short to be encrypted - may indicate corruption or truncation
195
+ // Log a warning with diagnostic info (scrubbed to avoid leaking sensitive data)
196
+ const expectedMinLength = IV_LENGTH + AUTH_TAG_LENGTH;
197
+ const scrubbedSample = `${ciphertext.slice(0, 8)}...`;
198
+ console.warn(
199
+ `[decryptField] Data too short to decrypt: got ${data.length} bytes, ` +
200
+ `expected at least ${expectedMinLength} bytes. ` +
201
+ `Ciphertext sample: "${scrubbedSample}" (length: ${ciphertext.length}). ` +
202
+ `Returning original value for backward compatibility.`
203
+ );
204
+ return ciphertext;
205
+ }
206
+
207
+ const iv = data.subarray(0, IV_LENGTH);
208
+ const encryptedAndTag = data.subarray(IV_LENGTH);
209
+
210
+ try {
211
+ const decipher = gcm(new Uint8Array(workspaceKey), new Uint8Array(iv));
212
+ const decryptedBytes = decipher.decrypt(new Uint8Array(encryptedAndTag));
213
+ return new TextDecoder().decode(decryptedBytes);
214
+ } catch (error) {
215
+ // Decryption failed - log scrubbed warning and return original for backward compatibility
216
+ const scrubbedSample = `${ciphertext.slice(0, 8)}...`;
217
+ const errorMessage =
218
+ error instanceof Error ? error.message : 'Unknown error';
219
+ console.warn(
220
+ `[decryptField] Decryption failed: ${errorMessage}. ` +
221
+ `Ciphertext sample: "${scrubbedSample}" (length: ${ciphertext.length}). ` +
222
+ `Returning original value for backward compatibility.`
223
+ );
224
+ return ciphertext;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Encrypt sensitive fields of a calendar event
230
+ * @param event - The calendar event with plaintext fields
231
+ * @param workspaceKey - The decrypted workspace key
232
+ * @returns The event with encrypted title, description, location
233
+ */
234
+ export function encryptCalendarEventFields(
235
+ event: EncryptedCalendarEventFields,
236
+ workspaceKey: Buffer
237
+ ): EncryptedCalendarEventFields {
238
+ return {
239
+ title: encryptField(event.title || '', workspaceKey),
240
+ description: encryptField(event.description || '', workspaceKey),
241
+ location: event.location
242
+ ? encryptField(event.location, workspaceKey)
243
+ : undefined,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Decrypt sensitive fields of a calendar event
249
+ * @param event - The calendar event with encrypted fields
250
+ * @param workspaceKey - The decrypted workspace key
251
+ * @returns The event with decrypted title, description, location
252
+ */
253
+ export function decryptCalendarEventFields(
254
+ event: EncryptedCalendarEventFields,
255
+ workspaceKey: Buffer
256
+ ): EncryptedCalendarEventFields {
257
+ return {
258
+ title: decryptField(event.title || '', workspaceKey),
259
+ description: decryptField(event.description || '', workspaceKey),
260
+ location: event.location
261
+ ? decryptField(event.location, workspaceKey)
262
+ : undefined,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Check if encryption is enabled (master key is configured)
268
+ */
269
+ export function isEncryptionEnabled(): boolean {
270
+ return !!process.env.ENCRYPTION_MASTER_KEY?.trim();
271
+ }
272
+
273
+ /**
274
+ * Get the master key from environment
275
+ * @throws Error if master key is not configured
276
+ */
277
+ export function getMasterKey(): string {
278
+ const masterKey = process.env.ENCRYPTION_MASTER_KEY?.trim();
279
+ if (!masterKey) {
280
+ throw new Error(
281
+ 'ENCRYPTION_MASTER_KEY environment variable is not configured'
282
+ );
283
+ }
284
+ return masterKey;
285
+ }
286
+
287
+ /**
288
+ * Batch decrypt multiple calendar events
289
+ * After successful decryption, is_encrypted is set to false to indicate
290
+ * the event data is now plaintext (in memory).
291
+ */
292
+ export function decryptCalendarEvents<
293
+ T extends {
294
+ is_encrypted?: boolean;
295
+ title?: string | null;
296
+ description?: string | null;
297
+ location?: string | null;
298
+ [key: string]: any;
299
+ },
300
+ >(events: T[], workspaceKey: Buffer): T[] {
301
+ return events.map((event) => {
302
+ if (!event.is_encrypted) {
303
+ return event; // Already plaintext
304
+ }
305
+ const decrypted = decryptCalendarEventFields(
306
+ {
307
+ title: event.title ?? '',
308
+ description: event.description ?? '',
309
+ location: event.location ?? undefined,
310
+ },
311
+ workspaceKey
312
+ );
313
+ return {
314
+ ...event,
315
+ ...decrypted,
316
+ is_encrypted: false, // Mark as decrypted (plaintext in memory)
317
+ };
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Batch encrypt multiple calendar events
323
+ */
324
+ export function encryptCalendarEvents<T extends EncryptedCalendarEventFields>(
325
+ events: T[],
326
+ workspaceKey: Buffer
327
+ ): (T & { is_encrypted: boolean })[] {
328
+ return events.map((event) => {
329
+ const encrypted = encryptCalendarEventFields(
330
+ {
331
+ title: event.title,
332
+ description: event.description,
333
+ location: event.location,
334
+ },
335
+ workspaceKey
336
+ );
337
+ return {
338
+ ...event,
339
+ ...encrypted,
340
+ is_encrypted: true,
341
+ };
342
+ });
343
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Encryption utilities for transparent encryption at rest
3
+ */
4
+
5
+ export {
6
+ decryptCalendarEventFields,
7
+ decryptCalendarEvents,
8
+ decryptField,
9
+ decryptWorkspaceKey,
10
+ encryptCalendarEventFields,
11
+ encryptCalendarEvents,
12
+ encryptField,
13
+ encryptWorkspaceKey,
14
+ generateWorkspaceKey,
15
+ getMasterKey,
16
+ isEncryptionEnabled,
17
+ } from './encryption-service';
18
+
19
+ export type {
20
+ CalendarEventWithEncryption,
21
+ EncryptedCalendarEventFields,
22
+ EncryptedField,
23
+ EncryptionConfig,
24
+ WorkspaceEncryptionKey,
25
+ } from './types';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Type definitions for workspace encryption
3
+ */
4
+
5
+ import type { Tables } from '@tuturuuu/types';
6
+
7
+ /**
8
+ * Encrypted calendar event - fields that can be encrypted
9
+ */
10
+ export interface EncryptedCalendarEventFields {
11
+ title: string;
12
+ description: string;
13
+ location?: string;
14
+ }
15
+
16
+ /**
17
+ * Base calendar event type from database schema
18
+ */
19
+ type BaseCalendarEvent = Tables<'workspace_calendar_events'>;
20
+
21
+ /**
22
+ * Calendar event with encryption metadata
23
+ * Derived from canonical DB types to stay in sync with schema changes
24
+ */
25
+ export type CalendarEventWithEncryption = Pick<
26
+ BaseCalendarEvent,
27
+ | 'id'
28
+ | 'title'
29
+ | 'description'
30
+ | 'location'
31
+ | 'start_at'
32
+ | 'end_at'
33
+ | 'color'
34
+ | 'ws_id'
35
+ | 'is_encrypted'
36
+ >;
37
+
38
+ /**
39
+ * Workspace encryption key record from database
40
+ * Re-exported from auto-generated types to stay in sync with schema
41
+ */
42
+ export type { WorkspaceEncryptionKey } from '@tuturuuu/types';
43
+
44
+ /**
45
+ * Encryption configuration
46
+ */
47
+ export interface EncryptionConfig {
48
+ algorithm: 'AES-GCM';
49
+ keyLength: 256;
50
+ ivLength: 12;
51
+ tagLength: 128;
52
+ }
53
+
54
+ /**
55
+ * Encrypted field format: base64(iv + ciphertext + authTag)
56
+ */
57
+ export type EncryptedField = string;
@@ -0,0 +1,49 @@
1
+ export interface ExchangeRate {
2
+ base_currency: string;
3
+ target_currency: string;
4
+ rate: number;
5
+ date: string;
6
+ }
7
+
8
+ /**
9
+ * Convert an amount from one currency to another using USD-based exchange rates.
10
+ *
11
+ * @param amount - The amount to convert
12
+ * @param fromCurrency - Source currency code (e.g., 'EUR')
13
+ * @param toCurrency - Target currency code (e.g., 'VND')
14
+ * @param rates - Array of USD-based exchange rates
15
+ * @returns The converted amount, or null if conversion is not possible
16
+ */
17
+ export function convertCurrency(
18
+ amount: number,
19
+ fromCurrency: string,
20
+ toCurrency: string,
21
+ rates: ExchangeRate[]
22
+ ): number | null {
23
+ const from = fromCurrency.toUpperCase();
24
+ const to = toCurrency.toUpperCase();
25
+
26
+ if (from === to) return amount;
27
+
28
+ // Find USD -> from and USD -> to rates
29
+ const fromRate =
30
+ from === 'USD'
31
+ ? 1
32
+ : (rates.find(
33
+ (r) =>
34
+ r.base_currency === 'USD' &&
35
+ r.target_currency.toUpperCase() === from
36
+ )?.rate ?? null);
37
+
38
+ const toRate =
39
+ to === 'USD'
40
+ ? 1
41
+ : (rates.find(
42
+ (r) =>
43
+ r.base_currency === 'USD' && r.target_currency.toUpperCase() === to
44
+ )?.rate ?? null);
45
+
46
+ if (fromRate === null || toRate === null || fromRate === 0) return null;
47
+
48
+ return amount * (toRate / fromRate);
49
+ }