lsh-framework 3.0.0 ā 3.1.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/dist/cli.js +1 -1
- package/dist/commands/doctor.js +32 -15
- package/dist/commands/init.js +31 -12
- 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 +63 -24
- package/package.json +4 -3
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
|
@@ -8,6 +8,7 @@ import * as path from 'path';
|
|
|
8
8
|
import { createClient } from '@supabase/supabase-js';
|
|
9
9
|
import { getPlatformPaths, getPlatformInfo } from '../lib/platform-utils.js';
|
|
10
10
|
import { IPFSClientManager } from '../lib/ipfs-client-manager.js';
|
|
11
|
+
import * as os from 'os';
|
|
11
12
|
/**
|
|
12
13
|
* Register doctor commands
|
|
13
14
|
*/
|
|
@@ -15,6 +16,7 @@ export function registerDoctorCommands(program) {
|
|
|
15
16
|
program
|
|
16
17
|
.command('doctor')
|
|
17
18
|
.description('Health check and troubleshooting')
|
|
19
|
+
.option('-g, --global', 'Use global workspace ($HOME)')
|
|
18
20
|
.option('-v, --verbose', 'Show detailed information')
|
|
19
21
|
.option('--json', 'Output results as JSON')
|
|
20
22
|
.action(async (options) => {
|
|
@@ -28,12 +30,25 @@ export function registerDoctorCommands(program) {
|
|
|
28
30
|
}
|
|
29
31
|
});
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the base directory for .env files
|
|
35
|
+
*/
|
|
36
|
+
function getBaseDir(globalMode) {
|
|
37
|
+
return globalMode ? os.homedir() : process.cwd();
|
|
38
|
+
}
|
|
31
39
|
/**
|
|
32
40
|
* Run comprehensive health check
|
|
33
41
|
*/
|
|
34
42
|
async function runHealthCheck(options) {
|
|
43
|
+
const baseDir = getBaseDir(options.global);
|
|
35
44
|
if (!options.json) {
|
|
36
|
-
|
|
45
|
+
if (options.global) {
|
|
46
|
+
console.log(chalk.bold.cyan('\nš„ LSH Health Check (Global Workspace)'));
|
|
47
|
+
console.log(chalk.yellow(` Location: ${baseDir}`));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(chalk.bold.cyan('\nš„ LSH Health Check'));
|
|
51
|
+
}
|
|
37
52
|
console.log(chalk.gray('ā'.repeat(50)));
|
|
38
53
|
console.log('');
|
|
39
54
|
}
|
|
@@ -41,18 +56,20 @@ async function runHealthCheck(options) {
|
|
|
41
56
|
// Platform check
|
|
42
57
|
checks.push(await checkPlatform(options.verbose));
|
|
43
58
|
// .env file check
|
|
44
|
-
checks.push(await checkEnvFile(options.verbose));
|
|
59
|
+
checks.push(await checkEnvFile(options.verbose, baseDir));
|
|
45
60
|
// Encryption key check
|
|
46
|
-
checks.push(await checkEncryptionKey(options.verbose));
|
|
61
|
+
checks.push(await checkEncryptionKey(options.verbose, baseDir));
|
|
47
62
|
// Storage backend check
|
|
48
|
-
const storageChecks = await checkStorageBackend(options.verbose);
|
|
63
|
+
const storageChecks = await checkStorageBackend(options.verbose, baseDir);
|
|
49
64
|
checks.push(...storageChecks);
|
|
50
|
-
// Git repository check
|
|
51
|
-
|
|
65
|
+
// Git repository check (skip for global mode)
|
|
66
|
+
if (!options.global) {
|
|
67
|
+
checks.push(await checkGitRepository(options.verbose));
|
|
68
|
+
}
|
|
52
69
|
// IPFS client check
|
|
53
70
|
checks.push(await checkIPFSClient(options.verbose));
|
|
54
71
|
// Permissions check
|
|
55
|
-
checks.push(await checkPermissions(options.verbose));
|
|
72
|
+
checks.push(await checkPermissions(options.verbose, baseDir));
|
|
56
73
|
// Display results
|
|
57
74
|
if (options.json) {
|
|
58
75
|
console.log(JSON.stringify({ checks, summary: getSummary(checks) }, null, 2));
|
|
@@ -86,9 +103,9 @@ async function checkPlatform(verbose) {
|
|
|
86
103
|
/**
|
|
87
104
|
* Check .env file
|
|
88
105
|
*/
|
|
89
|
-
async function checkEnvFile(verbose) {
|
|
106
|
+
async function checkEnvFile(verbose, baseDir) {
|
|
90
107
|
try {
|
|
91
|
-
const envPath = path.join(process.cwd(), '.env');
|
|
108
|
+
const envPath = path.join(baseDir || process.cwd(), '.env');
|
|
92
109
|
// Read file directly without access check to avoid TOCTOU race condition
|
|
93
110
|
const content = await fs.readFile(envPath, 'utf-8');
|
|
94
111
|
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
@@ -111,9 +128,9 @@ async function checkEnvFile(verbose) {
|
|
|
111
128
|
/**
|
|
112
129
|
* Check encryption key
|
|
113
130
|
*/
|
|
114
|
-
async function checkEncryptionKey(verbose) {
|
|
131
|
+
async function checkEncryptionKey(verbose, baseDir) {
|
|
115
132
|
try {
|
|
116
|
-
const envPath = path.join(process.cwd(), '.env');
|
|
133
|
+
const envPath = path.join(baseDir || process.cwd(), '.env');
|
|
117
134
|
const content = await fs.readFile(envPath, 'utf-8');
|
|
118
135
|
const match = content.match(/^LSH_SECRETS_KEY=(.+)$/m);
|
|
119
136
|
if (!match) {
|
|
@@ -163,10 +180,10 @@ async function checkEncryptionKey(verbose) {
|
|
|
163
180
|
/**
|
|
164
181
|
* Check storage backend configuration
|
|
165
182
|
*/
|
|
166
|
-
async function checkStorageBackend(verbose) {
|
|
183
|
+
async function checkStorageBackend(verbose, baseDir) {
|
|
167
184
|
const checks = [];
|
|
168
185
|
try {
|
|
169
|
-
const envPath = path.join(process.cwd(), '.env');
|
|
186
|
+
const envPath = path.join(baseDir || process.cwd(), '.env');
|
|
170
187
|
const content = await fs.readFile(envPath, 'utf-8');
|
|
171
188
|
const supabaseUrl = content.match(/^SUPABASE_URL=(.+)$/m)?.[1]?.trim();
|
|
172
189
|
const supabaseKey = content.match(/^SUPABASE_ANON_KEY=(.+)$/m)?.[1]?.trim();
|
|
@@ -263,7 +280,7 @@ async function testSupabaseConnection(url, key, verbose) {
|
|
|
263
280
|
/**
|
|
264
281
|
* Check if in git repository
|
|
265
282
|
*/
|
|
266
|
-
async function checkGitRepository(
|
|
283
|
+
async function checkGitRepository(_verbose) {
|
|
267
284
|
try {
|
|
268
285
|
const gitPath = path.join(process.cwd(), '.git');
|
|
269
286
|
// Use stat instead of access to avoid TOCTOU race condition
|
|
@@ -332,7 +349,7 @@ async function checkIPFSClient(verbose) {
|
|
|
332
349
|
/**
|
|
333
350
|
* Check file permissions
|
|
334
351
|
*/
|
|
335
|
-
async function checkPermissions(verbose) {
|
|
352
|
+
async function checkPermissions(verbose, _baseDir) {
|
|
336
353
|
try {
|
|
337
354
|
const paths = getPlatformPaths();
|
|
338
355
|
// Check if we can write to temp directory with secure permissions
|
package/dist/commands/init.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createClient } from '@supabase/supabase-js';
|
|
|
11
11
|
import ora from 'ora';
|
|
12
12
|
import { getPlatformPaths } from '../lib/platform-utils.js';
|
|
13
13
|
import { getGitRepoInfo } from '../lib/git-utils.js';
|
|
14
|
+
import * as os from 'os';
|
|
14
15
|
/**
|
|
15
16
|
* Register init commands
|
|
16
17
|
*/
|
|
@@ -18,6 +19,7 @@ export function registerInitCommands(program) {
|
|
|
18
19
|
program
|
|
19
20
|
.command('init')
|
|
20
21
|
.description('Interactive setup wizard (first-time configuration)')
|
|
22
|
+
.option('-g, --global', 'Use global workspace ($HOME)')
|
|
21
23
|
.option('--local', 'Use local-only encryption (no cloud sync)')
|
|
22
24
|
.option('--storacha', 'Use Storacha IPFS network sync (recommended)')
|
|
23
25
|
.option('--supabase', 'Use Supabase cloud storage')
|
|
@@ -34,15 +36,30 @@ export function registerInitCommands(program) {
|
|
|
34
36
|
}
|
|
35
37
|
});
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the base directory for .env files
|
|
41
|
+
*/
|
|
42
|
+
function getBaseDir(globalMode) {
|
|
43
|
+
return globalMode ? os.homedir() : process.cwd();
|
|
44
|
+
}
|
|
37
45
|
/**
|
|
38
46
|
* Run the interactive setup wizard
|
|
39
47
|
*/
|
|
40
48
|
async function runSetupWizard(options) {
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
const globalMode = options.global ?? false;
|
|
50
|
+
const baseDir = getBaseDir(globalMode);
|
|
51
|
+
if (globalMode) {
|
|
52
|
+
console.log(chalk.bold.cyan('\nš LSH Secrets Manager - Global Setup Wizard'));
|
|
53
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
54
|
+
console.log(chalk.yellow(`\nš Global Mode: Using $HOME (${baseDir})`));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log(chalk.bold.cyan('\nš LSH Secrets Manager - Setup Wizard'));
|
|
58
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
59
|
+
}
|
|
43
60
|
console.log('');
|
|
44
61
|
// Check if already configured
|
|
45
|
-
const existingConfig = await checkExistingConfig();
|
|
62
|
+
const existingConfig = await checkExistingConfig(baseDir);
|
|
46
63
|
if (existingConfig) {
|
|
47
64
|
const { overwrite } = await inquirer.prompt([
|
|
48
65
|
{
|
|
@@ -181,16 +198,16 @@ async function runSetupWizard(options) {
|
|
|
181
198
|
}
|
|
182
199
|
}
|
|
183
200
|
// Save configuration
|
|
184
|
-
await saveConfiguration(config);
|
|
201
|
+
await saveConfiguration(config, baseDir, globalMode);
|
|
185
202
|
// Show success message
|
|
186
203
|
showSuccessMessage(config);
|
|
187
204
|
}
|
|
188
205
|
/**
|
|
189
206
|
* Check if LSH is already configured
|
|
190
207
|
*/
|
|
191
|
-
async function checkExistingConfig() {
|
|
208
|
+
async function checkExistingConfig(baseDir) {
|
|
192
209
|
try {
|
|
193
|
-
const envPath = path.join(
|
|
210
|
+
const envPath = path.join(baseDir, '.env');
|
|
194
211
|
// Read file directly without access check to avoid TOCTOU race condition
|
|
195
212
|
const content = await fs.readFile(envPath, 'utf-8');
|
|
196
213
|
return content.includes('LSH_SECRETS_KEY') ||
|
|
@@ -204,7 +221,7 @@ async function checkExistingConfig() {
|
|
|
204
221
|
/**
|
|
205
222
|
* Pull secrets after init is complete
|
|
206
223
|
*/
|
|
207
|
-
async function pullSecretsAfterInit(
|
|
224
|
+
async function pullSecretsAfterInit(_encryptionKey, _repoName) {
|
|
208
225
|
const spinner = ora('Pulling secrets from cloud...').start();
|
|
209
226
|
try {
|
|
210
227
|
// Dynamically import SecretsManager to avoid circular dependencies
|
|
@@ -399,7 +416,7 @@ async function configurePostgres(config, skipTest) {
|
|
|
399
416
|
/**
|
|
400
417
|
* Configure local-only mode
|
|
401
418
|
*/
|
|
402
|
-
async function configureLocal(
|
|
419
|
+
async function configureLocal(_config) {
|
|
403
420
|
console.log(chalk.cyan('\nš¾ Local Encryption Mode'));
|
|
404
421
|
console.log(chalk.gray('Secrets will be encrypted locally. No cloud sync available.'));
|
|
405
422
|
console.log('');
|
|
@@ -417,10 +434,10 @@ function generateEncryptionKey() {
|
|
|
417
434
|
/**
|
|
418
435
|
* Save configuration to .env file
|
|
419
436
|
*/
|
|
420
|
-
async function saveConfiguration(config) {
|
|
437
|
+
async function saveConfiguration(config, baseDir, globalMode) {
|
|
421
438
|
const spinner = ora('Saving configuration...').start();
|
|
422
439
|
try {
|
|
423
|
-
const envPath = path.join(
|
|
440
|
+
const envPath = path.join(baseDir, '.env');
|
|
424
441
|
let envContent = '';
|
|
425
442
|
// Try to read existing .env
|
|
426
443
|
try {
|
|
@@ -461,8 +478,10 @@ async function saveConfiguration(config) {
|
|
|
461
478
|
}
|
|
462
479
|
// Write .env file
|
|
463
480
|
await fs.writeFile(envPath, envContent, 'utf-8');
|
|
464
|
-
// Update .gitignore
|
|
465
|
-
|
|
481
|
+
// Update .gitignore (skip for global mode since it's in $HOME)
|
|
482
|
+
if (!globalMode) {
|
|
483
|
+
await updateGitignore();
|
|
484
|
+
}
|
|
466
485
|
spinner.succeed(chalk.green('ā
Configuration saved'));
|
|
467
486
|
}
|
|
468
487
|
catch (error) {
|
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
|