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.
- package/LICENSE +21 -0
- package/README.md +72 -34
- package/dist/commands/ipfs.js +7 -12
- package/dist/commands/self.js +22 -16
- package/dist/commands/sync.js +49 -38
- package/dist/constants/config.js +3 -0
- package/dist/lib/floating-point-arithmetic.js +2 -2
- package/dist/lib/ipfs-client-manager.js +51 -13
- package/dist/lib/ipfs-secrets-storage.js +21 -16
- package/dist/lib/ipfs-sync.js +88 -14
- package/dist/lib/secrets-manager.js +117 -47
- package/dist/lib/sync-key-store.js +87 -0
- package/dist/services/secrets/secrets.js +77 -39
- package/package.json +16 -16
- package/dist/__tests__/fixtures/job-fixtures.js +0 -204
- package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
- package/dist/daemon/job-registry.js +0 -556
- package/dist/daemon/lshd.js +0 -968
- package/dist/daemon/saas-api-routes.js +0 -599
- package/dist/daemon/saas-api-server.js +0 -231
- package/dist/examples/supabase-integration.js +0 -106
- package/dist/lib/api-response.js +0 -226
- package/dist/lib/base-command-registrar.js +0 -287
- package/dist/lib/base-job-manager.js +0 -295
- package/dist/lib/cloud-config-manager.js +0 -348
- package/dist/lib/cron-job-manager.js +0 -368
- package/dist/lib/daemon-client-helper.js +0 -145
- package/dist/lib/daemon-client.js +0 -513
- package/dist/lib/database-persistence.js +0 -727
- package/dist/lib/database-schema.js +0 -259
- package/dist/lib/database-types.js +0 -90
- package/dist/lib/enhanced-history-system.js +0 -247
- package/dist/lib/history-system.js +0 -246
- package/dist/lib/job-manager.js +0 -436
- package/dist/lib/job-storage-database.js +0 -164
- package/dist/lib/job-storage-memory.js +0 -73
- package/dist/lib/local-storage-adapter.js +0 -507
- package/dist/lib/optimized-job-scheduler.js +0 -356
- package/dist/lib/saas-audit.js +0 -215
- package/dist/lib/saas-auth.js +0 -465
- package/dist/lib/saas-billing.js +0 -503
- package/dist/lib/saas-email.js +0 -403
- package/dist/lib/saas-encryption.js +0 -221
- package/dist/lib/saas-organizations.js +0 -662
- package/dist/lib/saas-secrets.js +0 -408
- package/dist/lib/saas-types.js +0 -165
- package/dist/lib/supabase-client.js +0 -125
- package/dist/lib/supabase-utils.js +0 -396
- package/dist/services/cron/cron-registrar.js +0 -240
- package/dist/services/cron/cron.js +0 -9
- package/dist/services/daemon/daemon-registrar.js +0 -585
- package/dist/services/daemon/daemon.js +0 -9
- package/dist/services/supabase/supabase-registrar.js +0 -375
- package/dist/services/supabase/supabase.js +0 -9
package/dist/lib/saas-secrets.js
DELETED
|
@@ -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();
|
package/dist/lib/saas-types.js
DELETED
|
@@ -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;
|