@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.
- package/CHANGELOG.md +313 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- 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
|
+
}
|