lsh-framework 3.2.4 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) 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/sync.js +51 -39
  5. package/dist/constants/config.js +3 -0
  6. package/dist/lib/floating-point-arithmetic.js +2 -2
  7. package/dist/lib/ipfs-client-manager.js +51 -13
  8. package/dist/lib/ipfs-secrets-storage.js +21 -16
  9. package/dist/lib/ipfs-sync.js +88 -14
  10. package/dist/lib/secrets-manager.js +117 -47
  11. package/dist/lib/sync-key-store.js +87 -0
  12. package/dist/services/secrets/secrets.js +77 -39
  13. package/package.json +16 -16
  14. package/dist/__tests__/fixtures/job-fixtures.js +0 -204
  15. package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
  16. package/dist/daemon/job-registry.js +0 -556
  17. package/dist/daemon/lshd.js +0 -968
  18. package/dist/daemon/saas-api-routes.js +0 -599
  19. package/dist/daemon/saas-api-server.js +0 -231
  20. package/dist/examples/supabase-integration.js +0 -106
  21. package/dist/lib/api-response.js +0 -226
  22. package/dist/lib/base-command-registrar.js +0 -287
  23. package/dist/lib/base-job-manager.js +0 -295
  24. package/dist/lib/cloud-config-manager.js +0 -348
  25. package/dist/lib/cron-job-manager.js +0 -368
  26. package/dist/lib/daemon-client-helper.js +0 -145
  27. package/dist/lib/daemon-client.js +0 -513
  28. package/dist/lib/database-persistence.js +0 -727
  29. package/dist/lib/database-schema.js +0 -259
  30. package/dist/lib/database-types.js +0 -90
  31. package/dist/lib/enhanced-history-system.js +0 -247
  32. package/dist/lib/history-system.js +0 -246
  33. package/dist/lib/job-manager.js +0 -436
  34. package/dist/lib/job-storage-database.js +0 -164
  35. package/dist/lib/job-storage-memory.js +0 -73
  36. package/dist/lib/local-storage-adapter.js +0 -507
  37. package/dist/lib/optimized-job-scheduler.js +0 -356
  38. package/dist/lib/saas-audit.js +0 -215
  39. package/dist/lib/saas-auth.js +0 -465
  40. package/dist/lib/saas-billing.js +0 -503
  41. package/dist/lib/saas-email.js +0 -403
  42. package/dist/lib/saas-encryption.js +0 -221
  43. package/dist/lib/saas-organizations.js +0 -662
  44. package/dist/lib/saas-secrets.js +0 -408
  45. package/dist/lib/saas-types.js +0 -165
  46. package/dist/lib/supabase-client.js +0 -125
  47. package/dist/lib/supabase-utils.js +0 -396
  48. package/dist/services/cron/cron-registrar.js +0 -240
  49. package/dist/services/cron/cron.js +0 -9
  50. package/dist/services/daemon/daemon-registrar.js +0 -585
  51. package/dist/services/daemon/daemon.js +0 -9
  52. package/dist/services/supabase/supabase-registrar.js +0 -375
  53. package/dist/services/supabase/supabase.js +0 -9
@@ -1,465 +0,0 @@
1
- /**
2
- * LSH SaaS Authentication Service
3
- * Handles user signup, login, email verification, and session management
4
- */
5
- import { randomBytes } from 'crypto';
6
- import bcrypt from 'bcrypt';
7
- import jwt from 'jsonwebtoken';
8
- import { getSupabaseClient } from './supabase-client.js';
9
- import { ENV_VARS } from '../constants/index.js';
10
- const BCRYPT_ROUNDS = 12;
11
- const TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days in seconds
12
- const REFRESH_TOKEN_EXPIRY = 30 * 24 * 60 * 60; // 30 days
13
- const EMAIL_VERIFICATION_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in ms
14
- /**
15
- * Generate a secure random token
16
- */
17
- function generateToken(length = 32) {
18
- return randomBytes(length).toString('hex');
19
- }
20
- /**
21
- * Hash a password using bcrypt
22
- */
23
- export async function hashPassword(password) {
24
- return bcrypt.hash(password, BCRYPT_ROUNDS);
25
- }
26
- /**
27
- * Verify a password against a hash
28
- */
29
- export async function verifyPassword(password, hash) {
30
- return bcrypt.compare(password, hash);
31
- }
32
- /**
33
- * Generate JWT access token
34
- */
35
- export function generateAccessToken(userId, email) {
36
- const secret = process.env[ENV_VARS.LSH_JWT_SECRET];
37
- if (!secret) {
38
- throw new Error('LSH_JWT_SECRET is not set');
39
- }
40
- return jwt.sign({
41
- sub: userId,
42
- email,
43
- type: 'access',
44
- }, secret, {
45
- expiresIn: TOKEN_EXPIRY,
46
- issuer: 'lsh-saas',
47
- audience: 'lsh-api',
48
- });
49
- }
50
- /**
51
- * Generate JWT refresh token
52
- */
53
- export function generateRefreshToken(userId) {
54
- const secret = process.env[ENV_VARS.LSH_JWT_SECRET];
55
- if (!secret) {
56
- throw new Error('LSH_JWT_SECRET is not set');
57
- }
58
- return jwt.sign({
59
- sub: userId,
60
- type: 'refresh',
61
- }, secret, {
62
- expiresIn: REFRESH_TOKEN_EXPIRY,
63
- issuer: 'lsh-saas',
64
- audience: 'lsh-api',
65
- });
66
- }
67
- /**
68
- * Verify and decode JWT token
69
- */
70
- export function verifyToken(token) {
71
- const secret = process.env[ENV_VARS.LSH_JWT_SECRET];
72
- if (!secret) {
73
- throw new Error('LSH_JWT_SECRET is not set');
74
- }
75
- try {
76
- const decoded = jwt.verify(token, secret, {
77
- issuer: 'lsh-saas',
78
- audience: 'lsh-api',
79
- });
80
- return {
81
- userId: decoded.sub,
82
- email: decoded.email,
83
- type: decoded.type,
84
- };
85
- }
86
- catch (_error) {
87
- throw new Error('Invalid or expired token');
88
- }
89
- }
90
- /**
91
- * Authentication Service
92
- */
93
- export class AuthService {
94
- supabase = getSupabaseClient();
95
- /**
96
- * Sign up a new user
97
- */
98
- async signup(input) {
99
- // Check if email already exists
100
- const { data: existingUser } = await this.supabase
101
- .from('users')
102
- .select('id')
103
- .eq('email', input.email.toLowerCase())
104
- .single();
105
- if (existingUser) {
106
- throw new Error('EMAIL_ALREADY_EXISTS');
107
- }
108
- // Hash password
109
- const passwordHash = await hashPassword(input.password);
110
- // Generate email verification token
111
- const verificationToken = generateToken();
112
- const verificationExpiresAt = new Date(Date.now() + EMAIL_VERIFICATION_EXPIRY);
113
- // Create user
114
- const { data: user, error } = await this.supabase
115
- .from('users')
116
- .insert({
117
- email: input.email.toLowerCase(),
118
- password_hash: passwordHash,
119
- first_name: input.firstName || null,
120
- last_name: input.lastName || null,
121
- email_verified: false,
122
- email_verification_token: verificationToken,
123
- email_verification_expires_at: verificationExpiresAt.toISOString(),
124
- })
125
- .select()
126
- .single();
127
- if (error) {
128
- throw new Error(`Failed to create user: ${error.message}`);
129
- }
130
- return {
131
- user: this.mapDbUserToUser(user),
132
- verificationToken,
133
- };
134
- }
135
- /**
136
- * Verify email address
137
- */
138
- async verifyEmail(token) {
139
- const { data: user, error } = await this.supabase
140
- .from('users')
141
- .select('*')
142
- .eq('email_verification_token', token)
143
- .single();
144
- if (error || !user) {
145
- throw new Error('INVALID_TOKEN');
146
- }
147
- // Check if token expired
148
- const expiresAt = new Date(user.email_verification_expires_at);
149
- if (expiresAt < new Date()) {
150
- throw new Error('INVALID_TOKEN');
151
- }
152
- // Mark email as verified
153
- const { data: updatedUser, error: updateError } = await this.supabase
154
- .from('users')
155
- .update({
156
- email_verified: true,
157
- email_verification_token: null,
158
- email_verification_expires_at: null,
159
- })
160
- .eq('id', user.id)
161
- .select()
162
- .single();
163
- if (updateError) {
164
- throw new Error('Failed to verify email');
165
- }
166
- return this.mapDbUserToUser(updatedUser);
167
- }
168
- /**
169
- * Resend email verification
170
- */
171
- async resendVerificationEmail(email) {
172
- const { data: user, error } = await this.supabase
173
- .from('users')
174
- .select('*')
175
- .eq('email', email.toLowerCase())
176
- .single();
177
- if (error || !user) {
178
- throw new Error('NOT_FOUND');
179
- }
180
- if (user.email_verified) {
181
- throw new Error('Email already verified');
182
- }
183
- // Generate new token
184
- const verificationToken = generateToken();
185
- const verificationExpiresAt = new Date(Date.now() + EMAIL_VERIFICATION_EXPIRY);
186
- await this.supabase
187
- .from('users')
188
- .update({
189
- email_verification_token: verificationToken,
190
- email_verification_expires_at: verificationExpiresAt.toISOString(),
191
- })
192
- .eq('id', user.id);
193
- return verificationToken;
194
- }
195
- /**
196
- * Login with email and password
197
- */
198
- async login(input, ipAddress) {
199
- // Find user
200
- const { data: user, error } = await this.supabase
201
- .from('users')
202
- .select('*')
203
- .eq('email', input.email.toLowerCase())
204
- .is('deleted_at', null)
205
- .single();
206
- if (error || !user) {
207
- throw new Error('INVALID_CREDENTIALS');
208
- }
209
- // Verify password
210
- if (!user.password_hash) {
211
- throw new Error('INVALID_CREDENTIALS');
212
- }
213
- const isValidPassword = await verifyPassword(input.password, user.password_hash);
214
- if (!isValidPassword) {
215
- throw new Error('INVALID_CREDENTIALS');
216
- }
217
- // Check if email is verified
218
- if (!user.email_verified) {
219
- throw new Error('EMAIL_NOT_VERIFIED');
220
- }
221
- // Update last login
222
- await this.supabase
223
- .from('users')
224
- .update({
225
- last_login_at: new Date().toISOString(),
226
- last_login_ip: ipAddress || null,
227
- })
228
- .eq('id', user.id);
229
- // Get user's organizations
230
- const organizations = await this.getUserOrganizations(user.id);
231
- // Generate tokens
232
- const accessToken = generateAccessToken(user.id, user.email);
233
- const refreshToken = generateRefreshToken(user.id);
234
- return {
235
- user: this.mapDbUserToUser(user),
236
- organizations,
237
- currentOrganization: organizations[0],
238
- token: {
239
- accessToken,
240
- refreshToken,
241
- expiresIn: TOKEN_EXPIRY,
242
- },
243
- };
244
- }
245
- /**
246
- * Refresh access token
247
- */
248
- async refreshAccessToken(refreshToken) {
249
- const { userId } = verifyToken(refreshToken);
250
- const { data: user, error } = await this.supabase
251
- .from('users')
252
- .select('email')
253
- .eq('id', userId)
254
- .is('deleted_at', null)
255
- .single();
256
- if (error || !user) {
257
- throw new Error('INVALID_TOKEN');
258
- }
259
- const accessToken = generateAccessToken(userId, user.email);
260
- const newRefreshToken = generateRefreshToken(userId);
261
- return {
262
- accessToken,
263
- refreshToken: newRefreshToken,
264
- expiresIn: TOKEN_EXPIRY,
265
- };
266
- }
267
- /**
268
- * Get user by ID
269
- */
270
- async getUserById(userId) {
271
- const { data: user, error } = await this.supabase
272
- .from('users')
273
- .select('*')
274
- .eq('id', userId)
275
- .is('deleted_at', null)
276
- .single();
277
- if (error || !user) {
278
- return null;
279
- }
280
- return this.mapDbUserToUser(user);
281
- }
282
- /**
283
- * Get user by email
284
- */
285
- async getUserByEmail(email) {
286
- const { data: user, error } = await this.supabase
287
- .from('users')
288
- .select('*')
289
- .eq('email', email.toLowerCase())
290
- .is('deleted_at', null)
291
- .single();
292
- if (error || !user) {
293
- return null;
294
- }
295
- return this.mapDbUserToUser(user);
296
- }
297
- /**
298
- * Get user's organizations
299
- */
300
- async getUserOrganizations(userId) {
301
- const { data, error } = await this.supabase
302
- .from('organization_members')
303
- .select(`
304
- organization_id,
305
- organizations (
306
- id,
307
- name,
308
- slug,
309
- created_at,
310
- updated_at,
311
- stripe_customer_id,
312
- subscription_tier,
313
- subscription_status,
314
- subscription_expires_at,
315
- settings,
316
- deleted_at
317
- )
318
- `)
319
- .eq('user_id', userId)
320
- .is('organizations.deleted_at', null);
321
- if (error) {
322
- throw new Error(`Failed to get user organizations: ${error.message}`);
323
- }
324
- return (data || []).map((row) => this.mapDbOrgToOrg(row.organizations[0]));
325
- }
326
- /**
327
- * Request password reset
328
- */
329
- async requestPasswordReset(email) {
330
- const { data: user, error } = await this.supabase
331
- .from('users')
332
- .select('id')
333
- .eq('email', email.toLowerCase())
334
- .is('deleted_at', null)
335
- .single();
336
- if (error || !user) {
337
- // Don't reveal if email exists
338
- return generateToken();
339
- }
340
- const resetToken = generateToken();
341
- const _expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour (for future use)
342
- // Store reset token (we'll need a password_reset_tokens table)
343
- // For now, just return the token
344
- return resetToken;
345
- }
346
- /**
347
- * Reset password
348
- */
349
- async resetPassword(_token, _newPassword) {
350
- // TODO: Implement password reset
351
- // Need to create password_reset_tokens table
352
- throw new Error('Not implemented');
353
- }
354
- /**
355
- * Change password
356
- */
357
- async changePassword(userId, currentPassword, newPassword) {
358
- const { data: user, error } = await this.supabase
359
- .from('users')
360
- .select('password_hash')
361
- .eq('id', userId)
362
- .single();
363
- if (error || !user) {
364
- throw new Error('NOT_FOUND');
365
- }
366
- // Verify current password
367
- if (!user.password_hash) {
368
- throw new Error('No password set');
369
- }
370
- const isValid = await verifyPassword(currentPassword, user.password_hash);
371
- if (!isValid) {
372
- throw new Error('INVALID_CREDENTIALS');
373
- }
374
- // Hash new password
375
- const newHash = await hashPassword(newPassword);
376
- // Update password
377
- await this.supabase.from('users').update({ password_hash: newHash }).eq('id', userId);
378
- }
379
- /**
380
- * Transform Supabase user record to domain model.
381
- *
382
- * Maps database snake_case columns to TypeScript camelCase properties:
383
- * - `email_verified` → `emailVerified` (boolean)
384
- * - `email_verification_token` → `emailVerificationToken` (nullable string)
385
- * - `email_verification_expires_at` → `emailVerificationExpiresAt` (nullable Date)
386
- * - `password_hash` → `passwordHash` (nullable, null for OAuth-only users)
387
- * - `oauth_provider` → `oauthProvider` ('google' | 'github' | 'microsoft' | null)
388
- * - `oauth_provider_id` → `oauthProviderId` (nullable)
389
- * - `first_name` → `firstName` (nullable)
390
- * - `last_name` → `lastName` (nullable)
391
- * - `avatar_url` → `avatarUrl` (nullable)
392
- * - `last_login_at` → `lastLoginAt` (nullable Date)
393
- * - `last_login_ip` → `lastLoginIp` (nullable)
394
- * - `deleted_at` → `deletedAt` (nullable Date, for soft delete)
395
- *
396
- * Security note: passwordHash is included in domain model but should
397
- * never be exposed in API responses.
398
- *
399
- * @param dbUser - Supabase record from 'users' table
400
- * @returns Domain User object
401
- * @see DbUserRecord in database-types.ts for input shape
402
- * @see User in saas-types.ts for output shape
403
- */
404
- mapDbUserToUser(dbUser) {
405
- return {
406
- id: dbUser.id,
407
- email: dbUser.email,
408
- emailVerified: dbUser.email_verified,
409
- emailVerificationToken: dbUser.email_verification_token,
410
- emailVerificationExpiresAt: dbUser.email_verification_expires_at
411
- ? new Date(dbUser.email_verification_expires_at)
412
- : null,
413
- passwordHash: dbUser.password_hash,
414
- oauthProvider: dbUser.oauth_provider,
415
- oauthProviderId: dbUser.oauth_provider_id,
416
- firstName: dbUser.first_name,
417
- lastName: dbUser.last_name,
418
- avatarUrl: dbUser.avatar_url,
419
- lastLoginAt: dbUser.last_login_at ? new Date(dbUser.last_login_at) : null,
420
- lastLoginIp: dbUser.last_login_ip,
421
- createdAt: new Date(dbUser.created_at),
422
- updatedAt: new Date(dbUser.updated_at),
423
- deletedAt: dbUser.deleted_at ? new Date(dbUser.deleted_at) : null,
424
- };
425
- }
426
- /**
427
- * Transform Supabase organization record to domain model.
428
- *
429
- * Used when fetching user's organizations via the organization_members join.
430
- * Identical mapping logic to OrganizationService.mapDbOrgToOrg().
431
- *
432
- * Maps database snake_case columns to TypeScript camelCase properties:
433
- * - `stripe_customer_id` → `stripeCustomerId`
434
- * - `subscription_tier` → `subscriptionTier` (SubscriptionTier type)
435
- * - `subscription_status` → `subscriptionStatus` (SubscriptionStatus type)
436
- * - `subscription_expires_at` → `subscriptionExpiresAt` (nullable Date)
437
- * - `deleted_at` → `deletedAt` (nullable Date)
438
- *
439
- * @param dbOrg - Supabase record from 'organizations' table (via join)
440
- * @returns Domain Organization object
441
- * @see DbOrganizationRecord in database-types.ts for input shape
442
- * @see Organization in saas-types.ts for output shape
443
- */
444
- mapDbOrgToOrg(dbOrg) {
445
- return {
446
- id: dbOrg.id,
447
- name: dbOrg.name,
448
- slug: dbOrg.slug,
449
- createdAt: new Date(dbOrg.created_at),
450
- updatedAt: new Date(dbOrg.updated_at),
451
- stripeCustomerId: dbOrg.stripe_customer_id,
452
- subscriptionTier: dbOrg.subscription_tier,
453
- subscriptionStatus: dbOrg.subscription_status,
454
- subscriptionExpiresAt: dbOrg.subscription_expires_at
455
- ? new Date(dbOrg.subscription_expires_at)
456
- : null,
457
- settings: dbOrg.settings || {},
458
- deletedAt: dbOrg.deleted_at ? new Date(dbOrg.deleted_at) : null,
459
- };
460
- }
461
- }
462
- /**
463
- * Singleton instance
464
- */
465
- export const authService = new AuthService();