lsh-framework 1.3.2 → 1.4.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.
@@ -0,0 +1,402 @@
1
+ /**
2
+ * LSH SaaS Email Service
3
+ * Email sending using Resend API
4
+ */
5
+ /**
6
+ * Email Service
7
+ */
8
+ export class EmailService {
9
+ config;
10
+ resendApiUrl = 'https://api.resend.com/emails';
11
+ constructor(config) {
12
+ this.config = {
13
+ apiKey: config?.apiKey || process.env.RESEND_API_KEY || '',
14
+ fromEmail: config?.fromEmail || process.env.EMAIL_FROM || 'noreply@lsh.dev',
15
+ fromName: config?.fromName || 'LSH Secrets Manager',
16
+ baseUrl: config?.baseUrl || process.env.BASE_URL || 'https://app.lsh.dev',
17
+ };
18
+ if (!this.config.apiKey) {
19
+ console.warn('RESEND_API_KEY not set - emails will not be sent');
20
+ }
21
+ }
22
+ /**
23
+ * Send email using Resend API
24
+ */
25
+ async sendEmail(params) {
26
+ if (!this.config.apiKey) {
27
+ // Sanitize email parameters to prevent log injection
28
+ const sanitizedTo = params.to.replace(/[\r\n]/g, '');
29
+ const sanitizedSubject = params.subject.replace(/[\r\n]/g, '');
30
+ const sanitizedText = params.text.replace(/[\r\n]/g, ' ');
31
+ console.log('Email would be sent to:', sanitizedTo);
32
+ console.log('Subject:', sanitizedSubject);
33
+ console.log('Text:', sanitizedText);
34
+ return;
35
+ }
36
+ try {
37
+ const response = await fetch(this.resendApiUrl, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ Authorization: `Bearer ${this.config.apiKey}`,
42
+ },
43
+ body: JSON.stringify({
44
+ from: `${this.config.fromName} <${this.config.fromEmail}>`,
45
+ to: params.to,
46
+ subject: params.subject,
47
+ html: params.html,
48
+ text: params.text,
49
+ }),
50
+ });
51
+ if (!response.ok) {
52
+ const error = await response.text();
53
+ throw new Error(`Failed to send email: ${error}`);
54
+ }
55
+ }
56
+ catch (error) {
57
+ console.error('Email send error:', error);
58
+ throw error;
59
+ }
60
+ }
61
+ /**
62
+ * Send email verification
63
+ */
64
+ async sendVerificationEmail(to, token, firstName) {
65
+ const verificationUrl = `${this.config.baseUrl}/verify-email?token=${token}`;
66
+ const name = firstName || 'there';
67
+ const template = this.getVerificationEmailTemplate(name, verificationUrl);
68
+ await this.sendEmail({
69
+ to,
70
+ subject: template.subject,
71
+ html: template.html,
72
+ text: template.text,
73
+ });
74
+ }
75
+ /**
76
+ * Send password reset email
77
+ */
78
+ async sendPasswordResetEmail(to, token, firstName) {
79
+ const resetUrl = `${this.config.baseUrl}/reset-password?token=${token}`;
80
+ const name = firstName || 'there';
81
+ const template = this.getPasswordResetTemplate(name, resetUrl);
82
+ await this.sendEmail({
83
+ to,
84
+ subject: template.subject,
85
+ html: template.html,
86
+ text: template.text,
87
+ });
88
+ }
89
+ /**
90
+ * Send organization invitation email
91
+ */
92
+ async sendOrganizationInvite(to, organizationName, inviterName, inviteUrl) {
93
+ const template = this.getOrganizationInviteTemplate(organizationName, inviterName, inviteUrl);
94
+ await this.sendEmail({
95
+ to,
96
+ subject: template.subject,
97
+ html: template.html,
98
+ text: template.text,
99
+ });
100
+ }
101
+ /**
102
+ * Send welcome email
103
+ */
104
+ async sendWelcomeEmail(to, firstName) {
105
+ const name = firstName || 'there';
106
+ const template = this.getWelcomeEmailTemplate(name);
107
+ await this.sendEmail({
108
+ to,
109
+ subject: template.subject,
110
+ html: template.html,
111
+ text: template.text,
112
+ });
113
+ }
114
+ /**
115
+ * Send subscription confirmation
116
+ */
117
+ async sendSubscriptionConfirmation(to, tier, firstName) {
118
+ const name = firstName || 'there';
119
+ const template = this.getSubscriptionConfirmationTemplate(name, tier);
120
+ await this.sendEmail({
121
+ to,
122
+ subject: template.subject,
123
+ html: template.html,
124
+ text: template.text,
125
+ });
126
+ }
127
+ /**
128
+ * Email verification template
129
+ */
130
+ getVerificationEmailTemplate(name, verificationUrl) {
131
+ return {
132
+ subject: 'Verify your email address',
133
+ html: `
134
+ <!DOCTYPE html>
135
+ <html>
136
+ <head>
137
+ <meta charset="utf-8">
138
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
139
+ </head>
140
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
141
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; text-align: center; border-radius: 8px 8px 0 0;">
142
+ <h1 style="color: white; margin: 0; font-size: 32px;">🔐 LSH</h1>
143
+ <p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0; font-size: 16px;">Secrets Manager</p>
144
+ </div>
145
+
146
+ <div style="background: white; padding: 40px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px;">
147
+ <h2 style="color: #333; margin-top: 0;">Hi ${name}! 👋</h2>
148
+
149
+ <p>Thanks for signing up for LSH Secrets Manager. Please verify your email address to get started.</p>
150
+
151
+ <div style="text-align: center; margin: 30px 0;">
152
+ <a href="${verificationUrl}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 32px; text-decoration: none; border-radius: 6px; font-weight: 600; display: inline-block;">
153
+ Verify Email Address
154
+ </a>
155
+ </div>
156
+
157
+ <p style="color: #666; font-size: 14px;">Or copy and paste this link into your browser:</p>
158
+ <p style="color: #667eea; font-size: 12px; word-break: break-all; background: #f5f5f5; padding: 10px; border-radius: 4px;">${verificationUrl}</p>
159
+
160
+ <p style="color: #666; font-size: 14px; margin-top: 30px;">This link will expire in 24 hours.</p>
161
+
162
+ <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 30px 0;">
163
+
164
+ <p style="color: #999; font-size: 12px; margin: 0;">If you didn't create an account, you can safely ignore this email.</p>
165
+ </div>
166
+ </body>
167
+ </html>
168
+ `,
169
+ text: `
170
+ Hi ${name}!
171
+
172
+ Thanks for signing up for LSH Secrets Manager. Please verify your email address to get started.
173
+
174
+ Verification link: ${verificationUrl}
175
+
176
+ This link will expire in 24 hours.
177
+
178
+ If you didn't create an account, you can safely ignore this email.
179
+
180
+ ---
181
+ LSH Secrets Manager
182
+ `.trim(),
183
+ };
184
+ }
185
+ /**
186
+ * Password reset template
187
+ */
188
+ getPasswordResetTemplate(name, resetUrl) {
189
+ return {
190
+ subject: 'Reset your password',
191
+ html: `
192
+ <!DOCTYPE html>
193
+ <html>
194
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
195
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; text-align: center; border-radius: 8px 8px 0 0;">
196
+ <h1 style="color: white; margin: 0; font-size: 32px;">🔐 LSH</h1>
197
+ <p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0;">Secrets Manager</p>
198
+ </div>
199
+
200
+ <div style="background: white; padding: 40px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px;">
201
+ <h2 style="color: #333; margin-top: 0;">Hi ${name},</h2>
202
+
203
+ <p>We received a request to reset your password. Click the button below to create a new password.</p>
204
+
205
+ <div style="text-align: center; margin: 30px 0;">
206
+ <a href="${resetUrl}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 32px; text-decoration: none; border-radius: 6px; font-weight: 600; display: inline-block;">
207
+ Reset Password
208
+ </a>
209
+ </div>
210
+
211
+ <p style="color: #666; font-size: 14px;">Or copy and paste this link:</p>
212
+ <p style="color: #667eea; font-size: 12px; word-break: break-all; background: #f5f5f5; padding: 10px; border-radius: 4px;">${resetUrl}</p>
213
+
214
+ <p style="color: #666; font-size: 14px; margin-top: 30px;">This link will expire in 1 hour.</p>
215
+
216
+ <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 30px 0;">
217
+
218
+ <p style="color: #999; font-size: 12px;">If you didn't request a password reset, you can safely ignore this email.</p>
219
+ </div>
220
+ </body>
221
+ </html>
222
+ `,
223
+ text: `
224
+ Hi ${name},
225
+
226
+ We received a request to reset your password. Use the link below to create a new password:
227
+
228
+ ${resetUrl}
229
+
230
+ This link will expire in 1 hour.
231
+
232
+ If you didn't request a password reset, you can safely ignore this email.
233
+
234
+ ---
235
+ LSH Secrets Manager
236
+ `.trim(),
237
+ };
238
+ }
239
+ /**
240
+ * Organization invite template
241
+ */
242
+ getOrganizationInviteTemplate(organizationName, inviterName, inviteUrl) {
243
+ return {
244
+ subject: `You've been invited to join ${organizationName} on LSH`,
245
+ html: `
246
+ <!DOCTYPE html>
247
+ <html>
248
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
249
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; text-align: center; border-radius: 8px 8px 0 0;">
250
+ <h1 style="color: white; margin: 0; font-size: 32px;">🔐 LSH</h1>
251
+ </div>
252
+
253
+ <div style="background: white; padding: 40px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px;">
254
+ <h2 style="color: #333; margin-top: 0;">You've been invited! 🎉</h2>
255
+
256
+ <p><strong>${inviterName}</strong> has invited you to join <strong>${organizationName}</strong> on LSH Secrets Manager.</p>
257
+
258
+ <div style="text-align: center; margin: 30px 0;">
259
+ <a href="${inviteUrl}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 32px; text-decoration: none; border-radius: 6px; font-weight: 600; display: inline-block;">
260
+ Accept Invitation
261
+ </a>
262
+ </div>
263
+
264
+ <p style="color: #666; font-size: 14px;">Or copy and paste this link:</p>
265
+ <p style="color: #667eea; font-size: 12px; word-break: break-all; background: #f5f5f5; padding: 10px; border-radius: 4px;">${inviteUrl}</p>
266
+ </div>
267
+ </body>
268
+ </html>
269
+ `,
270
+ text: `
271
+ You've been invited!
272
+
273
+ ${inviterName} has invited you to join ${organizationName} on LSH Secrets Manager.
274
+
275
+ Accept invitation: ${inviteUrl}
276
+
277
+ ---
278
+ LSH Secrets Manager
279
+ `.trim(),
280
+ };
281
+ }
282
+ /**
283
+ * Welcome email template
284
+ */
285
+ getWelcomeEmailTemplate(name) {
286
+ const dashboardUrl = `${this.config.baseUrl}/dashboard`;
287
+ const docsUrl = 'https://docs.lsh.dev';
288
+ return {
289
+ subject: 'Welcome to LSH Secrets Manager! 🚀',
290
+ html: `
291
+ <!DOCTYPE html>
292
+ <html>
293
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
294
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; text-align: center; border-radius: 8px 8px 0 0;">
295
+ <h1 style="color: white; margin: 0; font-size: 32px;">🔐 LSH</h1>
296
+ </div>
297
+
298
+ <div style="background: white; padding: 40px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px;">
299
+ <h2 style="color: #333; margin-top: 0;">Welcome, ${name}! 🎉</h2>
300
+
301
+ <p>Your account is now active. Here's how to get started:</p>
302
+
303
+ <ol style="color: #666;">
304
+ <li><strong>Create a team</strong> - Organize your secrets by project or environment</li>
305
+ <li><strong>Add secrets</strong> - Securely store API keys, tokens, and credentials</li>
306
+ <li><strong>Invite team members</strong> - Collaborate securely with your team</li>
307
+ <li><strong>Install the CLI</strong> - <code>npm install -g lsh</code></li>
308
+ </ol>
309
+
310
+ <div style="text-align: center; margin: 30px 0;">
311
+ <a href="${dashboardUrl}" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 32px; text-decoration: none; border-radius: 6px; font-weight: 600; display: inline-block; margin-right: 10px;">
312
+ Go to Dashboard
313
+ </a>
314
+ <a href="${docsUrl}" style="background: white; color: #667eea; border: 2px solid #667eea; padding: 12px 30px; text-decoration: none; border-radius: 6px; font-weight: 600; display: inline-block;">
315
+ View Docs
316
+ </a>
317
+ </div>
318
+
319
+ <p style="color: #666;">Need help? Reply to this email or check out our <a href="${docsUrl}" style="color: #667eea;">documentation</a>.</p>
320
+ </div>
321
+ </body>
322
+ </html>
323
+ `,
324
+ text: `
325
+ Welcome to LSH Secrets Manager, ${name}!
326
+
327
+ Your account is now active. Here's how to get started:
328
+
329
+ 1. Create a team - Organize your secrets by project or environment
330
+ 2. Add secrets - Securely store API keys, tokens, and credentials
331
+ 3. Invite team members - Collaborate securely with your team
332
+ 4. Install the CLI - npm install -g lsh
333
+
334
+ Dashboard: ${dashboardUrl}
335
+ Documentation: ${docsUrl}
336
+
337
+ Need help? Reply to this email or check out our documentation.
338
+
339
+ ---
340
+ LSH Secrets Manager
341
+ `.trim(),
342
+ };
343
+ }
344
+ /**
345
+ * Subscription confirmation template
346
+ */
347
+ getSubscriptionConfirmationTemplate(name, tier) {
348
+ return {
349
+ subject: `Your ${tier} subscription is active! 🎉`,
350
+ html: `
351
+ <!DOCTYPE html>
352
+ <html>
353
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
354
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; text-align: center; border-radius: 8px 8px 0 0;">
355
+ <h1 style="color: white; margin: 0; font-size: 32px;">🔐 LSH</h1>
356
+ </div>
357
+
358
+ <div style="background: white; padding: 40px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px;">
359
+ <h2 style="color: #333; margin-top: 0;">Thanks, ${name}! 🎉</h2>
360
+
361
+ <p>Your <strong>${tier}</strong> subscription is now active. You now have access to:</p>
362
+
363
+ <ul style="color: #666;">
364
+ ${tier === 'Pro'
365
+ ? `
366
+ <li>Unlimited team members</li>
367
+ <li>Unlimited secrets</li>
368
+ <li>Unlimited environments</li>
369
+ <li>1-year audit log retention</li>
370
+ <li>Priority support</li>
371
+ `
372
+ : `
373
+ <li>Multiple organizations</li>
374
+ <li>SSO/SAML integration</li>
375
+ <li>Unlimited audit log retention</li>
376
+ <li>SLA support</li>
377
+ <li>On-premise deployment option</li>
378
+ `}
379
+ </ul>
380
+
381
+ <p style="color: #666;">Manage your subscription anytime from your account settings.</p>
382
+ </div>
383
+ </body>
384
+ </html>
385
+ `,
386
+ text: `
387
+ Thanks, ${name}!
388
+
389
+ Your ${tier} subscription is now active.
390
+
391
+ Manage your subscription anytime from your account settings.
392
+
393
+ ---
394
+ LSH Secrets Manager
395
+ `.trim(),
396
+ };
397
+ }
398
+ }
399
+ /**
400
+ * Singleton instance
401
+ */
402
+ export const emailService = new EmailService();
@@ -0,0 +1,220 @@
1
+ /**
2
+ * LSH SaaS Per-Team Encryption Service
3
+ * Manages encryption keys for each team
4
+ */
5
+ import { randomBytes, createCipheriv, createDecipheriv, createHash, pbkdf2Sync } from 'crypto';
6
+ import { getSupabaseClient } from './supabase-client.js';
7
+ const ALGORITHM = 'aes-256-cbc';
8
+ const KEY_LENGTH = 32; // 256 bits
9
+ const IV_LENGTH = 16; // 128 bits
10
+ const SALT_LENGTH = 32;
11
+ const PBKDF2_ITERATIONS = 100000;
12
+ /**
13
+ * Get master encryption key from environment
14
+ * This key is used to encrypt/decrypt team encryption keys
15
+ */
16
+ function getMasterKey() {
17
+ const masterKeyHex = process.env.LSH_MASTER_KEY || process.env.LSH_SECRETS_KEY;
18
+ if (!masterKeyHex) {
19
+ throw new Error('LSH_MASTER_KEY or LSH_SECRETS_KEY environment variable must be set for encryption');
20
+ }
21
+ // If it's a hex string, convert it
22
+ if (/^[0-9a-fA-F]+$/.test(masterKeyHex)) {
23
+ return Buffer.from(masterKeyHex, 'hex');
24
+ }
25
+ // Otherwise, derive a key from it using PBKDF2
26
+ const salt = createHash('sha256').update('lsh-saas-master-key-salt').digest();
27
+ return pbkdf2Sync(masterKeyHex, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
28
+ }
29
+ /**
30
+ * Encryption Service
31
+ */
32
+ export class EncryptionService {
33
+ supabase = getSupabaseClient();
34
+ masterKey;
35
+ constructor() {
36
+ this.masterKey = getMasterKey();
37
+ }
38
+ /**
39
+ * Generate a new encryption key for a team
40
+ */
41
+ async generateTeamKey(teamId, createdBy) {
42
+ // Generate random key
43
+ const teamKey = randomBytes(KEY_LENGTH);
44
+ // Encrypt the team key with the master key
45
+ const encryptedKey = this.encryptWithMasterKey(teamKey);
46
+ // Store in database
47
+ const { data, error } = await this.supabase
48
+ .from('encryption_keys')
49
+ .insert({
50
+ team_id: teamId,
51
+ encrypted_key: encryptedKey,
52
+ key_version: 1,
53
+ algorithm: ALGORITHM,
54
+ is_active: true,
55
+ created_by: createdBy,
56
+ })
57
+ .select()
58
+ .single();
59
+ if (error) {
60
+ throw new Error(`Failed to create encryption key: ${error.message}`);
61
+ }
62
+ // Update team to use this key
63
+ await this.supabase
64
+ .from('teams')
65
+ .update({ encryption_key_id: data.id })
66
+ .eq('id', teamId);
67
+ return this.mapDbKeyToKey(data);
68
+ }
69
+ /**
70
+ * Rotate team encryption key
71
+ */
72
+ async rotateTeamKey(teamId, rotatedBy) {
73
+ // Get current key version
74
+ const { data: currentKeys } = await this.supabase
75
+ .from('encryption_keys')
76
+ .select('key_version')
77
+ .eq('team_id', teamId)
78
+ .order('key_version', { ascending: false })
79
+ .limit(1);
80
+ const newVersion = currentKeys && currentKeys.length > 0 ? currentKeys[0].key_version + 1 : 1;
81
+ // Mark old keys as inactive
82
+ await this.supabase
83
+ .from('encryption_keys')
84
+ .update({
85
+ is_active: false,
86
+ rotated_at: new Date().toISOString(),
87
+ })
88
+ .eq('team_id', teamId)
89
+ .eq('is_active', true);
90
+ // Generate new key
91
+ const teamKey = randomBytes(KEY_LENGTH);
92
+ const encryptedKey = this.encryptWithMasterKey(teamKey);
93
+ // Store new key
94
+ const { data, error } = await this.supabase
95
+ .from('encryption_keys')
96
+ .insert({
97
+ team_id: teamId,
98
+ encrypted_key: encryptedKey,
99
+ key_version: newVersion,
100
+ algorithm: ALGORITHM,
101
+ is_active: true,
102
+ created_by: rotatedBy,
103
+ })
104
+ .select()
105
+ .single();
106
+ if (error) {
107
+ throw new Error(`Failed to rotate encryption key: ${error.message}`);
108
+ }
109
+ // Update team
110
+ await this.supabase
111
+ .from('teams')
112
+ .update({ encryption_key_id: data.id })
113
+ .eq('id', teamId);
114
+ return this.mapDbKeyToKey(data);
115
+ }
116
+ /**
117
+ * Get active encryption key for a team
118
+ */
119
+ async getTeamKey(teamId) {
120
+ const { data, error } = await this.supabase
121
+ .from('encryption_keys')
122
+ .select('*')
123
+ .eq('team_id', teamId)
124
+ .eq('is_active', true)
125
+ .single();
126
+ if (error || !data) {
127
+ return null;
128
+ }
129
+ return this.mapDbKeyToKey(data);
130
+ }
131
+ /**
132
+ * Get decrypted team key (for encryption/decryption operations)
133
+ */
134
+ async getDecryptedTeamKey(teamId) {
135
+ const key = await this.getTeamKey(teamId);
136
+ if (!key) {
137
+ throw new Error('No active encryption key found for team');
138
+ }
139
+ return this.decryptWithMasterKey(key.encryptedKey);
140
+ }
141
+ /**
142
+ * Encrypt data with team's key
143
+ */
144
+ async encryptForTeam(teamId, data) {
145
+ const teamKey = await this.getDecryptedTeamKey(teamId);
146
+ // Generate random IV
147
+ const iv = randomBytes(IV_LENGTH);
148
+ // Encrypt
149
+ const cipher = createCipheriv(ALGORITHM, teamKey, iv);
150
+ let encrypted = cipher.update(data, 'utf8', 'hex');
151
+ encrypted += cipher.final('hex');
152
+ // Return IV + encrypted data
153
+ return iv.toString('hex') + ':' + encrypted;
154
+ }
155
+ /**
156
+ * Decrypt data with team's key
157
+ */
158
+ async decryptForTeam(teamId, encryptedData) {
159
+ const teamKey = await this.getDecryptedTeamKey(teamId);
160
+ // Split IV and encrypted data
161
+ const parts = encryptedData.split(':');
162
+ if (parts.length !== 2) {
163
+ throw new Error('Invalid encrypted data format');
164
+ }
165
+ const iv = Buffer.from(parts[0], 'hex');
166
+ const encrypted = parts[1];
167
+ // Decrypt
168
+ const decipher = createDecipheriv(ALGORITHM, teamKey, iv);
169
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
170
+ decrypted += decipher.final('utf8');
171
+ return decrypted;
172
+ }
173
+ /**
174
+ * Encrypt team key with master key
175
+ */
176
+ encryptWithMasterKey(teamKey) {
177
+ const iv = randomBytes(IV_LENGTH);
178
+ const cipher = createCipheriv(ALGORITHM, this.masterKey, iv);
179
+ let encrypted = cipher.update(teamKey);
180
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
181
+ // Return IV + encrypted key
182
+ return iv.toString('hex') + ':' + encrypted.toString('hex');
183
+ }
184
+ /**
185
+ * Decrypt team key with master key
186
+ */
187
+ decryptWithMasterKey(encryptedKey) {
188
+ const parts = encryptedKey.split(':');
189
+ if (parts.length !== 2) {
190
+ throw new Error('Invalid encrypted key format');
191
+ }
192
+ const iv = Buffer.from(parts[0], 'hex');
193
+ const encrypted = Buffer.from(parts[1], 'hex');
194
+ const decipher = createDecipheriv(ALGORITHM, this.masterKey, iv);
195
+ let decrypted = decipher.update(encrypted);
196
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
197
+ return decrypted;
198
+ }
199
+ /**
200
+ * Map database key to EncryptionKey type
201
+ */
202
+ mapDbKeyToKey(dbKey) {
203
+ return {
204
+ id: dbKey.id,
205
+ teamId: dbKey.team_id,
206
+ encryptedKey: dbKey.encrypted_key,
207
+ keyVersion: dbKey.key_version,
208
+ algorithm: dbKey.algorithm,
209
+ isActive: dbKey.is_active,
210
+ rotatedAt: dbKey.rotated_at ? new Date(dbKey.rotated_at) : null,
211
+ expiresAt: dbKey.expires_at ? new Date(dbKey.expires_at) : null,
212
+ createdAt: new Date(dbKey.created_at),
213
+ createdBy: dbKey.created_by,
214
+ };
215
+ }
216
+ }
217
+ /**
218
+ * Singleton instance
219
+ */
220
+ export const encryptionService = new EncryptionService();