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