lsh-framework 2.3.2 ā 3.1.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.
- package/README.md +200 -832
- package/dist/cli.js +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/init.js +2 -2
- package/dist/commands/self.js +0 -1
- package/dist/constants/validation.js +2 -0
- package/dist/daemon/lshd.js +21 -4
- package/dist/daemon/saas-api-routes.js +44 -37
- package/dist/daemon/saas-api-server.js +9 -5
- package/dist/lib/cron-job-manager.js +2 -0
- package/dist/lib/ipfs-secrets-storage.js +26 -7
- package/dist/lib/job-manager.js +0 -1
- package/dist/lib/lshrc-init.js +0 -1
- package/dist/lib/saas-audit.js +6 -3
- package/dist/lib/saas-auth.js +6 -3
- package/dist/lib/saas-billing.js +10 -2
- package/dist/lib/saas-encryption.js +2 -1
- package/dist/lib/saas-organizations.js +5 -0
- package/dist/lib/saas-secrets.js +4 -1
- package/dist/lib/saas-types.js +57 -0
- package/dist/lib/secrets-manager.js +63 -6
- package/dist/lib/supabase-client.js +1 -2
- package/dist/services/secrets/secrets.js +59 -23
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -47,7 +47,7 @@ program
|
|
|
47
47
|
program
|
|
48
48
|
.option('-v, --verbose', 'Verbose output')
|
|
49
49
|
.option('-d, --debug', 'Debug mode')
|
|
50
|
-
.action(async (
|
|
50
|
+
.action(async (_options) => {
|
|
51
51
|
// No arguments - show secrets-focused help
|
|
52
52
|
console.log('LSH - Encrypted Secrets Manager');
|
|
53
53
|
console.log('');
|
package/dist/commands/doctor.js
CHANGED
|
@@ -263,7 +263,7 @@ async function testSupabaseConnection(url, key, verbose) {
|
|
|
263
263
|
/**
|
|
264
264
|
* Check if in git repository
|
|
265
265
|
*/
|
|
266
|
-
async function checkGitRepository(
|
|
266
|
+
async function checkGitRepository(_verbose) {
|
|
267
267
|
try {
|
|
268
268
|
const gitPath = path.join(process.cwd(), '.git');
|
|
269
269
|
// Use stat instead of access to avoid TOCTOU race condition
|
package/dist/commands/init.js
CHANGED
|
@@ -204,7 +204,7 @@ async function checkExistingConfig() {
|
|
|
204
204
|
/**
|
|
205
205
|
* Pull secrets after init is complete
|
|
206
206
|
*/
|
|
207
|
-
async function pullSecretsAfterInit(
|
|
207
|
+
async function pullSecretsAfterInit(_encryptionKey, _repoName) {
|
|
208
208
|
const spinner = ora('Pulling secrets from cloud...').start();
|
|
209
209
|
try {
|
|
210
210
|
// Dynamically import SecretsManager to avoid circular dependencies
|
|
@@ -399,7 +399,7 @@ async function configurePostgres(config, skipTest) {
|
|
|
399
399
|
/**
|
|
400
400
|
* Configure local-only mode
|
|
401
401
|
*/
|
|
402
|
-
async function configureLocal(
|
|
402
|
+
async function configureLocal(_config) {
|
|
403
403
|
console.log(chalk.cyan('\nš¾ Local Encryption Mode'));
|
|
404
404
|
console.log(chalk.gray('Secrets will be encrypted locally. No cloud sync available.'));
|
|
405
405
|
console.log('');
|
package/dist/commands/self.js
CHANGED
|
@@ -127,7 +127,6 @@ async function checkCIStatus(_version) {
|
|
|
127
127
|
const ghData = JSON.parse(data);
|
|
128
128
|
const runs = ghData.workflow_runs || [];
|
|
129
129
|
// Find the most recent workflow run for main branch
|
|
130
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
130
|
const mainRuns = runs.filter((run) => run.head_branch === 'main' && run.status === 'completed');
|
|
132
131
|
if (mainRuns.length > 0) {
|
|
133
132
|
const latestRun = mainRuns[0];
|
|
@@ -87,6 +87,8 @@ export const DANGEROUS_PATTERNS = [
|
|
|
87
87
|
riskLevel: RISK_LEVELS.MEDIUM,
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
|
+
// Detect null byte injection attacks (legitimate security pattern)
|
|
91
|
+
// eslint-disable-next-line no-control-regex
|
|
90
92
|
pattern: /\x00/i,
|
|
91
93
|
description: ERRORS.NULL_BYTE,
|
|
92
94
|
riskLevel: RISK_LEVELS.HIGH,
|
package/dist/daemon/lshd.js
CHANGED
|
@@ -108,13 +108,15 @@ export class LSHJobDaemon extends EventEmitter {
|
|
|
108
108
|
await fs.promises.unlink(this.config.pidFile);
|
|
109
109
|
}
|
|
110
110
|
catch (_error) {
|
|
111
|
-
// Ignore if file doesn
|
|
111
|
+
// Ignore if file doesn't exist
|
|
112
112
|
}
|
|
113
|
-
//
|
|
113
|
+
// Log before closing stream
|
|
114
|
+
this.log('INFO', 'Daemon stopped');
|
|
115
|
+
// Close log stream AFTER logging
|
|
114
116
|
if (this.logStream) {
|
|
115
117
|
this.logStream.end();
|
|
118
|
+
this.logStream = undefined;
|
|
116
119
|
}
|
|
117
|
-
this.log('INFO', 'Daemon stopped');
|
|
118
120
|
this.emit('stopped');
|
|
119
121
|
}
|
|
120
122
|
/**
|
|
@@ -729,8 +731,23 @@ export class LSHJobDaemon extends EventEmitter {
|
|
|
729
731
|
}
|
|
730
732
|
// Module-level logger for CLI operations
|
|
731
733
|
const cliLogger = createLogger('LSHDaemonCLI');
|
|
734
|
+
// Helper to check if this module is run directly (ESM-compatible)
|
|
735
|
+
// Uses indirect eval to avoid parse-time errors in CommonJS/Jest environments
|
|
736
|
+
const isMainModule = () => {
|
|
737
|
+
try {
|
|
738
|
+
// Use Function constructor to avoid parse-time errors with import.meta
|
|
739
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
740
|
+
const getImportMetaUrl = new Function('return import.meta.url');
|
|
741
|
+
const metaUrl = getImportMetaUrl();
|
|
742
|
+
return metaUrl === `file://${process.argv[1]}`;
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
// Fallback for CommonJS or environments that don't support import.meta
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
};
|
|
732
749
|
// CLI interface for the daemon
|
|
733
|
-
if (
|
|
750
|
+
if (isMainModule()) {
|
|
734
751
|
const command = process.argv[2];
|
|
735
752
|
const subCommand = process.argv[3];
|
|
736
753
|
const _args = process.argv.slice(4);
|
|
@@ -9,6 +9,7 @@ import { billingService } from '../lib/saas-billing.js';
|
|
|
9
9
|
import { auditLogger, getIpFromRequest } from '../lib/saas-audit.js';
|
|
10
10
|
import { emailService } from '../lib/saas-email.js';
|
|
11
11
|
import { secretsService } from '../lib/saas-secrets.js';
|
|
12
|
+
import { getErrorMessage, getAuthenticatedUser } from '../lib/saas-types.js'; // eslint-disable-line no-duplicate-imports
|
|
12
13
|
/**
|
|
13
14
|
* Rate Limiters for different endpoint types
|
|
14
15
|
*/
|
|
@@ -83,7 +84,7 @@ export async function authenticateUser(req, res, next) {
|
|
|
83
84
|
success: false,
|
|
84
85
|
error: {
|
|
85
86
|
code: 'UNAUTHORIZED',
|
|
86
|
-
message: error
|
|
87
|
+
message: getErrorMessage(error) || 'Authentication failed',
|
|
87
88
|
},
|
|
88
89
|
});
|
|
89
90
|
}
|
|
@@ -101,20 +102,26 @@ export async function requireOrganizationMembership(req, res, next) {
|
|
|
101
102
|
error: { code: 'INVALID_INPUT', message: 'Organization ID required' },
|
|
102
103
|
});
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
+
if (!req.user) {
|
|
106
|
+
return res.status(401).json({
|
|
107
|
+
success: false,
|
|
108
|
+
error: { code: 'UNAUTHORIZED', message: 'User not authenticated' },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const role = await organizationService.getUserRole(organizationId, getAuthenticatedUser(req).id);
|
|
105
112
|
if (!role) {
|
|
106
113
|
return res.status(403).json({
|
|
107
114
|
success: false,
|
|
108
115
|
error: { code: 'FORBIDDEN', message: 'Not a member of this organization' },
|
|
109
116
|
});
|
|
110
117
|
}
|
|
111
|
-
req.
|
|
118
|
+
req.organizationId = organizationId;
|
|
112
119
|
next();
|
|
113
120
|
}
|
|
114
121
|
catch (error) {
|
|
115
122
|
return res.status(500).json({
|
|
116
123
|
success: false,
|
|
117
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
124
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
118
125
|
});
|
|
119
126
|
}
|
|
120
127
|
}
|
|
@@ -183,7 +190,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
183
190
|
catch (error) {
|
|
184
191
|
res.status(400).json({
|
|
185
192
|
success: false,
|
|
186
|
-
error: { code: error.
|
|
193
|
+
error: { code: getErrorMessage(error).includes('ALREADY_EXISTS') ? 'EMAIL_ALREADY_EXISTS' : 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
187
194
|
});
|
|
188
195
|
}
|
|
189
196
|
});
|
|
@@ -215,10 +222,10 @@ export function setupSaaSApiRoutes(app) {
|
|
|
215
222
|
});
|
|
216
223
|
}
|
|
217
224
|
catch (error) {
|
|
218
|
-
const statusCode = error.
|
|
225
|
+
const statusCode = getErrorMessage(error).includes('EMAIL_NOT_VERIFIED') ? 403 : 401;
|
|
219
226
|
res.status(statusCode).json({
|
|
220
227
|
success: false,
|
|
221
|
-
error: { code: error
|
|
228
|
+
error: { code: getErrorMessage(error) || 'INVALID_CREDENTIALS', message: getErrorMessage(error) },
|
|
222
229
|
});
|
|
223
230
|
}
|
|
224
231
|
});
|
|
@@ -252,7 +259,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
252
259
|
catch (error) {
|
|
253
260
|
res.status(400).json({
|
|
254
261
|
success: false,
|
|
255
|
-
error: { code: 'INVALID_TOKEN', message: error
|
|
262
|
+
error: { code: 'INVALID_TOKEN', message: getErrorMessage(error) },
|
|
256
263
|
});
|
|
257
264
|
}
|
|
258
265
|
});
|
|
@@ -276,7 +283,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
276
283
|
catch (error) {
|
|
277
284
|
res.status(400).json({
|
|
278
285
|
success: false,
|
|
279
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
286
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
280
287
|
});
|
|
281
288
|
}
|
|
282
289
|
});
|
|
@@ -302,7 +309,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
302
309
|
catch (error) {
|
|
303
310
|
res.status(401).json({
|
|
304
311
|
success: false,
|
|
305
|
-
error: { code: 'INVALID_TOKEN', message: error
|
|
312
|
+
error: { code: 'INVALID_TOKEN', message: getErrorMessage(error) },
|
|
306
313
|
});
|
|
307
314
|
}
|
|
308
315
|
});
|
|
@@ -335,7 +342,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
335
342
|
const organization = await organizationService.createOrganization({
|
|
336
343
|
name,
|
|
337
344
|
slug,
|
|
338
|
-
ownerId: req.
|
|
345
|
+
ownerId: getAuthenticatedUser(req).id,
|
|
339
346
|
});
|
|
340
347
|
res.json({
|
|
341
348
|
success: true,
|
|
@@ -345,7 +352,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
345
352
|
catch (error) {
|
|
346
353
|
res.status(400).json({
|
|
347
354
|
success: false,
|
|
348
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
355
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
349
356
|
});
|
|
350
357
|
}
|
|
351
358
|
});
|
|
@@ -372,7 +379,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
372
379
|
catch (error) {
|
|
373
380
|
res.status(500).json({
|
|
374
381
|
success: false,
|
|
375
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
382
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
376
383
|
});
|
|
377
384
|
}
|
|
378
385
|
});
|
|
@@ -391,7 +398,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
391
398
|
catch (error) {
|
|
392
399
|
res.status(500).json({
|
|
393
400
|
success: false,
|
|
394
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
401
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
395
402
|
});
|
|
396
403
|
}
|
|
397
404
|
});
|
|
@@ -402,7 +409,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
402
409
|
app.post('/api/v1/organizations/:organizationId/members', writeLimiter, authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
403
410
|
try {
|
|
404
411
|
// Check permission
|
|
405
|
-
const canInvite = await organizationService.hasPermission(req.params.organizationId, req.
|
|
412
|
+
const canInvite = await organizationService.hasPermission(req.params.organizationId, getAuthenticatedUser(req).id, 'canInviteMembers');
|
|
406
413
|
if (!canInvite) {
|
|
407
414
|
return res.status(403).json({
|
|
408
415
|
success: false,
|
|
@@ -414,7 +421,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
414
421
|
organizationId: req.params.organizationId,
|
|
415
422
|
userId,
|
|
416
423
|
role: role || 'member',
|
|
417
|
-
invitedBy: req.
|
|
424
|
+
invitedBy: getAuthenticatedUser(req).id,
|
|
418
425
|
});
|
|
419
426
|
res.json({
|
|
420
427
|
success: true,
|
|
@@ -424,7 +431,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
424
431
|
catch (error) {
|
|
425
432
|
res.status(400).json({
|
|
426
433
|
success: false,
|
|
427
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
434
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
428
435
|
});
|
|
429
436
|
}
|
|
430
437
|
});
|
|
@@ -449,7 +456,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
449
456
|
name,
|
|
450
457
|
slug,
|
|
451
458
|
description,
|
|
452
|
-
}, req.
|
|
459
|
+
}, getAuthenticatedUser(req).id);
|
|
453
460
|
res.json({
|
|
454
461
|
success: true,
|
|
455
462
|
data: { team },
|
|
@@ -458,7 +465,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
458
465
|
catch (error) {
|
|
459
466
|
res.status(400).json({
|
|
460
467
|
success: false,
|
|
461
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
468
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
462
469
|
});
|
|
463
470
|
}
|
|
464
471
|
});
|
|
@@ -477,7 +484,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
477
484
|
catch (error) {
|
|
478
485
|
res.status(500).json({
|
|
479
486
|
success: false,
|
|
480
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
487
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
481
488
|
});
|
|
482
489
|
}
|
|
483
490
|
});
|
|
@@ -505,7 +512,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
505
512
|
description,
|
|
506
513
|
tags,
|
|
507
514
|
rotationIntervalDays,
|
|
508
|
-
createdBy: req.
|
|
515
|
+
createdBy: getAuthenticatedUser(req).id,
|
|
509
516
|
});
|
|
510
517
|
res.json({
|
|
511
518
|
success: true,
|
|
@@ -513,10 +520,10 @@ export function setupSaaSApiRoutes(app) {
|
|
|
513
520
|
});
|
|
514
521
|
}
|
|
515
522
|
catch (error) {
|
|
516
|
-
const statusCode = error.
|
|
523
|
+
const statusCode = getErrorMessage(error).includes('TIER_LIMIT_EXCEEDED') ? 402 : 400;
|
|
517
524
|
res.status(statusCode).json({
|
|
518
525
|
success: false,
|
|
519
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
526
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
520
527
|
});
|
|
521
528
|
}
|
|
522
529
|
});
|
|
@@ -541,7 +548,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
541
548
|
catch (error) {
|
|
542
549
|
res.status(500).json({
|
|
543
550
|
success: false,
|
|
544
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
551
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
545
552
|
});
|
|
546
553
|
}
|
|
547
554
|
});
|
|
@@ -574,7 +581,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
574
581
|
catch (error) {
|
|
575
582
|
res.status(500).json({
|
|
576
583
|
success: false,
|
|
577
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
584
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
578
585
|
});
|
|
579
586
|
}
|
|
580
587
|
});
|
|
@@ -590,7 +597,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
590
597
|
description,
|
|
591
598
|
tags,
|
|
592
599
|
rotationIntervalDays,
|
|
593
|
-
updatedBy: req.
|
|
600
|
+
updatedBy: getAuthenticatedUser(req).id,
|
|
594
601
|
});
|
|
595
602
|
res.json({
|
|
596
603
|
success: true,
|
|
@@ -600,7 +607,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
600
607
|
catch (error) {
|
|
601
608
|
res.status(400).json({
|
|
602
609
|
success: false,
|
|
603
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
610
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
604
611
|
});
|
|
605
612
|
}
|
|
606
613
|
});
|
|
@@ -610,7 +617,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
610
617
|
*/
|
|
611
618
|
app.delete('/api/v1/teams/:teamId/secrets/:secretId', writeLimiter, authenticateUser, async (req, res) => {
|
|
612
619
|
try {
|
|
613
|
-
await secretsService.deleteSecret(req.params.secretId, req.
|
|
620
|
+
await secretsService.deleteSecret(req.params.secretId, getAuthenticatedUser(req).id);
|
|
614
621
|
res.json({
|
|
615
622
|
success: true,
|
|
616
623
|
data: { message: 'Secret deleted successfully' },
|
|
@@ -619,7 +626,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
619
626
|
catch (error) {
|
|
620
627
|
res.status(400).json({
|
|
621
628
|
success: false,
|
|
622
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
629
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
623
630
|
});
|
|
624
631
|
}
|
|
625
632
|
});
|
|
@@ -644,7 +651,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
644
651
|
catch (error) {
|
|
645
652
|
res.status(500).json({
|
|
646
653
|
success: false,
|
|
647
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
654
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
648
655
|
});
|
|
649
656
|
}
|
|
650
657
|
});
|
|
@@ -661,7 +668,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
661
668
|
error: { code: 'INVALID_INPUT', message: 'Environment and content required' },
|
|
662
669
|
});
|
|
663
670
|
}
|
|
664
|
-
const result = await secretsService.importFromEnv(req.params.teamId, environment, content, req.
|
|
671
|
+
const result = await secretsService.importFromEnv(req.params.teamId, environment, content, getAuthenticatedUser(req).id);
|
|
665
672
|
res.json({
|
|
666
673
|
success: true,
|
|
667
674
|
data: result,
|
|
@@ -670,7 +677,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
670
677
|
catch (error) {
|
|
671
678
|
res.status(400).json({
|
|
672
679
|
success: false,
|
|
673
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
680
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
674
681
|
});
|
|
675
682
|
}
|
|
676
683
|
});
|
|
@@ -684,7 +691,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
684
691
|
app.get('/api/v1/organizations/:organizationId/audit-logs', authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
685
692
|
try {
|
|
686
693
|
// Check permission
|
|
687
|
-
const canView = await organizationService.hasPermission(req.params.organizationId, req.
|
|
694
|
+
const canView = await organizationService.hasPermission(req.params.organizationId, getAuthenticatedUser(req).id, 'canViewAuditLogs');
|
|
688
695
|
if (!canView) {
|
|
689
696
|
return res.status(403).json({
|
|
690
697
|
success: false,
|
|
@@ -709,7 +716,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
709
716
|
catch (error) {
|
|
710
717
|
res.status(500).json({
|
|
711
718
|
success: false,
|
|
712
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
719
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
713
720
|
});
|
|
714
721
|
}
|
|
715
722
|
});
|
|
@@ -723,7 +730,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
723
730
|
app.post('/api/v1/organizations/:organizationId/billing/checkout', writeLimiter, authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
724
731
|
try {
|
|
725
732
|
// Check permission
|
|
726
|
-
const canManage = await organizationService.hasPermission(req.params.organizationId, req.
|
|
733
|
+
const canManage = await organizationService.hasPermission(req.params.organizationId, getAuthenticatedUser(req).id, 'canManageBilling');
|
|
727
734
|
if (!canManage) {
|
|
728
735
|
return res.status(403).json({
|
|
729
736
|
success: false,
|
|
@@ -754,7 +761,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
754
761
|
catch (error) {
|
|
755
762
|
res.status(400).json({
|
|
756
763
|
success: false,
|
|
757
|
-
error: { code: 'INTERNAL_ERROR', message: error
|
|
764
|
+
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(error) },
|
|
758
765
|
});
|
|
759
766
|
}
|
|
760
767
|
});
|
|
@@ -771,7 +778,7 @@ export function setupSaaSApiRoutes(app) {
|
|
|
771
778
|
}
|
|
772
779
|
catch (error) {
|
|
773
780
|
console.error('Webhook error:', error);
|
|
774
|
-
res.status(400).json({ error: error
|
|
781
|
+
res.status(400).json({ error: getErrorMessage(error) });
|
|
775
782
|
}
|
|
776
783
|
});
|
|
777
784
|
console.log('ā
SaaS API routes registered');
|
|
@@ -6,6 +6,7 @@ import express from 'express';
|
|
|
6
6
|
import cors from 'cors';
|
|
7
7
|
import rateLimit from 'express-rate-limit';
|
|
8
8
|
import { setupSaaSApiRoutes } from './saas-api-routes.js';
|
|
9
|
+
import { getErrorMessage } from '../lib/saas-types.js';
|
|
9
10
|
/**
|
|
10
11
|
* SaaS API Server
|
|
11
12
|
*/
|
|
@@ -123,19 +124,22 @@ export class SaaSApiServer {
|
|
|
123
124
|
*/
|
|
124
125
|
setupErrorHandlers() {
|
|
125
126
|
// Global error handler
|
|
126
|
-
|
|
127
|
+
const errorHandler = (err, _req, res, _next) => {
|
|
127
128
|
console.error('API Error:', err);
|
|
128
129
|
// Don't leak error details in production
|
|
129
130
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
130
|
-
|
|
131
|
+
const statusCode = err.status || 500;
|
|
132
|
+
const errorCode = err.code || 'INTERNAL_ERROR';
|
|
133
|
+
res.status(statusCode).json({
|
|
131
134
|
success: false,
|
|
132
135
|
error: {
|
|
133
|
-
code:
|
|
134
|
-
message: isDev ? err
|
|
136
|
+
code: errorCode,
|
|
137
|
+
message: isDev ? getErrorMessage(err) : 'Internal server error',
|
|
135
138
|
details: isDev ? err.stack : undefined,
|
|
136
139
|
},
|
|
137
140
|
});
|
|
138
|
-
}
|
|
141
|
+
};
|
|
142
|
+
this.app.use(errorHandler);
|
|
139
143
|
}
|
|
140
144
|
/**
|
|
141
145
|
* Start the server
|
|
@@ -160,6 +160,8 @@ export class CronJobManager extends BaseJobManager {
|
|
|
160
160
|
*/
|
|
161
161
|
async getJobReport(jobId) {
|
|
162
162
|
// Try to get historical data from database if available, otherwise use current job info
|
|
163
|
+
// Using any[] because getJobHistory returns ShellJob (snake_case properties)
|
|
164
|
+
// but getJob returns JobSpec (camelCase properties), and this code handles both
|
|
163
165
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
166
|
let jobs = [];
|
|
165
167
|
try {
|
|
@@ -269,13 +269,32 @@ export class IPFSSecretsStorage {
|
|
|
269
269
|
* Decrypt secrets using AES-256
|
|
270
270
|
*/
|
|
271
271
|
decryptSecrets(encryptedData, encryptionKey) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
272
|
+
try {
|
|
273
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
274
|
+
const key = crypto.createHash('sha256').update(encryptionKey).digest();
|
|
275
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
276
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
277
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
278
|
+
decrypted += decipher.final('utf8');
|
|
279
|
+
return JSON.parse(decrypted);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const err = error;
|
|
283
|
+
// Catch crypto errors (bad decrypt, wrong block length) AND JSON parse errors
|
|
284
|
+
// (wrong key can produce garbage that fails JSON.parse)
|
|
285
|
+
if (err.message.includes('bad decrypt') ||
|
|
286
|
+
err.message.includes('wrong final block length') ||
|
|
287
|
+
err.message.includes('Unexpected token') ||
|
|
288
|
+
err.message.includes('JSON')) {
|
|
289
|
+
throw new Error('Decryption failed. This usually means:\n' +
|
|
290
|
+
' 1. You need to set LSH_SECRETS_KEY environment variable\n' +
|
|
291
|
+
' 2. The key must match the one used during encryption\n' +
|
|
292
|
+
' 3. Generate a shared key with: lsh secrets key\n' +
|
|
293
|
+
' 4. Add it to your .env: LSH_SECRETS_KEY=<key>\n' +
|
|
294
|
+
'\nOriginal error: ' + err.message);
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
279
298
|
}
|
|
280
299
|
/**
|
|
281
300
|
* Generate IPFS-compatible CID from content
|
package/dist/lib/job-manager.js
CHANGED
package/dist/lib/lshrc-init.js
CHANGED
package/dist/lib/saas-audit.js
CHANGED
|
@@ -173,6 +173,7 @@ export class AuditLogger {
|
|
|
173
173
|
/**
|
|
174
174
|
* Map database log to AuditLog type
|
|
175
175
|
*/
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
|
|
176
177
|
mapDbLogToLog(dbLog) {
|
|
177
178
|
return {
|
|
178
179
|
id: dbLog.id,
|
|
@@ -200,9 +201,11 @@ export const auditLogger = new AuditLogger();
|
|
|
200
201
|
* Helper function to extract IP from request
|
|
201
202
|
*/
|
|
202
203
|
export function getIpFromRequest(req) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
205
|
+
const forwardedIp = typeof forwarded === 'string' ? forwarded.split(',')[0].trim() : undefined;
|
|
206
|
+
const realIp = req.headers['x-real-ip'];
|
|
207
|
+
return (forwardedIp ||
|
|
208
|
+
(typeof realIp === 'string' ? realIp : undefined) ||
|
|
206
209
|
req.socket?.remoteAddress);
|
|
207
210
|
}
|
|
208
211
|
/**
|
package/dist/lib/saas-auth.js
CHANGED
|
@@ -82,7 +82,7 @@ export function verifyToken(token) {
|
|
|
82
82
|
type: decoded.type,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
catch (
|
|
85
|
+
catch (_error) {
|
|
86
86
|
throw new Error('Invalid or expired token');
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -320,6 +320,7 @@ export class AuthService {
|
|
|
320
320
|
if (error) {
|
|
321
321
|
throw new Error(`Failed to get user organizations: ${error.message}`);
|
|
322
322
|
}
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB join result
|
|
323
324
|
return (data || []).map((row) => this.mapDbOrgToOrg(row.organizations));
|
|
324
325
|
}
|
|
325
326
|
/**
|
|
@@ -337,7 +338,7 @@ export class AuthService {
|
|
|
337
338
|
return generateToken();
|
|
338
339
|
}
|
|
339
340
|
const resetToken = generateToken();
|
|
340
|
-
const
|
|
341
|
+
const _expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour (for future use)
|
|
341
342
|
// Store reset token (we'll need a password_reset_tokens table)
|
|
342
343
|
// For now, just return the token
|
|
343
344
|
return resetToken;
|
|
@@ -345,7 +346,7 @@ export class AuthService {
|
|
|
345
346
|
/**
|
|
346
347
|
* Reset password
|
|
347
348
|
*/
|
|
348
|
-
async resetPassword(
|
|
349
|
+
async resetPassword(_token, _newPassword) {
|
|
349
350
|
// TODO: Implement password reset
|
|
350
351
|
// Need to create password_reset_tokens table
|
|
351
352
|
throw new Error('Not implemented');
|
|
@@ -378,6 +379,7 @@ export class AuthService {
|
|
|
378
379
|
/**
|
|
379
380
|
* Map database user to User type
|
|
380
381
|
*/
|
|
382
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
|
|
381
383
|
mapDbUserToUser(dbUser) {
|
|
382
384
|
return {
|
|
383
385
|
id: dbUser.id,
|
|
@@ -403,6 +405,7 @@ export class AuthService {
|
|
|
403
405
|
/**
|
|
404
406
|
* Map database organization to Organization type
|
|
405
407
|
*/
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
|
|
406
409
|
mapDbOrgToOrg(dbOrg) {
|
|
407
410
|
return {
|
|
408
411
|
id: dbOrg.id,
|