lsh-framework 1.3.2 → 1.4.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.
- package/.env.example +43 -3
- package/README.md +25 -4
- package/dist/cli.js +6 -0
- package/dist/commands/config.js +240 -0
- package/dist/daemon/saas-api-routes.js +778 -0
- package/dist/daemon/saas-api-server.js +225 -0
- package/dist/lib/config-manager.js +321 -0
- package/dist/lib/database-persistence.js +75 -3
- package/dist/lib/env-validator.js +17 -0
- package/dist/lib/local-storage-adapter.js +493 -0
- package/dist/lib/saas-audit.js +213 -0
- package/dist/lib/saas-auth.js +427 -0
- package/dist/lib/saas-billing.js +402 -0
- package/dist/lib/saas-email.js +402 -0
- package/dist/lib/saas-encryption.js +220 -0
- package/dist/lib/saas-organizations.js +592 -0
- package/dist/lib/saas-secrets.js +378 -0
- package/dist/lib/saas-types.js +108 -0
- package/dist/lib/supabase-client.js +77 -11
- package/package.json +13 -2
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSH SaaS Secrets Management Service
|
|
3
|
+
* Multi-tenant secrets with per-team encryption
|
|
4
|
+
*/
|
|
5
|
+
import { getSupabaseClient } from './supabase-client.js';
|
|
6
|
+
import { encryptionService } from './saas-encryption.js';
|
|
7
|
+
import { auditLogger } from './saas-audit.js';
|
|
8
|
+
import { organizationService } from './saas-organizations.js';
|
|
9
|
+
/**
|
|
10
|
+
* Secrets Service
|
|
11
|
+
*/
|
|
12
|
+
export class SecretsService {
|
|
13
|
+
supabase = getSupabaseClient();
|
|
14
|
+
/**
|
|
15
|
+
* Create a new secret
|
|
16
|
+
*/
|
|
17
|
+
async createSecret(input) {
|
|
18
|
+
// Check tier limits
|
|
19
|
+
await this.checkSecretsLimit(input.teamId);
|
|
20
|
+
// Get or create encryption key for team
|
|
21
|
+
let encryptionKey = await encryptionService.getTeamKey(input.teamId);
|
|
22
|
+
if (!encryptionKey) {
|
|
23
|
+
// Auto-create encryption key for team
|
|
24
|
+
const team = await this.getTeamById(input.teamId);
|
|
25
|
+
if (!team) {
|
|
26
|
+
throw new Error('Team not found');
|
|
27
|
+
}
|
|
28
|
+
encryptionKey = await encryptionService.generateTeamKey(input.teamId, input.createdBy);
|
|
29
|
+
}
|
|
30
|
+
// Encrypt the secret value
|
|
31
|
+
const encryptedValue = await encryptionService.encryptForTeam(input.teamId, input.value);
|
|
32
|
+
// Store secret
|
|
33
|
+
const { data, error } = await this.supabase
|
|
34
|
+
.from('secrets')
|
|
35
|
+
.insert({
|
|
36
|
+
team_id: input.teamId,
|
|
37
|
+
environment: input.environment,
|
|
38
|
+
key: input.key,
|
|
39
|
+
encrypted_value: encryptedValue,
|
|
40
|
+
encryption_key_id: encryptionKey.id,
|
|
41
|
+
description: input.description || null,
|
|
42
|
+
tags: JSON.stringify(input.tags || []),
|
|
43
|
+
rotation_interval_days: input.rotationIntervalDays || null,
|
|
44
|
+
created_by: input.createdBy,
|
|
45
|
+
})
|
|
46
|
+
.select()
|
|
47
|
+
.single();
|
|
48
|
+
if (error) {
|
|
49
|
+
throw new Error(`Failed to create secret: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
// Audit log
|
|
52
|
+
const team = await this.getTeamById(input.teamId);
|
|
53
|
+
if (team) {
|
|
54
|
+
await auditLogger.log({
|
|
55
|
+
organizationId: team.organization_id,
|
|
56
|
+
teamId: input.teamId,
|
|
57
|
+
userId: input.createdBy,
|
|
58
|
+
action: 'secret.create',
|
|
59
|
+
resourceType: 'secret',
|
|
60
|
+
resourceId: data.id,
|
|
61
|
+
newValue: {
|
|
62
|
+
key: input.key,
|
|
63
|
+
environment: input.environment,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return this.mapDbSecretToSecret(data);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get secret by ID
|
|
71
|
+
*/
|
|
72
|
+
async getSecretById(id, decrypt = false) {
|
|
73
|
+
const { data, error } = await this.supabase
|
|
74
|
+
.from('secrets')
|
|
75
|
+
.select('*')
|
|
76
|
+
.eq('id', id)
|
|
77
|
+
.is('deleted_at', null)
|
|
78
|
+
.single();
|
|
79
|
+
if (error || !data) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const secret = this.mapDbSecretToSecret(data);
|
|
83
|
+
// Decrypt if requested
|
|
84
|
+
if (decrypt) {
|
|
85
|
+
const decryptedValue = await encryptionService.decryptForTeam(secret.teamId, secret.encryptedValue);
|
|
86
|
+
return { ...secret, encryptedValue: decryptedValue };
|
|
87
|
+
}
|
|
88
|
+
return secret;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get secrets for team/environment
|
|
92
|
+
*/
|
|
93
|
+
async getTeamSecrets(teamId, environment, decrypt = false) {
|
|
94
|
+
let query = this.supabase
|
|
95
|
+
.from('secrets')
|
|
96
|
+
.select('*')
|
|
97
|
+
.eq('team_id', teamId)
|
|
98
|
+
.is('deleted_at', null);
|
|
99
|
+
if (environment) {
|
|
100
|
+
query = query.eq('environment', environment);
|
|
101
|
+
}
|
|
102
|
+
query = query.order('key', { ascending: true });
|
|
103
|
+
const { data, error } = await query;
|
|
104
|
+
if (error) {
|
|
105
|
+
throw new Error(`Failed to get secrets: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
const secrets = (data || []).map(this.mapDbSecretToSecret);
|
|
108
|
+
// Decrypt if requested
|
|
109
|
+
if (decrypt) {
|
|
110
|
+
return Promise.all(secrets.map(async (secret) => {
|
|
111
|
+
try {
|
|
112
|
+
const decryptedValue = await encryptionService.decryptForTeam(teamId, secret.encryptedValue);
|
|
113
|
+
return { ...secret, encryptedValue: decryptedValue };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error(`Failed to decrypt secret ${secret.id}:`, error);
|
|
117
|
+
return secret;
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
return secrets;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Update secret
|
|
125
|
+
*/
|
|
126
|
+
async updateSecret(id, input) {
|
|
127
|
+
const secret = await this.getSecretById(id);
|
|
128
|
+
if (!secret) {
|
|
129
|
+
throw new Error('Secret not found');
|
|
130
|
+
}
|
|
131
|
+
const updateData = {
|
|
132
|
+
updated_by: input.updatedBy,
|
|
133
|
+
updated_at: new Date().toISOString(),
|
|
134
|
+
};
|
|
135
|
+
// Encrypt new value if provided
|
|
136
|
+
if (input.value) {
|
|
137
|
+
updateData.encrypted_value = await encryptionService.encryptForTeam(secret.teamId, input.value);
|
|
138
|
+
}
|
|
139
|
+
if (input.description !== undefined) {
|
|
140
|
+
updateData.description = input.description;
|
|
141
|
+
}
|
|
142
|
+
if (input.tags) {
|
|
143
|
+
updateData.tags = JSON.stringify(input.tags);
|
|
144
|
+
}
|
|
145
|
+
if (input.rotationIntervalDays !== undefined) {
|
|
146
|
+
updateData.rotation_interval_days = input.rotationIntervalDays;
|
|
147
|
+
}
|
|
148
|
+
const { data, error } = await this.supabase
|
|
149
|
+
.from('secrets')
|
|
150
|
+
.update(updateData)
|
|
151
|
+
.eq('id', id)
|
|
152
|
+
.select()
|
|
153
|
+
.single();
|
|
154
|
+
if (error) {
|
|
155
|
+
throw new Error(`Failed to update secret: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
// Audit log
|
|
158
|
+
const team = await this.getTeamById(secret.teamId);
|
|
159
|
+
if (team) {
|
|
160
|
+
await auditLogger.log({
|
|
161
|
+
organizationId: team.organization_id,
|
|
162
|
+
teamId: secret.teamId,
|
|
163
|
+
userId: input.updatedBy,
|
|
164
|
+
action: 'secret.update',
|
|
165
|
+
resourceType: 'secret',
|
|
166
|
+
resourceId: id,
|
|
167
|
+
oldValue: { description: secret.description },
|
|
168
|
+
newValue: { description: input.description },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return this.mapDbSecretToSecret(data);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Delete secret (soft delete)
|
|
175
|
+
*/
|
|
176
|
+
async deleteSecret(id, deletedBy) {
|
|
177
|
+
const secret = await this.getSecretById(id);
|
|
178
|
+
if (!secret) {
|
|
179
|
+
throw new Error('Secret not found');
|
|
180
|
+
}
|
|
181
|
+
const { error } = await this.supabase
|
|
182
|
+
.from('secrets')
|
|
183
|
+
.update({
|
|
184
|
+
deleted_at: new Date().toISOString(),
|
|
185
|
+
deleted_by: deletedBy,
|
|
186
|
+
})
|
|
187
|
+
.eq('id', id);
|
|
188
|
+
if (error) {
|
|
189
|
+
throw new Error(`Failed to delete secret: ${error.message}`);
|
|
190
|
+
}
|
|
191
|
+
// Audit log
|
|
192
|
+
const team = await this.getTeamById(secret.teamId);
|
|
193
|
+
if (team) {
|
|
194
|
+
await auditLogger.log({
|
|
195
|
+
organizationId: team.organization_id,
|
|
196
|
+
teamId: secret.teamId,
|
|
197
|
+
userId: deletedBy,
|
|
198
|
+
action: 'secret.delete',
|
|
199
|
+
resourceType: 'secret',
|
|
200
|
+
resourceId: id,
|
|
201
|
+
oldValue: {
|
|
202
|
+
key: secret.key,
|
|
203
|
+
environment: secret.environment,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get secrets summary by team
|
|
210
|
+
*/
|
|
211
|
+
async getSecretsSummary(teamId) {
|
|
212
|
+
const { data, error } = await this.supabase
|
|
213
|
+
.from('secrets_summary')
|
|
214
|
+
.select('*')
|
|
215
|
+
.eq('team_id', teamId);
|
|
216
|
+
if (error) {
|
|
217
|
+
throw new Error(`Failed to get secrets summary: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
return (data || []).map((row) => ({
|
|
220
|
+
teamId: row.team_id,
|
|
221
|
+
teamName: row.team_name,
|
|
222
|
+
environment: row.environment,
|
|
223
|
+
secretsCount: row.secrets_count || 0,
|
|
224
|
+
lastUpdated: row.last_updated ? new Date(row.last_updated) : null,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Export secrets to .env format
|
|
229
|
+
*/
|
|
230
|
+
async exportToEnv(teamId, environment) {
|
|
231
|
+
const secrets = await this.getTeamSecrets(teamId, environment, true);
|
|
232
|
+
const envLines = secrets.map((secret) => {
|
|
233
|
+
// Escape special characters in values (backslashes first, then quotes)
|
|
234
|
+
const value = secret.encryptedValue.includes(' ')
|
|
235
|
+
? `"${secret.encryptedValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
|
236
|
+
: secret.encryptedValue;
|
|
237
|
+
const comment = secret.description ? `# ${secret.description}\n` : '';
|
|
238
|
+
return `${comment}${secret.key}=${value}`;
|
|
239
|
+
});
|
|
240
|
+
return envLines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Import secrets from .env format
|
|
244
|
+
*/
|
|
245
|
+
async importFromEnv(teamId, environment, envContent, createdBy) {
|
|
246
|
+
const lines = envContent.split('\n');
|
|
247
|
+
const secrets = [];
|
|
248
|
+
let currentDescription = '';
|
|
249
|
+
// Parse .env file
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
const trimmed = line.trim();
|
|
252
|
+
// Skip empty lines
|
|
253
|
+
if (!trimmed) {
|
|
254
|
+
currentDescription = '';
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Comment line (description)
|
|
258
|
+
if (trimmed.startsWith('#')) {
|
|
259
|
+
currentDescription = trimmed.substring(1).trim();
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
// Key=value line
|
|
263
|
+
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
264
|
+
if (match) {
|
|
265
|
+
let value = match[2];
|
|
266
|
+
// Remove quotes if present
|
|
267
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
268
|
+
value = value.slice(1, -1);
|
|
269
|
+
}
|
|
270
|
+
secrets.push({
|
|
271
|
+
key: match[1],
|
|
272
|
+
value,
|
|
273
|
+
description: currentDescription || undefined,
|
|
274
|
+
});
|
|
275
|
+
currentDescription = '';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Import secrets
|
|
279
|
+
let created = 0;
|
|
280
|
+
let updated = 0;
|
|
281
|
+
const errors = [];
|
|
282
|
+
for (const secret of secrets) {
|
|
283
|
+
try {
|
|
284
|
+
// Check if secret already exists
|
|
285
|
+
const { data: existing } = await this.supabase
|
|
286
|
+
.from('secrets')
|
|
287
|
+
.select('id')
|
|
288
|
+
.eq('team_id', teamId)
|
|
289
|
+
.eq('environment', environment)
|
|
290
|
+
.eq('key', secret.key)
|
|
291
|
+
.is('deleted_at', null)
|
|
292
|
+
.single();
|
|
293
|
+
if (existing) {
|
|
294
|
+
// Update existing
|
|
295
|
+
await this.updateSecret(existing.id, {
|
|
296
|
+
value: secret.value,
|
|
297
|
+
description: secret.description,
|
|
298
|
+
updatedBy: createdBy,
|
|
299
|
+
});
|
|
300
|
+
updated++;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Create new
|
|
304
|
+
await this.createSecret({
|
|
305
|
+
teamId,
|
|
306
|
+
environment,
|
|
307
|
+
key: secret.key,
|
|
308
|
+
value: secret.value,
|
|
309
|
+
description: secret.description,
|
|
310
|
+
createdBy,
|
|
311
|
+
});
|
|
312
|
+
created++;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
errors.push(`${secret.key}: ${error.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { created, updated, errors };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check secrets limit for tier
|
|
323
|
+
*/
|
|
324
|
+
async checkSecretsLimit(teamId) {
|
|
325
|
+
const team = await this.getTeamById(teamId);
|
|
326
|
+
if (!team) {
|
|
327
|
+
throw new Error('Team not found');
|
|
328
|
+
}
|
|
329
|
+
const org = await organizationService.getOrganizationById(team.organization_id);
|
|
330
|
+
if (!org) {
|
|
331
|
+
throw new Error('Organization not found');
|
|
332
|
+
}
|
|
333
|
+
const usage = await organizationService.getUsageSummary(team.organization_id);
|
|
334
|
+
const { TIER_LIMITS } = await import('./saas-types.js');
|
|
335
|
+
const limits = TIER_LIMITS[org.subscriptionTier];
|
|
336
|
+
if (usage.secretCount >= limits.secrets) {
|
|
337
|
+
throw new Error('TIER_LIMIT_EXCEEDED: Secret limit reached. Please upgrade your plan.');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Helper to get team
|
|
342
|
+
*/
|
|
343
|
+
async getTeamById(teamId) {
|
|
344
|
+
const { data } = await this.supabase
|
|
345
|
+
.from('teams')
|
|
346
|
+
.select('*')
|
|
347
|
+
.eq('id', teamId)
|
|
348
|
+
.single();
|
|
349
|
+
return data;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Map database secret to Secret type
|
|
353
|
+
*/
|
|
354
|
+
mapDbSecretToSecret(dbSecret) {
|
|
355
|
+
return {
|
|
356
|
+
id: dbSecret.id,
|
|
357
|
+
teamId: dbSecret.team_id,
|
|
358
|
+
environment: dbSecret.environment,
|
|
359
|
+
key: dbSecret.key,
|
|
360
|
+
encryptedValue: dbSecret.encrypted_value,
|
|
361
|
+
encryptionKeyId: dbSecret.encryption_key_id,
|
|
362
|
+
description: dbSecret.description,
|
|
363
|
+
tags: typeof dbSecret.tags === 'string' ? JSON.parse(dbSecret.tags) : (dbSecret.tags || []),
|
|
364
|
+
lastRotatedAt: dbSecret.last_rotated_at ? new Date(dbSecret.last_rotated_at) : null,
|
|
365
|
+
rotationIntervalDays: dbSecret.rotation_interval_days,
|
|
366
|
+
createdAt: new Date(dbSecret.created_at),
|
|
367
|
+
createdBy: dbSecret.created_by,
|
|
368
|
+
updatedAt: new Date(dbSecret.updated_at),
|
|
369
|
+
updatedBy: dbSecret.updated_by,
|
|
370
|
+
deletedAt: dbSecret.deleted_at ? new Date(dbSecret.deleted_at) : null,
|
|
371
|
+
deletedBy: dbSecret.deleted_by,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Singleton instance
|
|
377
|
+
*/
|
|
378
|
+
export const secretsService = new SecretsService();
|
|
@@ -0,0 +1,108 @@
|
|
|
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 = {}));
|
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
* Provides database connectivity for LSH features
|
|
4
4
|
*/
|
|
5
5
|
import { createClient } from '@supabase/supabase-js';
|
|
6
|
-
// Supabase configuration
|
|
7
|
-
const SUPABASE_URL = 'https://uljsqvwkomdrlnofmlad.supabase.co';
|
|
8
|
-
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVsanNxdndrb21kcmxub2ZtbGFkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY4MDIyNDQsImV4cCI6MjA3MjM3ODI0NH0.QCpfcEpxGX_5Wn8ljf_J2KWjJLGdF8zRsV_7OatxmHI';
|
|
9
|
-
// Database connection string (for direct PostgreSQL access if needed)
|
|
10
|
-
const DATABASE_URL = 'postgresql://postgres:[YOUR-PASSWORD]@db.uljsqvwkomdrlnofmlad.supabase.co:5432/postgres';
|
|
11
6
|
export class SupabaseClient {
|
|
12
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
8
|
client;
|
|
14
9
|
config;
|
|
15
10
|
constructor(config) {
|
|
11
|
+
const url = config?.url || process.env.SUPABASE_URL;
|
|
12
|
+
const anonKey = config?.anonKey || process.env.SUPABASE_ANON_KEY;
|
|
13
|
+
const databaseUrl = config?.databaseUrl || process.env.DATABASE_URL;
|
|
14
|
+
if (!url || !anonKey) {
|
|
15
|
+
throw new Error('Supabase configuration missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.');
|
|
16
|
+
}
|
|
16
17
|
this.config = {
|
|
17
|
-
url
|
|
18
|
-
anonKey
|
|
19
|
-
databaseUrl
|
|
20
|
-
...config,
|
|
18
|
+
url,
|
|
19
|
+
anonKey,
|
|
20
|
+
databaseUrl,
|
|
21
21
|
};
|
|
22
22
|
this.client = createClient(this.config.url, this.config.anonKey);
|
|
23
23
|
}
|
|
@@ -54,6 +54,72 @@ export class SupabaseClient {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
// Default client instance
|
|
58
|
-
|
|
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.SUPABASE_URL && process.env.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.SUPABASE_URL;
|
|
119
|
+
const key = process.env.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
|
+
}
|
|
59
125
|
export default SupabaseClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Simple, cross-platform encrypted secrets manager with automatic sync and multi-environment support. Just run lsh sync and start managing your secrets.",
|
|
5
5
|
"main": "dist/app.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"clean": "rm -rf ./build; rm -rf ./bin; rm -rf ./dist",
|
|
23
23
|
"lint": "eslint src --ext .js,.ts,.tsx",
|
|
24
24
|
"lint:fix": "eslint src --ext .js,.ts,.tsx --fix",
|
|
25
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"saas:api": "node dist/daemon/saas-api-server.js",
|
|
27
|
+
"saas:dev": "tsc && node dist/daemon/saas-api-server.js"
|
|
26
28
|
},
|
|
27
29
|
"keywords": [
|
|
28
30
|
"secrets-manager",
|
|
@@ -58,13 +60,18 @@
|
|
|
58
60
|
],
|
|
59
61
|
"dependencies": {
|
|
60
62
|
"@supabase/supabase-js": "^2.57.4",
|
|
63
|
+
"bcrypt": "^5.1.1",
|
|
61
64
|
"chalk": "^5.3.0",
|
|
62
65
|
"chokidar": "^3.6.0",
|
|
63
66
|
"commander": "^10.0.1",
|
|
67
|
+
"cors": "^2.8.5",
|
|
64
68
|
"dotenv": "^16.4.5",
|
|
69
|
+
"express": "^4.18.2",
|
|
70
|
+
"express-rate-limit": "^7.5.1",
|
|
65
71
|
"glob": "^10.3.12",
|
|
66
72
|
"inquirer": "^9.2.12",
|
|
67
73
|
"js-yaml": "^4.1.0",
|
|
74
|
+
"jsonwebtoken": "^9.0.2",
|
|
68
75
|
"node-cron": "^3.0.3",
|
|
69
76
|
"ora": "^8.0.1",
|
|
70
77
|
"pg": "^8.16.3",
|
|
@@ -72,8 +79,12 @@
|
|
|
72
79
|
"uuid": "^10.0.0"
|
|
73
80
|
},
|
|
74
81
|
"devDependencies": {
|
|
82
|
+
"@types/bcrypt": "^5.0.2",
|
|
83
|
+
"@types/cors": "^2.8.17",
|
|
84
|
+
"@types/express": "^4.17.21",
|
|
75
85
|
"@types/jest": "^30.0.0",
|
|
76
86
|
"@types/js-yaml": "^4.0.9",
|
|
87
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
77
88
|
"@types/node": "^20.12.7",
|
|
78
89
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
|
79
90
|
"@typescript-eslint/parser": "^8.44.1",
|