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 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 (options) => {
50
+ .action(async (_options) => {
51
51
  // No arguments - show secrets-focused help
52
52
  console.log('LSH - Encrypted Secrets Manager');
53
53
  console.log('');
@@ -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
- console.log(chalk.bold.cyan('\nšŸ„ LSH Health Check'));
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
- checks.push(await checkGitRepository(options.verbose));
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(verbose) {
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
@@ -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
- console.log(chalk.bold.cyan('\nšŸ” LSH Secrets Manager - Setup Wizard'));
42
- console.log(chalk.gray('━'.repeat(50)));
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(process.cwd(), '.env');
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(encryptionKey, repoName) {
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(config) {
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(process.cwd(), '.env');
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
- await updateGitignore();
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) {
@@ -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,
@@ -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\'t exist
111
+ // Ignore if file doesn't exist
112
112
  }
113
- // Close log stream
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 (import.meta.url === `file://${process.argv[1]}`) {
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.message || 'Authentication failed',
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
- const role = await organizationService.getUserRole(organizationId, req.user.id);
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.organizationRole = role;
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.message },
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.message.includes('ALREADY_EXISTS') ? 'EMAIL_ALREADY_EXISTS' : 'INTERNAL_ERROR', message: error.message },
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.message.includes('EMAIL_NOT_VERIFIED') ? 403 : 401;
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.message || 'INVALID_CREDENTIALS', message: error.message },
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.message },
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.message },
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.message },
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.user.id,
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.message },
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.message },
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.message },
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.user.id, 'canInviteMembers');
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.user.id,
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.message },
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.user.id);
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.message },
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.message },
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.user.id,
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.message.includes('TIER_LIMIT_EXCEEDED') ? 402 : 400;
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.message },
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.message },
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.message },
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.user.id,
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.message },
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.user.id);
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.message },
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.message },
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.user.id);
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.message },
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.user.id, 'canViewAuditLogs');
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.message },
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.user.id, 'canManageBilling');
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.message },
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.message });
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
- this.app.use((err, req, res, _next) => {
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
- res.status(err.status || 500).json({
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: err.code || 'INTERNAL_ERROR',
134
- message: isDev ? err.message : 'Internal server error',
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