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,408 +0,0 @@
1
- /**
2
- * LSH SaaS Secrets Management Service
3
- * Multi-tenant secrets with per-team encryption
4
- */
5
- import { getErrorMessage, } from './saas-types.js';
6
- import { getSupabaseClient } from './supabase-client.js';
7
- import { encryptionService } from './saas-encryption.js';
8
- import { auditLogger } from './saas-audit.js';
9
- import { organizationService } from './saas-organizations.js';
10
- import { TABLES } from '../constants/index.js';
11
- /**
12
- * Secrets Service
13
- */
14
- export class SecretsService {
15
- supabase = getSupabaseClient();
16
- /**
17
- * Create a new secret
18
- */
19
- async createSecret(input) {
20
- // Check tier limits
21
- await this.checkSecretsLimit(input.teamId);
22
- // Get or create encryption key for team
23
- let encryptionKey = await encryptionService.getTeamKey(input.teamId);
24
- if (!encryptionKey) {
25
- // Auto-create encryption key for team
26
- const team = await this.getTeamById(input.teamId);
27
- if (!team) {
28
- throw new Error('Team not found');
29
- }
30
- encryptionKey = await encryptionService.generateTeamKey(input.teamId, input.createdBy);
31
- }
32
- // Encrypt the secret value
33
- const encryptedValue = await encryptionService.encryptForTeam(input.teamId, input.value);
34
- // Store secret
35
- const { data, error } = await this.supabase
36
- .from('secrets')
37
- .insert({
38
- team_id: input.teamId,
39
- environment: input.environment,
40
- key: input.key,
41
- encrypted_value: encryptedValue,
42
- encryption_key_id: encryptionKey.id,
43
- description: input.description || null,
44
- tags: JSON.stringify(input.tags || []),
45
- rotation_interval_days: input.rotationIntervalDays || null,
46
- created_by: input.createdBy,
47
- })
48
- .select()
49
- .single();
50
- if (error) {
51
- throw new Error(`Failed to create secret: ${error.message}`);
52
- }
53
- // Audit log
54
- const team = await this.getTeamById(input.teamId);
55
- if (team) {
56
- await auditLogger.log({
57
- organizationId: team.organization_id,
58
- teamId: input.teamId,
59
- userId: input.createdBy,
60
- action: 'secret.create',
61
- resourceType: 'secret',
62
- resourceId: data.id,
63
- newValue: {
64
- key: input.key,
65
- environment: input.environment,
66
- },
67
- });
68
- }
69
- return this.mapDbSecretToSecret(data);
70
- }
71
- /**
72
- * Get secret by ID
73
- */
74
- async getSecretById(id, decrypt = false) {
75
- const { data, error } = await this.supabase
76
- .from('secrets')
77
- .select('*')
78
- .eq('id', id)
79
- .is('deleted_at', null)
80
- .single();
81
- if (error || !data) {
82
- return null;
83
- }
84
- const secret = this.mapDbSecretToSecret(data);
85
- // Decrypt if requested
86
- if (decrypt) {
87
- const decryptedValue = await encryptionService.decryptForTeam(secret.teamId, secret.encryptedValue);
88
- return { ...secret, encryptedValue: decryptedValue };
89
- }
90
- return secret;
91
- }
92
- /**
93
- * Get secrets for team/environment
94
- */
95
- async getTeamSecrets(teamId, environment, decrypt = false) {
96
- let query = this.supabase
97
- .from('secrets')
98
- .select('*')
99
- .eq('team_id', teamId)
100
- .is('deleted_at', null);
101
- if (environment) {
102
- query = query.eq('environment', environment);
103
- }
104
- query = query.order('key', { ascending: true });
105
- const { data, error } = await query;
106
- if (error) {
107
- throw new Error(`Failed to get secrets: ${error.message}`);
108
- }
109
- const secrets = (data || []).map(this.mapDbSecretToSecret);
110
- // Decrypt if requested
111
- if (decrypt) {
112
- return Promise.all(secrets.map(async (secret) => {
113
- try {
114
- const decryptedValue = await encryptionService.decryptForTeam(teamId, secret.encryptedValue);
115
- return { ...secret, encryptedValue: decryptedValue };
116
- }
117
- catch (error) {
118
- console.error(`Failed to decrypt secret ${secret.id}:`, error);
119
- return secret;
120
- }
121
- }));
122
- }
123
- return secrets;
124
- }
125
- /**
126
- * Update secret
127
- */
128
- async updateSecret(id, input) {
129
- const secret = await this.getSecretById(id);
130
- if (!secret) {
131
- throw new Error('Secret not found');
132
- }
133
- const updateData = {
134
- updated_by: input.updatedBy,
135
- updated_at: new Date().toISOString(),
136
- };
137
- // Encrypt new value if provided
138
- if (input.value) {
139
- updateData.encrypted_value = await encryptionService.encryptForTeam(secret.teamId, input.value);
140
- }
141
- if (input.description !== undefined) {
142
- updateData.description = input.description;
143
- }
144
- if (input.tags) {
145
- updateData.tags = JSON.stringify(input.tags);
146
- }
147
- if (input.rotationIntervalDays !== undefined) {
148
- updateData.rotation_interval_days = input.rotationIntervalDays;
149
- }
150
- const { data, error } = await this.supabase
151
- .from('secrets')
152
- .update(updateData)
153
- .eq('id', id)
154
- .select()
155
- .single();
156
- if (error) {
157
- throw new Error(`Failed to update secret: ${error.message}`);
158
- }
159
- // Audit log
160
- const team = await this.getTeamById(secret.teamId);
161
- if (team) {
162
- await auditLogger.log({
163
- organizationId: team.organization_id,
164
- teamId: secret.teamId,
165
- userId: input.updatedBy,
166
- action: 'secret.update',
167
- resourceType: 'secret',
168
- resourceId: id,
169
- oldValue: { description: secret.description },
170
- newValue: { description: input.description },
171
- });
172
- }
173
- return this.mapDbSecretToSecret(data);
174
- }
175
- /**
176
- * Delete secret (soft delete)
177
- */
178
- async deleteSecret(id, deletedBy) {
179
- const secret = await this.getSecretById(id);
180
- if (!secret) {
181
- throw new Error('Secret not found');
182
- }
183
- const { error } = await this.supabase
184
- .from('secrets')
185
- .update({
186
- deleted_at: new Date().toISOString(),
187
- deleted_by: deletedBy,
188
- })
189
- .eq('id', id);
190
- if (error) {
191
- throw new Error(`Failed to delete secret: ${error.message}`);
192
- }
193
- // Audit log
194
- const team = await this.getTeamById(secret.teamId);
195
- if (team) {
196
- await auditLogger.log({
197
- organizationId: team.organization_id,
198
- teamId: secret.teamId,
199
- userId: deletedBy,
200
- action: 'secret.delete',
201
- resourceType: 'secret',
202
- resourceId: id,
203
- oldValue: {
204
- key: secret.key,
205
- environment: secret.environment,
206
- },
207
- });
208
- }
209
- }
210
- /**
211
- * Get secrets summary by team
212
- */
213
- async getSecretsSummary(teamId) {
214
- const { data, error } = await this.supabase
215
- .from(TABLES.SECRETS_SUMMARY)
216
- .select('*')
217
- .eq('team_id', teamId);
218
- if (error) {
219
- throw new Error(`Failed to get secrets summary: ${error.message}`);
220
- }
221
- return (data || []).map((row) => ({
222
- teamId: row.team_id,
223
- teamName: row.team_name,
224
- environment: row.environment,
225
- secretsCount: row.secrets_count || 0,
226
- lastUpdated: row.last_updated ? new Date(row.last_updated) : null,
227
- }));
228
- }
229
- /**
230
- * Export secrets to .env format
231
- */
232
- async exportToEnv(teamId, environment) {
233
- const secrets = await this.getTeamSecrets(teamId, environment, true);
234
- const envLines = secrets.map((secret) => {
235
- // Escape special characters in values (backslashes first, then quotes)
236
- const value = secret.encryptedValue.includes(' ')
237
- ? `"${secret.encryptedValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
238
- : secret.encryptedValue;
239
- const comment = secret.description ? `# ${secret.description}\n` : '';
240
- return `${comment}${secret.key}=${value}`;
241
- });
242
- return envLines.join('\n');
243
- }
244
- /**
245
- * Import secrets from .env format
246
- */
247
- async importFromEnv(teamId, environment, envContent, createdBy) {
248
- const lines = envContent.split('\n');
249
- const secrets = [];
250
- let currentDescription = '';
251
- // Parse .env file
252
- for (const line of lines) {
253
- const trimmed = line.trim();
254
- // Skip empty lines
255
- if (!trimmed) {
256
- currentDescription = '';
257
- continue;
258
- }
259
- // Comment line (description)
260
- if (trimmed.startsWith('#')) {
261
- currentDescription = trimmed.substring(1).trim();
262
- continue;
263
- }
264
- // Key=value line
265
- const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
266
- if (match) {
267
- let value = match[2];
268
- // Remove quotes if present
269
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
270
- value = value.slice(1, -1);
271
- }
272
- secrets.push({
273
- key: match[1],
274
- value,
275
- description: currentDescription || undefined,
276
- });
277
- currentDescription = '';
278
- }
279
- }
280
- // Import secrets
281
- let created = 0;
282
- let updated = 0;
283
- const errors = [];
284
- for (const secret of secrets) {
285
- try {
286
- // Check if secret already exists
287
- const { data: existing } = await this.supabase
288
- .from('secrets')
289
- .select('id')
290
- .eq('team_id', teamId)
291
- .eq('environment', environment)
292
- .eq('key', secret.key)
293
- .is('deleted_at', null)
294
- .single();
295
- if (existing) {
296
- // Update existing
297
- await this.updateSecret(existing.id, {
298
- value: secret.value,
299
- description: secret.description,
300
- updatedBy: createdBy,
301
- });
302
- updated++;
303
- }
304
- else {
305
- // Create new
306
- await this.createSecret({
307
- teamId,
308
- environment,
309
- key: secret.key,
310
- value: secret.value,
311
- description: secret.description,
312
- createdBy,
313
- });
314
- created++;
315
- }
316
- }
317
- catch (error) {
318
- errors.push(`${secret.key}: ${getErrorMessage(error)}`);
319
- }
320
- }
321
- return { created, updated, errors };
322
- }
323
- /**
324
- * Check secrets limit for tier
325
- */
326
- async checkSecretsLimit(teamId) {
327
- const team = await this.getTeamById(teamId);
328
- if (!team) {
329
- throw new Error('Team not found');
330
- }
331
- const org = await organizationService.getOrganizationById(team.organization_id);
332
- if (!org) {
333
- throw new Error('Organization not found');
334
- }
335
- const usage = await organizationService.getUsageSummary(team.organization_id);
336
- const { TIER_LIMITS } = await import('./saas-types.js');
337
- const limits = TIER_LIMITS[org.subscriptionTier];
338
- if (usage.secretCount >= limits.secrets) {
339
- throw new Error('TIER_LIMIT_EXCEEDED: Secret limit reached. Please upgrade your plan.');
340
- }
341
- }
342
- /**
343
- * Helper to get team record from database.
344
- *
345
- * Fetches raw team record from 'teams' table. Used internally to get
346
- * organization_id for audit logging and tier limit checks.
347
- *
348
- * @param teamId - UUID of team to fetch
349
- * @returns Raw Supabase team record or null if not found
350
- * @see DbTeamRecord in database-types.ts for return shape
351
- */
352
- async getTeamById(teamId) {
353
- const { data } = await this.supabase
354
- .from('teams')
355
- .select('*')
356
- .eq('id', teamId)
357
- .single();
358
- return data;
359
- }
360
- /**
361
- * Transform Supabase secret record to domain model.
362
- *
363
- * Maps database snake_case columns to TypeScript camelCase properties:
364
- * - `team_id` → `teamId`
365
- * - `encrypted_value` → `encryptedValue` (AES-256 encrypted)
366
- * - `encryption_key_id` → `encryptionKeyId` (FK to team's encryption key)
367
- * - `last_rotated_at` → `lastRotatedAt` (nullable Date)
368
- * - `rotation_interval_days` → `rotationIntervalDays` (nullable number)
369
- * - `created_by` → `createdBy` (FK to users.id)
370
- * - `updated_by` → `updatedBy` (FK to users.id)
371
- * - `deleted_by` → `deletedBy` (FK to users.id, for soft delete audit)
372
- *
373
- * Special handling:
374
- * - `tags`: Parses JSON string to string[] if stored as string, passes through if already array
375
- *
376
- * Note: The `encryptedValue` field contains the encrypted secret. Use
377
- * `encryptionService.decryptForTeam()` to decrypt it when needed.
378
- *
379
- * @param dbSecret - Supabase record from 'secrets' table
380
- * @returns Domain Secret object with parsed tags
381
- * @see DbSecretRecord in database-types.ts for input shape
382
- * @see Secret in saas-types.ts for output shape
383
- */
384
- mapDbSecretToSecret(dbSecret) {
385
- return {
386
- id: dbSecret.id,
387
- teamId: dbSecret.team_id,
388
- environment: dbSecret.environment,
389
- key: dbSecret.key,
390
- encryptedValue: dbSecret.encrypted_value,
391
- encryptionKeyId: dbSecret.encryption_key_id,
392
- description: dbSecret.description,
393
- tags: typeof dbSecret.tags === 'string' ? JSON.parse(dbSecret.tags) : (dbSecret.tags || []),
394
- lastRotatedAt: dbSecret.last_rotated_at ? new Date(dbSecret.last_rotated_at) : null,
395
- rotationIntervalDays: dbSecret.rotation_interval_days,
396
- createdAt: new Date(dbSecret.created_at),
397
- createdBy: dbSecret.created_by,
398
- updatedAt: new Date(dbSecret.updated_at),
399
- updatedBy: dbSecret.updated_by,
400
- deletedAt: dbSecret.deleted_at ? new Date(dbSecret.deleted_at) : null,
401
- deletedBy: dbSecret.deleted_by,
402
- };
403
- }
404
- }
405
- /**
406
- * Singleton instance
407
- */
408
- export const secretsService = new SecretsService();
@@ -1,165 +0,0 @@
1
- /**
2
- * LSH SaaS Platform TypeScript Type Definitions
3
- * Mirrors the database schema for multi-tenant support
4
- */
5
- export const TIER_LIMITS = {
6
- free: {
7
- organizations: 1,
8
- teamMembers: 3,
9
- secrets: 10,
10
- environments: 3,
11
- auditLogRetentionDays: 30,
12
- apiCallsPerMonth: 1000,
13
- ssoEnabled: false,
14
- prioritySupport: false,
15
- },
16
- pro: {
17
- organizations: 1,
18
- teamMembers: Infinity,
19
- secrets: Infinity,
20
- environments: Infinity,
21
- auditLogRetentionDays: 365,
22
- apiCallsPerMonth: 100000,
23
- ssoEnabled: false,
24
- prioritySupport: true,
25
- },
26
- enterprise: {
27
- organizations: Infinity,
28
- teamMembers: Infinity,
29
- secrets: Infinity,
30
- environments: Infinity,
31
- auditLogRetentionDays: Infinity,
32
- apiCallsPerMonth: Infinity,
33
- ssoEnabled: true,
34
- prioritySupport: true,
35
- },
36
- };
37
- export const ROLE_PERMISSIONS = {
38
- owner: {
39
- canManageBilling: true,
40
- canInviteMembers: true,
41
- canRemoveMembers: true,
42
- canCreateTeams: true,
43
- canDeleteTeams: true,
44
- canManageSecrets: true,
45
- canViewSecrets: true,
46
- canViewAuditLogs: true,
47
- canManageApiKeys: true,
48
- },
49
- admin: {
50
- canManageBilling: false,
51
- canInviteMembers: true,
52
- canRemoveMembers: true,
53
- canCreateTeams: true,
54
- canDeleteTeams: true,
55
- canManageSecrets: true,
56
- canViewSecrets: true,
57
- canViewAuditLogs: true,
58
- canManageApiKeys: true,
59
- },
60
- member: {
61
- canManageBilling: false,
62
- canInviteMembers: false,
63
- canRemoveMembers: false,
64
- canCreateTeams: false,
65
- canDeleteTeams: false,
66
- canManageSecrets: true,
67
- canViewSecrets: true,
68
- canViewAuditLogs: false,
69
- canManageApiKeys: true,
70
- },
71
- viewer: {
72
- canManageBilling: false,
73
- canInviteMembers: false,
74
- canRemoveMembers: false,
75
- canCreateTeams: false,
76
- canDeleteTeams: false,
77
- canManageSecrets: false,
78
- canViewSecrets: true,
79
- canViewAuditLogs: false,
80
- canManageApiKeys: false,
81
- },
82
- };
83
- // ============================================================================
84
- // ERROR CODES
85
- // ============================================================================
86
- export var ErrorCode;
87
- (function (ErrorCode) {
88
- // Auth
89
- ErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
90
- ErrorCode["INVALID_CREDENTIALS"] = "INVALID_CREDENTIALS";
91
- ErrorCode["EMAIL_NOT_VERIFIED"] = "EMAIL_NOT_VERIFIED";
92
- ErrorCode["EMAIL_ALREADY_EXISTS"] = "EMAIL_ALREADY_EXISTS";
93
- ErrorCode["INVALID_TOKEN"] = "INVALID_TOKEN";
94
- // Permissions
95
- ErrorCode["FORBIDDEN"] = "FORBIDDEN";
96
- ErrorCode["INSUFFICIENT_PERMISSIONS"] = "INSUFFICIENT_PERMISSIONS";
97
- // Resources
98
- ErrorCode["NOT_FOUND"] = "NOT_FOUND";
99
- ErrorCode["ALREADY_EXISTS"] = "ALREADY_EXISTS";
100
- ErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
101
- // Billing
102
- ErrorCode["TIER_LIMIT_EXCEEDED"] = "TIER_LIMIT_EXCEEDED";
103
- ErrorCode["SUBSCRIPTION_REQUIRED"] = "SUBSCRIPTION_REQUIRED";
104
- ErrorCode["PAYMENT_REQUIRED"] = "PAYMENT_REQUIRED";
105
- // General
106
- ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
107
- ErrorCode["SERVICE_UNAVAILABLE"] = "SERVICE_UNAVAILABLE";
108
- })(ErrorCode || (ErrorCode = {}));
109
- /**
110
- * Helper to safely extract error message
111
- */
112
- export function getErrorMessage(error) {
113
- if (error instanceof Error) {
114
- return error.message;
115
- }
116
- if (typeof error === 'string') {
117
- return error;
118
- }
119
- return 'Unknown error occurred';
120
- }
121
- /**
122
- * Helper to safely extract error for logging
123
- */
124
- export function getErrorDetails(error) {
125
- if (error instanceof Error) {
126
- return {
127
- message: error.message,
128
- stack: error.stack,
129
- code: error.code,
130
- };
131
- }
132
- return { message: String(error) };
133
- }
134
- /**
135
- * Helper to get authenticated user from request.
136
- * Use after authenticateUser middleware - throws if user not present.
137
- */
138
- export function getAuthenticatedUser(req) {
139
- if (!req.user) {
140
- throw new Error('User not authenticated');
141
- }
142
- return req.user;
143
- }
144
- /**
145
- * Create a standardized API error response
146
- */
147
- export function createErrorResponse(code, message, details) {
148
- return {
149
- success: false,
150
- error: {
151
- code,
152
- message,
153
- details,
154
- },
155
- };
156
- }
157
- /**
158
- * Create a standardized API success response
159
- */
160
- export function createSuccessResponse(data) {
161
- return {
162
- success: true,
163
- data,
164
- };
165
- }
@@ -1,125 +0,0 @@
1
- /**
2
- * Supabase Client Configuration
3
- * Provides database connectivity for LSH features
4
- */
5
- import { createClient } from '@supabase/supabase-js';
6
- import { ENV_VARS } from '../constants/index.js';
7
- export class SupabaseClient {
8
- client;
9
- config;
10
- constructor(config) {
11
- const url = config?.url || process.env[ENV_VARS.SUPABASE_URL];
12
- const anonKey = config?.anonKey || process.env[ENV_VARS.SUPABASE_ANON_KEY];
13
- const databaseUrl = config?.databaseUrl || process.env[ENV_VARS.DATABASE_URL];
14
- if (!url || !anonKey) {
15
- throw new Error('Supabase configuration missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.');
16
- }
17
- this.config = {
18
- url,
19
- anonKey,
20
- databaseUrl,
21
- };
22
- this.client = createClient(this.config.url, this.config.anonKey);
23
- }
24
- /**
25
- * Get the Supabase client instance
26
- */
27
- getClient() {
28
- return this.client;
29
- }
30
- /**
31
- * Test database connectivity
32
- */
33
- async testConnection() {
34
- try {
35
- const { error } = await this.client
36
- .from('shell_history')
37
- .select('count')
38
- .limit(1);
39
- return !error;
40
- }
41
- catch (error) {
42
- console.error('Supabase connection test failed:', error);
43
- return false;
44
- }
45
- }
46
- /**
47
- * Get database connection info
48
- */
49
- getConnectionInfo() {
50
- return {
51
- url: this.config.url,
52
- databaseUrl: this.config.databaseUrl,
53
- isConnected: !!this.client,
54
- };
55
- }
56
- }
57
- // Default client instance - lazily initialized to avoid errors at module load
58
- let _supabaseClient = null;
59
- let _clientInitializationFailed = false;
60
- function getDefaultClient() {
61
- if (_clientInitializationFailed) {
62
- return null;
63
- }
64
- if (!_supabaseClient) {
65
- try {
66
- _supabaseClient = new SupabaseClient();
67
- }
68
- catch (_error) {
69
- // Supabase not configured - will fall back to local storage
70
- _clientInitializationFailed = true;
71
- return null;
72
- }
73
- }
74
- return _supabaseClient;
75
- }
76
- /**
77
- * Check if Supabase is configured and available
78
- */
79
- export function isSupabaseConfigured() {
80
- return !!(process.env[ENV_VARS.SUPABASE_URL] && process.env[ENV_VARS.SUPABASE_ANON_KEY]);
81
- }
82
- export const supabaseClient = {
83
- getClient() {
84
- const client = getDefaultClient();
85
- if (!client) {
86
- throw new Error('Supabase client not initialized. Using local storage fallback.');
87
- }
88
- return client.getClient();
89
- },
90
- async testConnection() {
91
- const client = getDefaultClient();
92
- if (!client) {
93
- return false;
94
- }
95
- return client.testConnection();
96
- },
97
- getConnectionInfo() {
98
- const client = getDefaultClient();
99
- if (!client) {
100
- return {
101
- url: undefined,
102
- databaseUrl: undefined,
103
- isConnected: false,
104
- };
105
- }
106
- return client.getConnectionInfo();
107
- },
108
- isAvailable() {
109
- return getDefaultClient() !== null;
110
- }
111
- };
112
- /**
113
- * Get Supabase client for SaaS platform
114
- * Uses environment variables for configuration
115
- * @throws {Error} If SUPABASE_URL or SUPABASE_ANON_KEY are not set
116
- */
117
- export function getSupabaseClient() {
118
- const url = process.env[ENV_VARS.SUPABASE_URL];
119
- const key = process.env[ENV_VARS.SUPABASE_ANON_KEY];
120
- if (!url || !key) {
121
- throw new Error('Supabase configuration missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.');
122
- }
123
- return createClient(url, key);
124
- }
125
- export default SupabaseClient;