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.
@@ -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: SUPABASE_URL,
18
- anonKey: SUPABASE_ANON_KEY,
19
- databaseUrl: DATABASE_URL,
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
- export const supabaseClient = new SupabaseClient();
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.2",
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",