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,662 +0,0 @@
1
- /**
2
- * LSH SaaS Organization & Team Management Service
3
- * Handles organizations, teams, members, and RBAC
4
- */
5
- import { getSupabaseClient } from './supabase-client.js';
6
- import { auditLogger } from './saas-audit.js';
7
- import { TABLES } from '../constants/index.js';
8
- /**
9
- * Generate URL-friendly slug from name
10
- */
11
- function slugify(name) {
12
- let result = name
13
- .toLowerCase()
14
- .trim()
15
- .replace(/[^\w\s-]/g, '')
16
- .replace(/[\s_-]+/g, '-');
17
- // Remove leading and trailing dashes without vulnerable regex
18
- while (result.startsWith('-')) {
19
- result = result.slice(1);
20
- }
21
- while (result.endsWith('-')) {
22
- result = result.slice(0, -1);
23
- }
24
- return result;
25
- }
26
- /**
27
- * Organization Service
28
- */
29
- export class OrganizationService {
30
- supabase = getSupabaseClient();
31
- /**
32
- * Create a new organization
33
- */
34
- async createOrganization(input) {
35
- const slug = input.slug || slugify(input.name);
36
- // Check if slug already exists
37
- const { data: existing } = await this.supabase
38
- .from('organizations')
39
- .select('id')
40
- .eq('slug', slug)
41
- .single();
42
- if (existing) {
43
- throw new Error('ALREADY_EXISTS: Organization slug already taken');
44
- }
45
- // Create organization
46
- const { data: org, error } = await this.supabase
47
- .from('organizations')
48
- .insert({
49
- name: input.name,
50
- slug,
51
- subscription_tier: 'free',
52
- subscription_status: 'active',
53
- })
54
- .select()
55
- .single();
56
- if (error) {
57
- throw new Error(`Failed to create organization: ${error.message}`);
58
- }
59
- // Add owner as first member
60
- await this.addMember({
61
- organizationId: org.id,
62
- userId: input.ownerId,
63
- role: 'owner',
64
- });
65
- // Log audit event
66
- await auditLogger.log({
67
- organizationId: org.id,
68
- userId: input.ownerId,
69
- action: 'organization.create',
70
- resourceType: 'organization',
71
- resourceId: org.id,
72
- newValue: { name: org.name, slug: org.slug },
73
- });
74
- return this.mapDbOrgToOrg(org);
75
- }
76
- /**
77
- * Get organization by ID
78
- */
79
- async getOrganizationById(id) {
80
- const { data: org, error } = await this.supabase
81
- .from('organizations')
82
- .select('*')
83
- .eq('id', id)
84
- .is('deleted_at', null)
85
- .single();
86
- if (error || !org) {
87
- return null;
88
- }
89
- return this.mapDbOrgToOrg(org);
90
- }
91
- /**
92
- * Get organization by slug
93
- */
94
- async getOrganizationBySlug(slug) {
95
- const { data: org, error } = await this.supabase
96
- .from('organizations')
97
- .select('*')
98
- .eq('slug', slug)
99
- .is('deleted_at', null)
100
- .single();
101
- if (error || !org) {
102
- return null;
103
- }
104
- return this.mapDbOrgToOrg(org);
105
- }
106
- /**
107
- * Update organization
108
- */
109
- async updateOrganization(id, updates) {
110
- const updateData = {};
111
- if (updates.name) {
112
- updateData.name = updates.name;
113
- }
114
- if (updates.settings) {
115
- updateData.settings = updates.settings;
116
- }
117
- const { data: org, error } = await this.supabase
118
- .from('organizations')
119
- .update(updateData)
120
- .eq('id', id)
121
- .select()
122
- .single();
123
- if (error) {
124
- throw new Error(`Failed to update organization: ${error.message}`);
125
- }
126
- return this.mapDbOrgToOrg(org);
127
- }
128
- /**
129
- * Delete organization (soft delete)
130
- */
131
- async deleteOrganization(id, deletedBy) {
132
- const { error } = await this.supabase
133
- .from('organizations')
134
- .update({ deleted_at: new Date().toISOString() })
135
- .eq('id', id);
136
- if (error) {
137
- throw new Error(`Failed to delete organization: ${error.message}`);
138
- }
139
- await auditLogger.log({
140
- organizationId: id,
141
- userId: deletedBy,
142
- action: 'organization.delete',
143
- resourceType: 'organization',
144
- resourceId: id,
145
- });
146
- }
147
- /**
148
- * Add member to organization
149
- */
150
- async addMember(params) {
151
- // Check if already a member
152
- const { data: existing } = await this.supabase
153
- .from('organization_members')
154
- .select('id')
155
- .eq('organization_id', params.organizationId)
156
- .eq('user_id', params.userId)
157
- .single();
158
- if (existing) {
159
- throw new Error('ALREADY_EXISTS: User is already a member');
160
- }
161
- // Check tier limits
162
- await this.checkMemberLimit(params.organizationId);
163
- const { data: member, error } = await this.supabase
164
- .from('organization_members')
165
- .insert({
166
- organization_id: params.organizationId,
167
- user_id: params.userId,
168
- role: params.role,
169
- invited_by: params.invitedBy || null,
170
- accepted_at: new Date().toISOString(),
171
- })
172
- .select()
173
- .single();
174
- if (error) {
175
- throw new Error(`Failed to add member: ${error.message}`);
176
- }
177
- await auditLogger.log({
178
- organizationId: params.organizationId,
179
- userId: params.invitedBy || params.userId,
180
- action: 'member.accept',
181
- resourceType: 'user',
182
- resourceId: params.userId,
183
- newValue: { role: params.role },
184
- });
185
- return this.mapDbMemberToMember(member);
186
- }
187
- /**
188
- * Update member role
189
- */
190
- async updateMemberRole(organizationId, userId, newRole, updatedBy) {
191
- const { data: member, error } = await this.supabase
192
- .from('organization_members')
193
- .update({ role: newRole })
194
- .eq('organization_id', organizationId)
195
- .eq('user_id', userId)
196
- .select()
197
- .single();
198
- if (error) {
199
- throw new Error(`Failed to update member role: ${error.message}`);
200
- }
201
- await auditLogger.log({
202
- organizationId,
203
- userId: updatedBy,
204
- action: 'member.role_change',
205
- resourceType: 'user',
206
- resourceId: userId,
207
- newValue: { role: newRole },
208
- });
209
- return this.mapDbMemberToMember(member);
210
- }
211
- /**
212
- * Remove member from organization
213
- */
214
- async removeMember(organizationId, userId, removedBy) {
215
- const { error } = await this.supabase
216
- .from('organization_members')
217
- .delete()
218
- .eq('organization_id', organizationId)
219
- .eq('user_id', userId);
220
- if (error) {
221
- throw new Error(`Failed to remove member: ${error.message}`);
222
- }
223
- await auditLogger.log({
224
- organizationId,
225
- userId: removedBy,
226
- action: 'member.remove',
227
- resourceType: 'user',
228
- resourceId: userId,
229
- });
230
- }
231
- /**
232
- * Get organization members
233
- */
234
- async getOrganizationMembers(organizationId) {
235
- const { data, error } = await this.supabase
236
- .from(TABLES.ORGANIZATION_MEMBERS_DETAILED)
237
- .select('*')
238
- .eq('organization_id', organizationId);
239
- if (error) {
240
- throw new Error(`Failed to get members: ${error.message}`);
241
- }
242
- return (data || []).map(this.mapDbMemberDetailedToMemberDetailed);
243
- }
244
- /**
245
- * Get user's role in organization
246
- */
247
- async getUserRole(organizationId, userId) {
248
- const { data, error } = await this.supabase
249
- .from('organization_members')
250
- .select('role')
251
- .eq('organization_id', organizationId)
252
- .eq('user_id', userId)
253
- .single();
254
- if (error || !data) {
255
- return null;
256
- }
257
- return data.role;
258
- }
259
- /**
260
- * Check if user has permission
261
- */
262
- async hasPermission(organizationId, userId, permission) {
263
- const role = await this.getUserRole(organizationId, userId);
264
- if (!role)
265
- return false;
266
- const { ROLE_PERMISSIONS } = await import('./saas-types.js');
267
- return ROLE_PERMISSIONS[role][permission];
268
- }
269
- /**
270
- * Get organization usage summary
271
- */
272
- async getUsageSummary(organizationId) {
273
- const { data, error } = await this.supabase
274
- .from(TABLES.ORGANIZATION_USAGE_SUMMARY)
275
- .select('*')
276
- .eq('organization_id', organizationId)
277
- .single();
278
- if (error) {
279
- throw new Error(`Failed to get usage summary: ${error.message}`);
280
- }
281
- return {
282
- organizationId: data.organization_id,
283
- name: data.name,
284
- slug: data.slug,
285
- subscriptionTier: data.subscription_tier,
286
- memberCount: data.member_count || 0,
287
- teamCount: data.team_count || 0,
288
- secretCount: data.secret_count || 0,
289
- environmentCount: data.environment_count || 0,
290
- };
291
- }
292
- /**
293
- * Check if organization is within tier limits
294
- */
295
- async checkTierLimits(organizationId) {
296
- const org = await this.getOrganizationById(organizationId);
297
- if (!org) {
298
- throw new Error('NOT_FOUND: Organization not found');
299
- }
300
- const usage = await this.getUsageSummary(organizationId);
301
- const { TIER_LIMITS } = await import('./saas-types.js');
302
- const limits = TIER_LIMITS[org.subscriptionTier];
303
- const violations = [];
304
- if (usage.memberCount > limits.teamMembers) {
305
- violations.push(`Team member limit exceeded (${usage.memberCount}/${limits.teamMembers})`);
306
- }
307
- if (usage.secretCount > limits.secrets) {
308
- violations.push(`Secret limit exceeded (${usage.secretCount}/${limits.secrets})`);
309
- }
310
- if (usage.environmentCount > limits.environments) {
311
- violations.push(`Environment limit exceeded (${usage.environmentCount}/${limits.environments})`);
312
- }
313
- return {
314
- withinLimits: violations.length === 0,
315
- violations,
316
- };
317
- }
318
- /**
319
- * Check member limit before adding
320
- */
321
- async checkMemberLimit(organizationId) {
322
- const usage = await this.getUsageSummary(organizationId);
323
- const org = await this.getOrganizationById(organizationId);
324
- if (!org) {
325
- throw new Error('NOT_FOUND: Organization not found');
326
- }
327
- const { TIER_LIMITS } = await import('./saas-types.js');
328
- const limits = TIER_LIMITS[org.subscriptionTier];
329
- if (usage.memberCount >= limits.teamMembers) {
330
- throw new Error('TIER_LIMIT_EXCEEDED: Team member limit reached. Please upgrade your plan.');
331
- }
332
- }
333
- /**
334
- * Transform Supabase organization record to domain model.
335
- *
336
- * Maps database snake_case columns to TypeScript camelCase properties:
337
- * - `created_at` (ISO string) → `createdAt` (Date)
338
- * - `subscription_tier` (string) → `subscriptionTier` (SubscriptionTier type)
339
- * - `subscription_status` (string) → `subscriptionStatus` (SubscriptionStatus type)
340
- * - `stripe_customer_id` → `stripeCustomerId`
341
- * - `subscription_expires_at` → `subscriptionExpiresAt` (nullable Date)
342
- * - `deleted_at` → `deletedAt` (nullable Date, for soft delete filtering)
343
- *
344
- * Settings are passed through as-is (JSONB column).
345
- *
346
- * @param dbOrg - Supabase record from 'organizations' table
347
- * @returns Domain Organization object with validated types
348
- * @see DbOrganizationRecord in database-types.ts for input shape
349
- * @see Organization in saas-types.ts for output shape
350
- */
351
- mapDbOrgToOrg(dbOrg) {
352
- return {
353
- id: dbOrg.id,
354
- name: dbOrg.name,
355
- slug: dbOrg.slug,
356
- createdAt: new Date(dbOrg.created_at),
357
- updatedAt: new Date(dbOrg.updated_at),
358
- stripeCustomerId: dbOrg.stripe_customer_id,
359
- subscriptionTier: dbOrg.subscription_tier,
360
- subscriptionStatus: dbOrg.subscription_status,
361
- subscriptionExpiresAt: dbOrg.subscription_expires_at
362
- ? new Date(dbOrg.subscription_expires_at)
363
- : null,
364
- settings: dbOrg.settings || {},
365
- deletedAt: dbOrg.deleted_at ? new Date(dbOrg.deleted_at) : null,
366
- };
367
- }
368
- /**
369
- * Transform Supabase organization member record to domain model.
370
- *
371
- * Maps database snake_case columns to TypeScript camelCase properties:
372
- * - `organization_id` → `organizationId`
373
- * - `user_id` → `userId`
374
- * - `role` (string) → `role` (OrganizationRole type)
375
- * - `invited_by` → `invitedBy` (nullable, FK to users.id)
376
- * - `invited_at` (ISO string) → `invitedAt` (Date)
377
- * - `accepted_at` (ISO string) → `acceptedAt` (nullable Date)
378
- *
379
- * @param dbMember - Supabase record from 'organization_members' table
380
- * @returns Domain OrganizationMember object
381
- * @see DbOrganizationMemberRecord in database-types.ts for input shape
382
- * @see OrganizationMember in saas-types.ts for output shape
383
- */
384
- mapDbMemberToMember(dbMember) {
385
- return {
386
- id: dbMember.id,
387
- organizationId: dbMember.organization_id,
388
- userId: dbMember.user_id,
389
- role: dbMember.role,
390
- invitedBy: dbMember.invited_by,
391
- invitedAt: new Date(dbMember.invited_at),
392
- acceptedAt: dbMember.accepted_at ? new Date(dbMember.accepted_at) : null,
393
- createdAt: new Date(dbMember.created_at),
394
- updatedAt: new Date(dbMember.updated_at),
395
- };
396
- }
397
- /**
398
- * Transform Supabase organization member detailed view record to domain model.
399
- *
400
- * This mapper handles records from the 'organization_members_detailed' view,
401
- * which joins organization_members with users and organizations tables.
402
- *
403
- * Maps all OrganizationMember fields plus:
404
- * - `email` (from users table)
405
- * - `first_name` → `firstName` (from users table)
406
- * - `last_name` → `lastName` (from users table)
407
- * - `avatar_url` → `avatarUrl` (from users table)
408
- * - `last_login_at` → `lastLoginAt` (from users table, nullable Date)
409
- * - `organization_name` → `organizationName` (from organizations table)
410
- * - `organization_slug` → `organizationSlug` (from organizations table)
411
- *
412
- * @param dbMember - Supabase record from 'organization_members_detailed' view
413
- * @returns Domain OrganizationMemberDetailed object
414
- * @see DbOrganizationMemberDetailedRecord in database-types.ts for input shape
415
- * @see OrganizationMemberDetailed in saas-types.ts for output shape
416
- */
417
- mapDbMemberDetailedToMemberDetailed(dbMember) {
418
- return {
419
- id: dbMember.id,
420
- organizationId: dbMember.organization_id,
421
- userId: dbMember.user_id,
422
- role: dbMember.role,
423
- invitedBy: dbMember.invited_by,
424
- invitedAt: new Date(dbMember.invited_at),
425
- acceptedAt: dbMember.accepted_at ? new Date(dbMember.accepted_at) : null,
426
- createdAt: new Date(dbMember.created_at),
427
- updatedAt: new Date(dbMember.updated_at),
428
- email: dbMember.email,
429
- firstName: dbMember.first_name,
430
- lastName: dbMember.last_name,
431
- avatarUrl: dbMember.avatar_url,
432
- lastLoginAt: dbMember.last_login_at ? new Date(dbMember.last_login_at) : null,
433
- organizationName: dbMember.organization_name,
434
- organizationSlug: dbMember.organization_slug,
435
- };
436
- }
437
- }
438
- /**
439
- * Team Service
440
- */
441
- export class TeamService {
442
- supabase = getSupabaseClient();
443
- /**
444
- * Create a new team
445
- */
446
- async createTeam(input, createdBy) {
447
- const slug = input.slug || slugify(input.name);
448
- // Check if slug already exists in this org
449
- const { data: existing } = await this.supabase
450
- .from('teams')
451
- .select('id')
452
- .eq('organization_id', input.organizationId)
453
- .eq('slug', slug)
454
- .single();
455
- if (existing) {
456
- throw new Error('ALREADY_EXISTS: Team slug already taken in this organization');
457
- }
458
- const { data: team, error } = await this.supabase
459
- .from('teams')
460
- .insert({
461
- organization_id: input.organizationId,
462
- name: input.name,
463
- slug,
464
- description: input.description || null,
465
- })
466
- .select()
467
- .single();
468
- if (error) {
469
- throw new Error(`Failed to create team: ${error.message}`);
470
- }
471
- await auditLogger.log({
472
- organizationId: input.organizationId,
473
- teamId: team.id,
474
- userId: createdBy,
475
- action: 'team.create',
476
- resourceType: 'team',
477
- resourceId: team.id,
478
- newValue: { name: team.name, slug: team.slug },
479
- });
480
- return this.mapDbTeamToTeam(team);
481
- }
482
- /**
483
- * Get team by ID
484
- */
485
- async getTeamById(id) {
486
- const { data: team, error } = await this.supabase
487
- .from('teams')
488
- .select('*')
489
- .eq('id', id)
490
- .is('deleted_at', null)
491
- .single();
492
- if (error || !team) {
493
- return null;
494
- }
495
- return this.mapDbTeamToTeam(team);
496
- }
497
- /**
498
- * Get teams for organization
499
- */
500
- async getOrganizationTeams(organizationId) {
501
- const { data: teams, error } = await this.supabase
502
- .from('teams')
503
- .select('*')
504
- .eq('organization_id', organizationId)
505
- .is('deleted_at', null)
506
- .order('created_at', { ascending: false });
507
- if (error) {
508
- throw new Error(`Failed to get teams: ${error.message}`);
509
- }
510
- return (teams || []).map(this.mapDbTeamToTeam);
511
- }
512
- /**
513
- * Update team
514
- */
515
- async updateTeam(id, updates, updatedBy) {
516
- const { data: team, error } = await this.supabase
517
- .from('teams')
518
- .update(updates)
519
- .eq('id', id)
520
- .select()
521
- .single();
522
- if (error) {
523
- throw new Error(`Failed to update team: ${error.message}`);
524
- }
525
- await auditLogger.log({
526
- organizationId: team.organization_id,
527
- teamId: id,
528
- userId: updatedBy,
529
- action: 'team.update',
530
- resourceType: 'team',
531
- resourceId: id,
532
- newValue: updates,
533
- });
534
- return this.mapDbTeamToTeam(team);
535
- }
536
- /**
537
- * Delete team (soft delete)
538
- */
539
- async deleteTeam(id, deletedBy) {
540
- const team = await this.getTeamById(id);
541
- if (!team) {
542
- throw new Error('NOT_FOUND: Team not found');
543
- }
544
- const { error } = await this.supabase
545
- .from('teams')
546
- .update({ deleted_at: new Date().toISOString() })
547
- .eq('id', id);
548
- if (error) {
549
- throw new Error(`Failed to delete team: ${error.message}`);
550
- }
551
- await auditLogger.log({
552
- organizationId: team.organizationId,
553
- teamId: id,
554
- userId: deletedBy,
555
- action: 'team.delete',
556
- resourceType: 'team',
557
- resourceId: id,
558
- });
559
- }
560
- /**
561
- * Add member to team
562
- */
563
- async addTeamMember(teamId, userId, role = 'member') {
564
- const { data: member, error } = await this.supabase
565
- .from('team_members')
566
- .insert({
567
- team_id: teamId,
568
- user_id: userId,
569
- role,
570
- })
571
- .select()
572
- .single();
573
- if (error) {
574
- throw new Error(`Failed to add team member: ${error.message}`);
575
- }
576
- return this.mapDbTeamMemberToTeamMember(member);
577
- }
578
- /**
579
- * Remove member from team
580
- */
581
- async removeTeamMember(teamId, userId) {
582
- const { error } = await this.supabase
583
- .from('team_members')
584
- .delete()
585
- .eq('team_id', teamId)
586
- .eq('user_id', userId);
587
- if (error) {
588
- throw new Error(`Failed to remove team member: ${error.message}`);
589
- }
590
- }
591
- /**
592
- * Get team members
593
- */
594
- async getTeamMembers(teamId) {
595
- const { data, error } = await this.supabase
596
- .from(TABLES.TEAM_MEMBERS_DETAILED)
597
- .select('*')
598
- .eq('team_id', teamId);
599
- if (error) {
600
- throw new Error(`Failed to get team members: ${error.message}`);
601
- }
602
- return data || [];
603
- }
604
- /**
605
- * Transform Supabase team record to domain model.
606
- *
607
- * Maps database snake_case columns to TypeScript camelCase properties:
608
- * - `organization_id` → `organizationId`
609
- * - `encryption_key_id` → `encryptionKeyId` (FK to active team encryption key)
610
- * - `created_at` (ISO string) → `createdAt` (Date)
611
- * - `updated_at` (ISO string) → `updatedAt` (Date)
612
- * - `deleted_at` (ISO string) → `deletedAt` (nullable Date, for soft delete)
613
- *
614
- * @param dbTeam - Supabase record from 'teams' table
615
- * @returns Domain Team object
616
- * @see DbTeamRecord in database-types.ts for input shape
617
- * @see Team in saas-types.ts for output shape
618
- */
619
- mapDbTeamToTeam(dbTeam) {
620
- return {
621
- id: dbTeam.id,
622
- organizationId: dbTeam.organization_id,
623
- name: dbTeam.name,
624
- slug: dbTeam.slug,
625
- description: dbTeam.description,
626
- encryptionKeyId: dbTeam.encryption_key_id,
627
- createdAt: new Date(dbTeam.created_at),
628
- updatedAt: new Date(dbTeam.updated_at),
629
- deletedAt: dbTeam.deleted_at ? new Date(dbTeam.deleted_at) : null,
630
- };
631
- }
632
- /**
633
- * Transform Supabase team member record to domain model.
634
- *
635
- * Maps database snake_case columns to TypeScript camelCase properties:
636
- * - `team_id` → `teamId`
637
- * - `user_id` → `userId`
638
- * - `role` (string) → `role` (TeamRole type: 'admin' | 'member' | 'viewer')
639
- * - `created_at` (ISO string) → `createdAt` (Date)
640
- * - `updated_at` (ISO string) → `updatedAt` (Date)
641
- *
642
- * @param dbMember - Supabase record from 'team_members' table
643
- * @returns Domain TeamMember object
644
- * @see DbTeamMemberRecord in database-types.ts for input shape
645
- * @see TeamMember in saas-types.ts for output shape
646
- */
647
- mapDbTeamMemberToTeamMember(dbMember) {
648
- return {
649
- id: dbMember.id,
650
- teamId: dbMember.team_id,
651
- userId: dbMember.user_id,
652
- role: dbMember.role,
653
- createdAt: new Date(dbMember.created_at),
654
- updatedAt: new Date(dbMember.updated_at),
655
- };
656
- }
657
- }
658
- /**
659
- * Singleton instances
660
- */
661
- export const organizationService = new OrganizationService();
662
- export const teamService = new TeamService();