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,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();