lsh-framework 3.1.0 → 3.1.2

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.
@@ -5,11 +5,12 @@
5
5
  import { spawn } from 'child_process';
6
6
  import { getConfigManager, loadGlobalConfig } from '../lib/config-manager.js';
7
7
  import * as fs from 'fs/promises';
8
+ import { ENV_VARS } from '../constants/index.js';
8
9
  /**
9
10
  * Get user's preferred editor
10
11
  */
11
12
  function getEditor() {
12
- return process.env.VISUAL || process.env.EDITOR || 'vi';
13
+ return process.env[ENV_VARS.VISUAL] || process.env[ENV_VARS.EDITOR] || 'vi';
13
14
  }
14
15
  /**
15
16
  * Open config file in user's editor
@@ -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();
@@ -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') ||
@@ -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) {
@@ -9,6 +9,7 @@ import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import chalk from 'chalk';
11
11
  import { fileURLToPath } from 'url';
12
+ import { ENV_VARS } from '../constants/index.js';
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = path.dirname(__filename);
14
15
  const selfCommand = new Command('self');
@@ -300,9 +301,9 @@ selfCommand
300
301
  console.log();
301
302
  // Environment
302
303
  console.log(chalk.yellow('Environment:'));
303
- console.log(' NODE_ENV:', process.env.NODE_ENV || 'not set');
304
- console.log(' HOME:', process.env.HOME || 'not set');
305
- console.log(' USER:', process.env.USER || 'not set');
304
+ console.log(' NODE_ENV:', process.env[ENV_VARS.NODE_ENV] || 'not set');
305
+ console.log(' HOME:', process.env[ENV_VARS.HOME] || 'not set');
306
+ console.log(' USER:', process.env[ENV_VARS.USER] || 'not set');
306
307
  console.log();
307
308
  // Configuration
308
309
  const envFile = path.join(process.cwd(), '.env');
@@ -7,7 +7,7 @@ export const ENDPOINTS = {
7
7
  // Health and status
8
8
  HEALTH: '/health',
9
9
  ROOT: '/',
10
- // Authentication
10
+ // Authentication (legacy)
11
11
  AUTH: '/api/auth',
12
12
  AUTH_REGISTER: '/auth/register',
13
13
  AUTH_LOGIN: '/auth/login',
@@ -48,6 +48,39 @@ export const ENDPOINTS = {
48
48
  API_ANALYTICS_PREDICTIONS: '/api/analytics/predictions',
49
49
  API_ANALYTICS_COSTS: '/api/analytics/costs',
50
50
  API_ANALYTICS_BOTTLENECKS: '/api/analytics/bottlenecks',
51
+ // SaaS API v1 - Authentication
52
+ API_V1_AUTH_SIGNUP: '/api/v1/auth/signup',
53
+ API_V1_AUTH_LOGIN: '/api/v1/auth/login',
54
+ API_V1_AUTH_VERIFY_EMAIL: '/api/v1/auth/verify-email',
55
+ API_V1_AUTH_RESEND_VERIFICATION: '/api/v1/auth/resend-verification',
56
+ API_V1_AUTH_REFRESH: '/api/v1/auth/refresh',
57
+ API_V1_AUTH_ME: '/api/v1/auth/me',
58
+ // SaaS API v1 - Organizations
59
+ API_V1_ORGANIZATIONS: '/api/v1/organizations',
60
+ API_V1_ORGANIZATION: '/api/v1/organizations/:organizationId',
61
+ API_V1_ORGANIZATION_MEMBERS: '/api/v1/organizations/:organizationId/members',
62
+ API_V1_ORGANIZATION_TEAMS: '/api/v1/organizations/:organizationId/teams',
63
+ API_V1_ORGANIZATION_AUDIT: '/api/v1/organizations/:organizationId/audit',
64
+ API_V1_ORGANIZATION_BILLING_SUBSCRIPTION: '/api/v1/organizations/:organizationId/billing/subscription',
65
+ API_V1_ORGANIZATION_BILLING_CHECKOUT: '/api/v1/organizations/:organizationId/billing/checkout',
66
+ // SaaS API v1 - Teams
67
+ API_V1_TEAM_SECRETS: '/api/v1/teams/:teamId/secrets',
68
+ API_V1_TEAM_SECRET: '/api/v1/teams/:teamId/secrets/:secretId',
69
+ API_V1_TEAM_SECRET_RETRIEVE: '/api/v1/teams/:teamId/secrets/:secretId/retrieve',
70
+ API_V1_TEAM_SECRETS_EXPORT: '/api/v1/teams/:teamId/secrets/export/env',
71
+ API_V1_TEAM_SECRETS_IMPORT: '/api/v1/teams/:teamId/secrets/import/env',
72
+ // SaaS API v1 - Webhooks
73
+ API_V1_WEBHOOKS_STRIPE: '/api/v1/webhooks/stripe',
74
+ API_V1_BILLING_WEBHOOKS: '/api/v1/billing/webhooks',
75
+ API_V1_ORGANIZATION_AUDIT_LOGS: '/api/v1/organizations/:organizationId/audit-logs',
76
+ // API Info/Documentation (patterns)
77
+ API_V1_ROOT: '/api/v1',
78
+ API_V1_AUTH_WILDCARD: '/api/v1/auth/*',
79
+ API_V1_ORGANIZATIONS_WILDCARD: '/api/v1/organizations/*',
80
+ API_V1_ORG_TEAMS_WILDCARD: '/api/v1/organizations/:id/teams/*',
81
+ API_V1_TEAMS_SECRETS_WILDCARD: '/api/v1/teams/:id/secrets/*',
82
+ API_V1_ORG_BILLING_WILDCARD: '/api/v1/organizations/:id/billing/*',
83
+ API_V1_ORG_AUDIT_LOGS: '/api/v1/organizations/:id/audit-logs',
51
84
  };
52
85
  export const HTTP_HEADERS = {
53
86
  // Standard headers
@@ -8,14 +8,37 @@ export const ENV_VARS = {
8
8
  NODE_ENV: 'NODE_ENV',
9
9
  USER: 'USER',
10
10
  HOSTNAME: 'HOSTNAME',
11
+ HOME: 'HOME',
12
+ SHELL: 'SHELL',
13
+ EDITOR: 'EDITOR',
14
+ VISUAL: 'VISUAL',
15
+ // Platform-specific
16
+ USERPROFILE: 'USERPROFILE',
17
+ APPDATA: 'APPDATA',
18
+ LOCALAPPDATA: 'LOCALAPPDATA',
19
+ COMSPEC: 'COMSPEC',
11
20
  // LSH API configuration
12
21
  LSH_API_ENABLED: 'LSH_API_ENABLED',
13
22
  LSH_API_PORT: 'LSH_API_PORT',
14
23
  LSH_API_KEY: 'LSH_API_KEY',
15
24
  LSH_JWT_SECRET: 'LSH_JWT_SECRET',
16
25
  LSH_ALLOW_DANGEROUS_COMMANDS: 'LSH_ALLOW_DANGEROUS_COMMANDS',
26
+ // LSH SaaS API
27
+ LSH_SAAS_API_PORT: 'LSH_SAAS_API_PORT',
28
+ LSH_SAAS_API_HOST: 'LSH_SAAS_API_HOST',
29
+ LSH_CORS_ORIGINS: 'LSH_CORS_ORIGINS',
17
30
  // Secrets management
18
31
  LSH_SECRETS_KEY: 'LSH_SECRETS_KEY',
32
+ LSH_MASTER_KEY: 'LSH_MASTER_KEY',
33
+ // Feature flags
34
+ LSH_LOCAL_STORAGE_QUIET: 'LSH_LOCAL_STORAGE_QUIET',
35
+ LSH_V1_COMPAT: 'LSH_V1_COMPAT',
36
+ LSH_STORACHA_ENABLED: 'LSH_STORACHA_ENABLED',
37
+ DISABLE_IPFS_SYNC: 'DISABLE_IPFS_SYNC',
38
+ // Logging
39
+ LSH_LOG_LEVEL: 'LSH_LOG_LEVEL',
40
+ LSH_LOG_FORMAT: 'LSH_LOG_FORMAT',
41
+ NO_COLOR: 'NO_COLOR',
19
42
  // Webhooks
20
43
  LSH_ENABLE_WEBHOOKS: 'LSH_ENABLE_WEBHOOKS',
21
44
  WEBHOOK_PORT: 'WEBHOOK_PORT',
@@ -29,28 +52,65 @@ export const ENV_VARS = {
29
52
  REDIS_URL: 'REDIS_URL',
30
53
  // Monitoring
31
54
  MONITORING_API_PORT: 'MONITORING_API_PORT',
55
+ // Stripe billing
56
+ STRIPE_SECRET_KEY: 'STRIPE_SECRET_KEY',
57
+ STRIPE_WEBHOOK_SECRET: 'STRIPE_WEBHOOK_SECRET',
58
+ STRIPE_PRICE_PRO_MONTHLY: 'STRIPE_PRICE_PRO_MONTHLY',
59
+ STRIPE_PRICE_PRO_YEARLY: 'STRIPE_PRICE_PRO_YEARLY',
60
+ STRIPE_PRICE_ENTERPRISE_MONTHLY: 'STRIPE_PRICE_ENTERPRISE_MONTHLY',
61
+ STRIPE_PRICE_ENTERPRISE_YEARLY: 'STRIPE_PRICE_ENTERPRISE_YEARLY',
62
+ // Email (Resend)
63
+ RESEND_API_KEY: 'RESEND_API_KEY',
64
+ EMAIL_FROM: 'EMAIL_FROM',
65
+ BASE_URL: 'BASE_URL',
32
66
  };
33
67
  export const DEFAULTS = {
34
68
  // Version
35
69
  VERSION: '0.5.1',
36
70
  // Ports
37
71
  API_PORT: 3030,
72
+ SAAS_API_PORT: 3031,
38
73
  WEBHOOK_PORT: 3033,
39
74
  MONITORING_API_PORT: 3031,
75
+ MONITORING_PORT: 3000,
76
+ // Hosts
77
+ DEFAULT_HOST: '0.0.0.0',
78
+ DEFAULT_CORS_ORIGINS: ['*'],
40
79
  // URLs
41
80
  REDIS_URL: 'redis://localhost:6379',
42
81
  DATABASE_URL: 'postgresql://localhost:5432/cicd',
43
- // Timeouts and intervals
82
+ STRIPE_API_URL: 'https://api.stripe.com/v1',
83
+ RESEND_API_URL: 'https://api.resend.com/emails',
84
+ LSH_APP_URL: 'https://app.lsh.dev',
85
+ LSH_DOCS_URL: 'https://docs.lsh.dev',
86
+ KUBO_RELEASES_URL: 'https://api.github.com/repos/ipfs/kubo/releases/latest',
87
+ // Email defaults
88
+ DEFAULT_EMAIL_FROM: 'noreply@lsh.dev',
89
+ // Timeouts and intervals (in milliseconds)
44
90
  CHECK_INTERVAL_MS: 2000,
45
91
  REQUEST_TIMEOUT_MS: 10000,
92
+ DAEMON_RESTART_DELAY_MS: 1000,
93
+ JOB_TIMEOUT_1H_MS: 3600000,
94
+ JOB_TIMEOUT_5M_MS: 300000,
95
+ JOB_TIMEOUT_1M_MS: 60000,
96
+ JOB_TIMEOUT_2H_MS: 7200000,
46
97
  // Sizes
47
98
  MAX_BUFFER_SIZE_BYTES: 1024 * 1024, // 1MB
48
99
  MAX_LOG_SIZE_BYTES: 10 * 1024 * 1024, // 10MB
49
100
  MAX_COMMAND_LENGTH: 10000,
101
+ SOCKET_BUFFER_SIZE: 1024,
102
+ MAX_EVENTS_LIMIT: 100,
103
+ // History limits
104
+ MAX_HISTORY_SIZE: 10000,
105
+ HISTORY_SAVE_INTERVAL_MS: 30000,
50
106
  // Limits
51
107
  MAX_COMMAND_CHAINS: 5,
52
108
  MAX_PIPE_USAGE: 3,
53
109
  // Cache and retention
54
110
  REDIS_CACHE_EXPIRY_SECONDS: 3600, // 1 hour
55
111
  METRICS_RETENTION_SECONDS: 30 * 24 * 60 * 60, // 30 days
112
+ // Shell defaults
113
+ DEFAULT_SHELL_UNIX: '/bin/sh',
114
+ DEFAULT_SHELL_WIN: 'cmd.exe',
115
+ DEFAULT_EDITOR: 'vi',
56
116
  };
@@ -14,6 +14,27 @@ export const TABLES = {
14
14
  SHELL_COMPLETIONS: 'shell_completions',
15
15
  // CI/CD tables
16
16
  PIPELINE_EVENTS: 'pipeline_events',
17
+ // SaaS tables - Users and Authentication
18
+ USERS: 'users',
19
+ USER_SESSIONS: 'user_sessions',
20
+ // SaaS tables - Organizations and Teams
21
+ ORGANIZATIONS: 'organizations',
22
+ ORGANIZATION_MEMBERS: 'organization_members',
23
+ TEAMS: 'teams',
24
+ TEAM_MEMBERS: 'team_members',
25
+ // SaaS tables - Secrets
26
+ SECRETS: 'secrets',
27
+ SECRET_ACCESS_LOGS: 'secret_access_logs',
28
+ SECRET_VERSIONS: 'secret_versions',
29
+ // SaaS tables - Audit
30
+ AUDIT_LOGS: 'audit_logs',
31
+ // SaaS tables - Billing
32
+ SUBSCRIPTIONS: 'subscriptions',
33
+ INVOICES: 'invoices',
34
+ // SaaS tables - Encryption
35
+ ENCRYPTION_KEYS: 'encryption_keys',
36
+ // LSH Secrets (legacy name)
37
+ LSH_SECRETS: 'lsh_secrets',
17
38
  // Trading/ML tables (legacy)
18
39
  TRADING_DISCLOSURES: 'trading_disclosures',
19
40
  POLITICIANS: 'politicians',
@@ -77,3 +77,27 @@ export const RISK_LEVELS = {
77
77
  MEDIUM: 'medium',
78
78
  LOW: 'low',
79
79
  };
80
+ /**
81
+ * API Error Codes
82
+ *
83
+ * Standard error codes used across the SaaS API and services.
84
+ */
85
+ export const ERROR_CODES = {
86
+ // Authentication errors
87
+ UNAUTHORIZED: 'UNAUTHORIZED',
88
+ INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
89
+ INVALID_TOKEN: 'INVALID_TOKEN',
90
+ EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED',
91
+ EMAIL_ALREADY_EXISTS: 'EMAIL_ALREADY_EXISTS',
92
+ // Authorization errors
93
+ FORBIDDEN: 'FORBIDDEN',
94
+ // Validation errors
95
+ INVALID_INPUT: 'INVALID_INPUT',
96
+ NOT_FOUND: 'NOT_FOUND',
97
+ ALREADY_EXISTS: 'ALREADY_EXISTS',
98
+ // Payment errors
99
+ PAYMENT_REQUIRED: 'PAYMENT_REQUIRED',
100
+ TIER_LIMIT_EXCEEDED: 'TIER_LIMIT_EXCEEDED',
101
+ // Server errors
102
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
103
+ };
@@ -17,6 +17,9 @@ export const PATHS = {
17
17
  DAEMON_JOBS_FILE_TEMPLATE: '/tmp/lsh-daemon-jobs-${USER}.json',
18
18
  // Job persistence
19
19
  DEFAULT_JOBS_PERSISTENCE_FILE: '/tmp/lsh-jobs.json',
20
+ // Job registry files
21
+ JOB_REGISTRY_FILE: '/tmp/lsh-job-registry.json',
22
+ JOB_LOGS_DIR: '/tmp/lsh-job-logs',
20
23
  };
21
24
  export const PREFIXES = {
22
25
  SESSION_ID: 'lsh_',
@@ -71,3 +71,83 @@ export const LOG_LEVELS = {
71
71
  ERROR: 'ERROR',
72
72
  DEBUG: 'DEBUG',
73
73
  };
74
+ /**
75
+ * Emoji prefixes for consistent UI output
76
+ */
77
+ export const EMOJI = {
78
+ SUCCESS: 'āœ…',
79
+ ERROR: 'āŒ',
80
+ WARNING: 'āš ļø',
81
+ INFO: 'ā„¹ļø',
82
+ TIP: 'šŸ’”',
83
+ KEY: 'šŸ”‘',
84
+ FILE: 'šŸ“„',
85
+ FOLDER: 'šŸ“',
86
+ LIST: 'šŸ“‹',
87
+ SEARCH: 'šŸ”',
88
+ LOCATION: 'šŸ“',
89
+ UP: 'ā¬†ļø',
90
+ DOWN: 'ā¬‡ļø',
91
+ CALENDAR: 'šŸ“…',
92
+ GEAR: 'āš™ļø',
93
+ };
94
+ /**
95
+ * Status messages with emoji
96
+ */
97
+ export const STATUS_MESSAGES = {
98
+ // Success messages
99
+ SUCCESS: 'āœ…',
100
+ SUCCESS_GENERIC: 'āœ… Success',
101
+ CONNECTION_SUCCESS: 'āœ… Connection successful!',
102
+ CONFIG_SAVED: 'āœ… Configuration saved',
103
+ SECRETS_PULLED: 'āœ… Secrets pulled successfully!',
104
+ IPFS_INSTALLED: 'āœ… IPFS client installed',
105
+ // Error messages
106
+ ERROR: 'āŒ',
107
+ ERROR_GENERIC: 'āŒ Error',
108
+ CONNECTION_FAILED: 'āŒ Connection failed',
109
+ CONFIG_SAVE_FAILED: 'āŒ Failed to save configuration',
110
+ PULL_FAILED: 'āŒ Failed to pull secrets',
111
+ // Warning messages
112
+ WARNING: 'āš ļø',
113
+ WARNING_GENERIC: 'āš ļø Warning',
114
+ IPFS_NOT_INSTALLED: 'āš ļø IPFS client not installed',
115
+ NOT_GIT_REPO: 'ā„¹ļø Not in a git repository',
116
+ // Info messages
117
+ INFO: 'ā„¹ļø',
118
+ RECOMMENDATIONS: 'šŸ’” Recommendations:',
119
+ CURRENT_REPO: 'šŸ“ Current Repository:',
120
+ };
121
+ /**
122
+ * Doctor/diagnostic messages
123
+ */
124
+ export const DOCTOR_MESSAGES = {
125
+ CHECKING: 'šŸ” Checking:',
126
+ ALL_PASSED: 'āœ… All checks passed!',
127
+ ISSUES_FOUND: 'āŒ Issues found',
128
+ RECOMMENDATIONS: 'šŸ’” Recommendations:',
129
+ };
130
+ /**
131
+ * Init/setup messages
132
+ */
133
+ export const INIT_MESSAGES = {
134
+ WELCOME: 'šŸš€ Welcome to LSH Setup',
135
+ STEP_COMPLETE: 'āœ… Step complete',
136
+ SETUP_COMPLETE: 'āœ… Setup complete!',
137
+ CONNECTION_TEST_SKIPPED: 'āš ļø Connection test skipped. Run "lsh doctor" after setup to verify.',
138
+ };
139
+ /**
140
+ * Migration messages
141
+ */
142
+ export const MIGRATION_MESSAGES = {
143
+ SCANNING: 'šŸ” Scanning for Firebase references...',
144
+ MIGRATING: 'šŸ”„ Migrating...',
145
+ COMPLETE: 'āœ… Migration complete',
146
+ NO_CHANGES: 'ā„¹ļø No changes needed',
147
+ };
148
+ /**
149
+ * Deprecation warnings
150
+ */
151
+ export const DEPRECATION_WARNINGS = {
152
+ LIB_COMMANDS: '\x1b[33māš ļø WARNING: "lsh lib" commands are deprecated as of v1.0.0\x1b[0m',
153
+ };
@@ -11,6 +11,7 @@ import * as os from 'os';
11
11
  import { exec } from 'child_process';
12
12
  import { BaseJobManager } from '../lib/base-job-manager.js';
13
13
  import MemoryJobStorage from '../lib/job-storage-memory.js';
14
+ import { ENV_VARS } from '../constants/index.js';
14
15
  export class JobRegistry extends BaseJobManager {
15
16
  config;
16
17
  records = new Map(); // jobId -> execution records
@@ -47,7 +48,7 @@ export class JobRegistry extends BaseJobManager {
47
48
  outputSize: 0,
48
49
  environment: { ...(job.env || {}) },
49
50
  workingDirectory: job.cwd || process.cwd(),
50
- user: job.user || process.env.USER || 'unknown',
51
+ user: job.user || process.env[ENV_VARS.USER] || 'unknown',
51
52
  hostname: os.hostname(),
52
53
  tags: [...(job.tags || [])],
53
54
  priority: job.priority || 5,
@@ -14,6 +14,7 @@ import { validateCommand } from '../lib/command-validator.js';
14
14
  import { validateEnvironment, printValidationResults } from '../lib/env-validator.js';
15
15
  import { createLogger } from '../lib/logger.js';
16
16
  import { getPlatformPaths } from '../lib/platform-utils.js';
17
+ import { ENV_VARS, DEFAULTS, ERRORS } from '../constants/index.js';
17
18
  const execAsync = promisify(exec);
18
19
  export class LSHJobDaemon extends EventEmitter {
19
20
  config;
@@ -34,13 +35,13 @@ export class LSHJobDaemon extends EventEmitter {
34
35
  logFile: platformPaths.logFile,
35
36
  jobsFile: jobsFilePath,
36
37
  socketPath: platformPaths.socketPath,
37
- checkInterval: 2000, // 2 seconds for better cron accuracy
38
- maxLogSize: 10 * 1024 * 1024, // 10MB
38
+ checkInterval: DEFAULTS.CHECK_INTERVAL_MS,
39
+ maxLogSize: DEFAULTS.MAX_LOG_SIZE_BYTES,
39
40
  autoRestart: true,
40
- apiEnabled: process.env.LSH_API_ENABLED === 'true' || false,
41
- apiPort: parseInt(process.env.LSH_API_PORT || '3030'),
42
- apiKey: process.env.LSH_API_KEY,
43
- enableWebhooks: process.env.LSH_ENABLE_WEBHOOKS === 'true',
41
+ apiEnabled: process.env[ENV_VARS.LSH_API_ENABLED] === 'true' || false,
42
+ apiPort: parseInt(process.env[ENV_VARS.LSH_API_PORT] || String(DEFAULTS.API_PORT)),
43
+ apiKey: process.env[ENV_VARS.LSH_API_KEY],
44
+ enableWebhooks: process.env[ENV_VARS.LSH_ENABLE_WEBHOOKS] === 'true',
44
45
  ...config
45
46
  };
46
47
  this.jobManager = new JobManager(this.config.jobsFile);
@@ -62,9 +63,9 @@ export class LSHJobDaemon extends EventEmitter {
62
63
  printValidationResults(envValidation, false);
63
64
  }
64
65
  // Fail fast in production if validation fails
65
- if (!envValidation.isValid && process.env.NODE_ENV === 'production') {
66
+ if (!envValidation.isValid && process.env[ENV_VARS.NODE_ENV] === 'production') {
66
67
  this.log('ERROR', 'Environment validation failed in production');
67
- throw new Error('Invalid environment configuration. Check logs for details.');
68
+ throw new Error(ERRORS.INVALID_ENV_CONFIG);
68
69
  }
69
70
  // Log warnings even in development
70
71
  if (envValidation.warnings.length > 0) {
@@ -124,7 +125,7 @@ export class LSHJobDaemon extends EventEmitter {
124
125
  */
125
126
  async restart() {
126
127
  await this.stop();
127
- await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
128
+ await new Promise(resolve => setTimeout(resolve, DEFAULTS.DAEMON_RESTART_DELAY_MS));
128
129
  await this.start();
129
130
  }
130
131
  /**
@@ -181,8 +182,8 @@ export class LSHJobDaemon extends EventEmitter {
181
182
  }
182
183
  // Validate command for security issues
183
184
  const validation = validateCommand(job.command, {
184
- allowDangerousCommands: process.env.LSH_ALLOW_DANGEROUS_COMMANDS === 'true',
185
- maxLength: 10000
185
+ allowDangerousCommands: process.env[ENV_VARS.LSH_ALLOW_DANGEROUS_COMMANDS] === 'true',
186
+ maxLength: DEFAULTS.MAX_COMMAND_LENGTH
186
187
  });
187
188
  if (!validation.isValid) {
188
189
  const errorMsg = `Command validation failed: ${validation.errors.join(', ')}`;
@@ -298,8 +299,7 @@ export class LSHJobDaemon extends EventEmitter {
298
299
  return sanitizedJobs.slice(0, limit);
299
300
  }
300
301
  // Default limit to prevent oversized responses
301
- const defaultLimit = 100;
302
- return sanitizedJobs.slice(0, defaultLimit);
302
+ return sanitizedJobs.slice(0, DEFAULTS.MAX_EVENTS_LIMIT);
303
303
  }
304
304
  catch (error) {
305
305
  this.log('ERROR', `Failed to list jobs: ${error.message}`);
@@ -736,7 +736,6 @@ const cliLogger = createLogger('LSHDaemonCLI');
736
736
  const isMainModule = () => {
737
737
  try {
738
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
739
  const getImportMetaUrl = new Function('return import.meta.url');
741
740
  const metaUrl = getImportMetaUrl();
742
741
  return metaUrl === `file://${process.argv[1]}`;
@@ -774,7 +773,7 @@ if (isMainModule()) {
774
773
  schedule: { interval: 0 }, // Run once
775
774
  env: process.env,
776
775
  cwd: process.cwd(),
777
- user: process.env.USER,
776
+ user: process.env[ENV_VARS.USER],
778
777
  priority: 5,
779
778
  tags: ['manual'],
780
779
  maxRetries: 0,
@@ -7,6 +7,8 @@ import cors from 'cors';
7
7
  import rateLimit from 'express-rate-limit';
8
8
  import { setupSaaSApiRoutes } from './saas-api-routes.js';
9
9
  import { getErrorMessage } from '../lib/saas-types.js';
10
+ import { ENV_VARS, DEFAULTS } from '../constants/index.js';
11
+ import { ERROR_CODES } from '../constants/errors.js';
10
12
  /**
11
13
  * SaaS API Server
12
14
  */
@@ -16,11 +18,11 @@ export class SaaSApiServer {
16
18
  server;
17
19
  constructor(config) {
18
20
  this.config = {
19
- port: config?.port || parseInt(process.env.LSH_SAAS_API_PORT || '3031'),
20
- host: config?.host || process.env.LSH_SAAS_API_HOST || '0.0.0.0',
21
- corsOrigins: config?.corsOrigins || (process.env.LSH_CORS_ORIGINS?.split(',') || ['*']),
21
+ port: config?.port || parseInt(process.env[ENV_VARS.LSH_SAAS_API_PORT] || String(DEFAULTS.SAAS_API_PORT)),
22
+ host: config?.host || process.env[ENV_VARS.LSH_SAAS_API_HOST] || DEFAULTS.DEFAULT_HOST,
23
+ corsOrigins: config?.corsOrigins || (process.env[ENV_VARS.LSH_CORS_ORIGINS]?.split(',') || [...DEFAULTS.DEFAULT_CORS_ORIGINS]),
22
24
  rateLimitWindowMs: config?.rateLimitWindowMs || 15 * 60 * 1000, // 15 minutes
23
- rateLimitMax: config?.rateLimitMax || 100, // Max 100 requests per windowMs
25
+ rateLimitMax: config?.rateLimitMax || DEFAULTS.MAX_EVENTS_LIMIT, // Max 100 requests per windowMs
24
26
  };
25
27
  this.app = express();
26
28
  this.setupMiddleware();
@@ -113,7 +115,7 @@ export class SaaSApiServer {
113
115
  res.status(404).json({
114
116
  success: false,
115
117
  error: {
116
- code: 'NOT_FOUND',
118
+ code: ERROR_CODES.NOT_FOUND,
117
119
  message: `Endpoint ${req.method} ${req.path} not found`,
118
120
  },
119
121
  });
@@ -127,9 +129,9 @@ export class SaaSApiServer {
127
129
  const errorHandler = (err, _req, res, _next) => {
128
130
  console.error('API Error:', err);
129
131
  // Don't leak error details in production
130
- const isDev = process.env.NODE_ENV !== 'production';
132
+ const isDev = process.env[ENV_VARS.NODE_ENV] !== 'production';
131
133
  const statusCode = err.status || 500;
132
- const errorCode = err.code || 'INTERNAL_ERROR';
134
+ const errorCode = err.code || ERROR_CODES.INTERNAL_ERROR;
133
135
  res.status(statusCode).json({
134
136
  success: false,
135
137
  error: {
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { EventEmitter } from 'events';
13
13
  import { createLogger } from './logger.js';
14
+ import { ENV_VARS } from '../constants/index.js';
14
15
  /**
15
16
  * Abstract base class for job managers
16
17
  */
@@ -54,7 +55,7 @@ export class BaseJobManager extends EventEmitter {
54
55
  createdAt: new Date(),
55
56
  env: spec.env,
56
57
  cwd: spec.cwd || process.cwd(),
57
- user: spec.user || process.env.USER,
58
+ user: spec.user || process.env[ENV_VARS.USER],
58
59
  schedule: spec.schedule,
59
60
  tags: spec.tags || [],
60
61
  description: spec.description,
@@ -8,6 +8,7 @@ import { EventEmitter } from 'events';
8
8
  import DatabasePersistence from './database-persistence.js';
9
9
  import { createLogger } from './logger.js';
10
10
  import { getPlatformPaths } from './platform-utils.js';
11
+ import { ENV_VARS } from '../constants/index.js';
11
12
  export class DaemonClient extends EventEmitter {
12
13
  socketPath;
13
14
  socket;
@@ -253,7 +254,7 @@ export class DaemonClient extends EventEmitter {
253
254
  schedule: jobSpec.schedule,
254
255
  env: jobSpec.environment || {},
255
256
  cwd: jobSpec.workingDirectory || process.cwd(),
256
- user: jobSpec.user || process.env.USER,
257
+ user: jobSpec.user || process.env[ENV_VARS.USER],
257
258
  priority: jobSpec.priority || 0,
258
259
  tags: jobSpec.tags || [],
259
260
  enabled: jobSpec.enabled !== false,
@@ -5,6 +5,7 @@
5
5
  import { supabaseClient, isSupabaseConfigured } from './supabase-client.js';
6
6
  import * as os from 'os';
7
7
  import { LocalStorageAdapter } from './local-storage-adapter.js';
8
+ import { ENV_VARS } from '../constants/index.js';
8
9
  export class DatabasePersistence {
9
10
  client;
10
11
  localStorage;
@@ -16,7 +17,7 @@ export class DatabasePersistence {
16
17
  if (this.useLocalStorage) {
17
18
  // Using local storage is normal when Supabase is not configured
18
19
  // Only show this message once per session to avoid noise
19
- if (!process.env.LSH_LOCAL_STORAGE_QUIET) {
20
+ if (!process.env[ENV_VARS.LSH_LOCAL_STORAGE_QUIET]) {
20
21
  console.log('ā„¹ļø Using local storage (Supabase not configured)');
21
22
  }
22
23
  this.localStorage = new LocalStorageAdapter(userId);
@@ -8,6 +8,7 @@ import * as os from 'os';
8
8
  import * as crypto from 'crypto';
9
9
  import { createLogger } from './logger.js';
10
10
  import { getStorachaClient } from './storacha-client.js';
11
+ import { ENV_VARS } from '../constants/index.js';
11
12
  const logger = createLogger('IPFSSecretsStorage');
12
13
  /**
13
14
  * IPFS Secrets Storage
@@ -25,7 +26,7 @@ export class IPFSSecretsStorage {
25
26
  metadataPath;
26
27
  metadata;
27
28
  constructor() {
28
- const homeDir = process.env.HOME || os.homedir();
29
+ const homeDir = process.env[ENV_VARS.HOME] || os.homedir();
29
30
  const lshDir = path.join(homeDir, '.lsh');
30
31
  this.cacheDir = path.join(lshDir, 'secrets-cache');
31
32
  this.metadataPath = path.join(lshDir, 'secrets-metadata.json');
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import * as os from 'os';
8
8
  import * as crypto from 'crypto';
9
9
  import { getGitRepoInfo } from './git-utils.js';
10
+ import { ENV_VARS } from '../constants/index.js';
10
11
  /**
11
12
  * IPFS Sync Logger
12
13
  *
@@ -36,7 +37,7 @@ export class IPFSSyncLogger {
36
37
  * Check if IPFS sync is enabled
37
38
  */
38
39
  isEnabled() {
39
- return process.env.DISABLE_IPFS_SYNC !== 'true';
40
+ return process.env[ENV_VARS.DISABLE_IPFS_SYNC] !== 'true';
40
41
  }
41
42
  /**
42
43
  * Record a sync operation to IPFS
@@ -195,7 +196,7 @@ export class IPFSSyncLogger {
195
196
  * Get encryption key fingerprint
196
197
  */
197
198
  getKeyFingerprint() {
198
- const key = process.env.LSH_SECRETS_KEY || 'default';
199
+ const key = process.env[ENV_VARS.LSH_SECRETS_KEY] || 'default';
199
200
  return `sha256:${crypto.createHash('sha256').update(key).digest('hex').substring(0, 16)}`;
200
201
  }
201
202
  /**
@@ -3,6 +3,7 @@
3
3
  * Centralized logging utility with support for different log levels,
4
4
  * structured logging, and environment-based configuration.
5
5
  */
6
+ import { ENV_VARS } from '../constants/index.js';
6
7
  export var LogLevel;
7
8
  (function (LogLevel) {
8
9
  LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
@@ -41,7 +42,7 @@ const colors = {
41
42
  * Get log level from environment variable
42
43
  */
43
44
  function getLogLevelFromEnv() {
44
- const level = process.env.LSH_LOG_LEVEL?.toUpperCase();
45
+ const level = process.env[ENV_VARS.LSH_LOG_LEVEL]?.toUpperCase();
45
46
  switch (level) {
46
47
  case 'DEBUG':
47
48
  return LogLevel.DEBUG;
@@ -54,7 +55,7 @@ function getLogLevelFromEnv() {
54
55
  case 'NONE':
55
56
  return LogLevel.NONE;
56
57
  default:
57
- return process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG;
58
+ return process.env[ENV_VARS.NODE_ENV] === 'production' ? LogLevel.INFO : LogLevel.DEBUG;
58
59
  }
59
60
  }
60
61
  /**
@@ -66,8 +67,8 @@ export class Logger {
66
67
  this.config = {
67
68
  level: config?.level ?? getLogLevelFromEnv(),
68
69
  enableTimestamp: config?.enableTimestamp ?? true,
69
- enableColors: config?.enableColors ?? (!process.env.NO_COLOR && process.stdout.isTTY),
70
- enableJSON: config?.enableJSON ?? (process.env.LSH_LOG_FORMAT === 'json'),
70
+ enableColors: config?.enableColors ?? (!process.env[ENV_VARS.NO_COLOR] && process.stdout.isTTY),
71
+ enableJSON: config?.enableJSON ?? (process.env[ENV_VARS.LSH_LOG_FORMAT] === 'json'),
71
72
  context: config?.context,
72
73
  };
73
74
  }
@@ -10,6 +10,7 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
12
  import { logger } from './logger.js';
13
+ import { ENV_VARS } from '../constants/index.js';
13
14
  export class LshConfigManager {
14
15
  configPath;
15
16
  config;
@@ -66,7 +67,7 @@ export class LshConfigManager {
66
67
  return this.config.keys[repoName].key;
67
68
  }
68
69
  // Fall back to environment variables
69
- const envKey = process.env.LSH_SECRETS_KEY || process.env.LSH_MASTER_KEY;
70
+ const envKey = process.env[ENV_VARS.LSH_SECRETS_KEY] || process.env[ENV_VARS.LSH_MASTER_KEY];
70
71
  if (envKey) {
71
72
  logger.debug(`Using encryption key from environment for ${repoName}`);
72
73
  return envKey;
@@ -6,13 +6,14 @@ import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import * as os from 'os';
8
8
  import { fileURLToPath } from 'url';
9
+ import { ENV_VARS } from '../constants/index.js';
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
  export class LshrcManager {
12
13
  lshrcPath;
13
14
  constructor(lshrcPath) {
14
- // Use process.env.HOME if set (for testability), fallback to os.homedir()
15
- const homeDir = process.env.HOME || os.homedir();
15
+ // Use process.env[ENV_VARS.HOME] if set (for testability), fallback to os.homedir()
16
+ const homeDir = process.env[ENV_VARS.HOME] || os.homedir();
16
17
  this.lshrcPath = lshrcPath || path.join(homeDir, '.lshrc');
17
18
  }
18
19
  /**
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as path from 'path';
6
6
  import * as os from 'os';
7
+ import { ENV_VARS } from '../constants/index.js';
7
8
  /**
8
9
  * Get platform-specific paths
9
10
  * Handles differences between Windows, macOS, and Linux
@@ -31,7 +32,7 @@ export function getPlatformPaths(appName = 'lsh') {
31
32
  // Linux: ~/.config/lsh
32
33
  let configDir;
33
34
  if (isWindows) {
34
- configDir = path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), appName);
35
+ configDir = path.join(process.env[ENV_VARS.APPDATA] || path.join(homeDir, 'AppData', 'Roaming'), appName);
35
36
  }
36
37
  else if (isMac) {
37
38
  configDir = path.join(homeDir, 'Library', 'Application Support', appName);
@@ -45,7 +46,7 @@ export function getPlatformPaths(appName = 'lsh') {
45
46
  // Linux: ~/.local/share/lsh
46
47
  let dataDir;
47
48
  if (isWindows) {
48
- dataDir = path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), appName);
49
+ dataDir = path.join(process.env[ENV_VARS.LOCALAPPDATA] || path.join(homeDir, 'AppData', 'Local'), appName);
49
50
  }
50
51
  else if (isMac) {
51
52
  dataDir = path.join(homeDir, 'Library', 'Application Support', appName);
@@ -134,9 +135,9 @@ export async function ensureDir(dirPath) {
134
135
  */
135
136
  export function getDefaultShell() {
136
137
  if (isWindows()) {
137
- return process.env.COMSPEC || 'cmd.exe';
138
+ return process.env[ENV_VARS.COMSPEC] || 'cmd.exe';
138
139
  }
139
- return process.env.SHELL || '/bin/sh';
140
+ return process.env[ENV_VARS.SHELL] || '/bin/sh';
140
141
  }
141
142
  /**
142
143
  * Get path separator for current platform
@@ -6,6 +6,7 @@ import { randomBytes } from 'crypto';
6
6
  import bcrypt from 'bcrypt';
7
7
  import jwt from 'jsonwebtoken';
8
8
  import { getSupabaseClient } from './supabase-client.js';
9
+ import { ENV_VARS } from '../constants/index.js';
9
10
  const BCRYPT_ROUNDS = 12;
10
11
  const TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days in seconds
11
12
  const REFRESH_TOKEN_EXPIRY = 30 * 24 * 60 * 60; // 30 days
@@ -32,7 +33,7 @@ export async function verifyPassword(password, hash) {
32
33
  * Generate JWT access token
33
34
  */
34
35
  export function generateAccessToken(userId, email) {
35
- const secret = process.env.LSH_JWT_SECRET;
36
+ const secret = process.env[ENV_VARS.LSH_JWT_SECRET];
36
37
  if (!secret) {
37
38
  throw new Error('LSH_JWT_SECRET is not set');
38
39
  }
@@ -50,7 +51,7 @@ export function generateAccessToken(userId, email) {
50
51
  * Generate JWT refresh token
51
52
  */
52
53
  export function generateRefreshToken(userId) {
53
- const secret = process.env.LSH_JWT_SECRET;
54
+ const secret = process.env[ENV_VARS.LSH_JWT_SECRET];
54
55
  if (!secret) {
55
56
  throw new Error('LSH_JWT_SECRET is not set');
56
57
  }
@@ -67,7 +68,7 @@ export function generateRefreshToken(userId) {
67
68
  * Verify and decode JWT token
68
69
  */
69
70
  export function verifyToken(token) {
70
- const secret = process.env.LSH_JWT_SECRET;
71
+ const secret = process.env[ENV_VARS.LSH_JWT_SECRET];
71
72
  if (!secret) {
72
73
  throw new Error('LSH_JWT_SECRET is not set');
73
74
  }
@@ -4,22 +4,23 @@
4
4
  */
5
5
  import { getSupabaseClient } from './supabase-client.js';
6
6
  import { auditLogger } from './saas-audit.js';
7
+ import { ENV_VARS } from '../constants/index.js';
7
8
  /**
8
9
  * Stripe Pricing IDs (set via environment variables)
9
10
  */
10
11
  export const STRIPE_PRICE_IDS = {
11
- pro_monthly: process.env.STRIPE_PRICE_PRO_MONTHLY || '',
12
- pro_yearly: process.env.STRIPE_PRICE_PRO_YEARLY || '',
13
- enterprise_monthly: process.env.STRIPE_PRICE_ENTERPRISE_MONTHLY || '',
14
- enterprise_yearly: process.env.STRIPE_PRICE_ENTERPRISE_YEARLY || '',
12
+ pro_monthly: process.env[ENV_VARS.STRIPE_PRICE_PRO_MONTHLY] || '',
13
+ pro_yearly: process.env[ENV_VARS.STRIPE_PRICE_PRO_YEARLY] || '',
14
+ enterprise_monthly: process.env[ENV_VARS.STRIPE_PRICE_ENTERPRISE_MONTHLY] || '',
15
+ enterprise_yearly: process.env[ENV_VARS.STRIPE_PRICE_ENTERPRISE_YEARLY] || '',
15
16
  };
16
17
  /**
17
18
  * Billing Service
18
19
  */
19
20
  export class BillingService {
20
21
  supabase = getSupabaseClient();
21
- stripeSecretKey = process.env.STRIPE_SECRET_KEY || '';
22
- stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET || '';
22
+ stripeSecretKey = process.env[ENV_VARS.STRIPE_SECRET_KEY] || '';
23
+ stripeWebhookSecret = process.env[ENV_VARS.STRIPE_WEBHOOK_SECRET] || '';
23
24
  stripeApiUrl = 'https://api.stripe.com/v1';
24
25
  /**
25
26
  * Create Stripe customer
@@ -2,6 +2,7 @@
2
2
  * LSH SaaS Email Service
3
3
  * Email sending using Resend API
4
4
  */
5
+ import { ENV_VARS } from '../constants/index.js';
5
6
  /**
6
7
  * Email Service
7
8
  */
@@ -10,10 +11,10 @@ export class EmailService {
10
11
  resendApiUrl = 'https://api.resend.com/emails';
11
12
  constructor(config) {
12
13
  this.config = {
13
- apiKey: config?.apiKey || process.env.RESEND_API_KEY || '',
14
- fromEmail: config?.fromEmail || process.env.EMAIL_FROM || 'noreply@lsh.dev',
14
+ apiKey: config?.apiKey || process.env[ENV_VARS.RESEND_API_KEY] || '',
15
+ fromEmail: config?.fromEmail || process.env[ENV_VARS.EMAIL_FROM] || 'noreply@lsh.dev',
15
16
  fromName: config?.fromName || 'LSH Secrets Manager',
16
- baseUrl: config?.baseUrl || process.env.BASE_URL || 'https://app.lsh.dev',
17
+ baseUrl: config?.baseUrl || process.env[ENV_VARS.BASE_URL] || 'https://app.lsh.dev',
17
18
  };
18
19
  if (!this.config.apiKey) {
19
20
  console.warn('RESEND_API_KEY not set - emails will not be sent');
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { randomBytes, createCipheriv, createDecipheriv, createHash, pbkdf2Sync } from 'crypto';
6
6
  import { getSupabaseClient } from './supabase-client.js';
7
+ import { ENV_VARS } from '../constants/index.js';
7
8
  const ALGORITHM = 'aes-256-cbc';
8
9
  const KEY_LENGTH = 32; // 256 bits
9
10
  const IV_LENGTH = 16; // 128 bits
@@ -14,7 +15,7 @@ const PBKDF2_ITERATIONS = 100000;
14
15
  * This key is used to encrypt/decrypt team encryption keys
15
16
  */
16
17
  function getMasterKey() {
17
- const masterKeyHex = process.env.LSH_MASTER_KEY || process.env.LSH_SECRETS_KEY;
18
+ const masterKeyHex = process.env[ENV_VARS.LSH_MASTER_KEY] || process.env[ENV_VARS.LSH_SECRETS_KEY];
18
19
  if (!masterKeyHex) {
19
20
  throw new Error('LSH_MASTER_KEY or LSH_SECRETS_KEY environment variable must be set for encryption');
20
21
  }
@@ -9,6 +9,7 @@ import { createLogger, LogLevel } from './logger.js';
9
9
  import { getGitRepoInfo, hasEnvExample, ensureEnvInGitignore } from './git-utils.js';
10
10
  import { IPFSSyncLogger } from './ipfs-sync-logger.js';
11
11
  import { IPFSSecretsStorage } from './ipfs-secrets-storage.js';
12
+ import { ENV_VARS } from '../constants/index.js';
12
13
  const logger = createLogger('SecretsManager');
13
14
  export class SecretsManager {
14
15
  storage;
@@ -18,7 +19,7 @@ export class SecretsManager {
18
19
  homeDir;
19
20
  constructor(userIdOrOptions, encryptionKey, detectGit) {
20
21
  this.storage = new IPFSSecretsStorage();
21
- this.homeDir = process.env.HOME || process.env.USERPROFILE || '';
22
+ this.homeDir = process.env[ENV_VARS.HOME] || process.env[ENV_VARS.USERPROFILE] || '';
22
23
  // Handle both legacy and new constructor signatures
23
24
  let options;
24
25
  if (typeof userIdOrOptions === 'object') {
@@ -73,12 +74,13 @@ export class SecretsManager {
73
74
  */
74
75
  getDefaultEncryptionKey() {
75
76
  // Check for explicit key
76
- if (process.env.LSH_SECRETS_KEY) {
77
- return process.env.LSH_SECRETS_KEY;
77
+ const envKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
78
+ if (envKey) {
79
+ return envKey;
78
80
  }
79
81
  // Generate from machine ID and user
80
- const machineId = process.env.HOSTNAME || 'localhost';
81
- const user = process.env.USER || 'unknown';
82
+ const machineId = process.env[ENV_VARS.HOSTNAME] || 'localhost';
83
+ const user = process.env[ENV_VARS.USER] || 'unknown';
82
84
  const seed = `${machineId}-${user}-lsh-secrets`;
83
85
  // Create deterministic key
84
86
  return crypto.createHash('sha256').update(seed).digest('hex');
@@ -242,7 +244,7 @@ export class SecretsManager {
242
244
  // Get the effective environment name (repo-aware)
243
245
  const effectiveEnv = this.getRepoAwareEnvironment(environment);
244
246
  // Warn if using default key
245
- if (!process.env.LSH_SECRETS_KEY) {
247
+ if (!process.env[ENV_VARS.LSH_SECRETS_KEY]) {
246
248
  logger.warn('āš ļø Warning: No LSH_SECRETS_KEY set. Using machine-specific key.');
247
249
  logger.warn(' To share secrets across machines, generate a key with: lsh key');
248
250
  logger.warn(' Then add LSH_SECRETS_KEY=<key> to your .env on all machines');
@@ -428,7 +430,7 @@ export class SecretsManager {
428
430
  cloudExists: false,
429
431
  cloudKeys: 0,
430
432
  cloudModified: undefined,
431
- keySet: !!process.env.LSH_SECRETS_KEY,
433
+ keySet: !!process.env[ENV_VARS.LSH_SECRETS_KEY],
432
434
  keyMatches: undefined,
433
435
  suggestions: [],
434
436
  };
@@ -476,7 +478,7 @@ export class SecretsManager {
476
478
  return 'dev';
477
479
  }
478
480
  // Check for v1 compatibility mode
479
- if (process.env.LSH_V1_COMPAT === 'true') {
481
+ if (process.env[ENV_VARS.LSH_V1_COMPAT] === 'true') {
480
482
  return 'dev'; // v1.x behavior
481
483
  }
482
484
  // v2.0 behavior: use repo name as default in git repos
@@ -516,7 +518,7 @@ export class SecretsManager {
516
518
  * Generate encryption key if not set
517
519
  */
518
520
  async ensureEncryptionKey() {
519
- if (process.env.LSH_SECRETS_KEY) {
521
+ if (process.env[ENV_VARS.LSH_SECRETS_KEY]) {
520
522
  return true; // Key already set
521
523
  }
522
524
  logger.warn('āš ļø No encryption key found. Generating a new key...');
@@ -540,7 +542,7 @@ export class SecretsManager {
540
542
  }
541
543
  fs.writeFileSync(envPath, content, 'utf8');
542
544
  // Set in current process
543
- process.env.LSH_SECRETS_KEY = key;
545
+ process.env[ENV_VARS.LSH_SECRETS_KEY] = key;
544
546
  this.encryptionKey = key;
545
547
  logger.info('āœ… Generated and saved encryption key to .env');
546
548
  logger.info('šŸ’” Load it now: export LSH_SECRETS_KEY=' + key.substring(0, 8) + '...');
@@ -677,7 +679,7 @@ LSH_SECRETS_KEY=${this.encryptionKey}
677
679
  out();
678
680
  }
679
681
  // Step 1: Ensure encryption key exists
680
- if (!process.env.LSH_SECRETS_KEY) {
682
+ if (!process.env[ENV_VARS.LSH_SECRETS_KEY]) {
681
683
  logger.info('šŸ”‘ No encryption key found...');
682
684
  await this.ensureEncryptionKey();
683
685
  out();
@@ -7,6 +7,7 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { homedir } from 'os';
9
9
  import { logger } from './logger.js';
10
+ import { ENV_VARS } from '../constants/index.js';
10
11
  export class StorachaClient {
11
12
  client = null;
12
13
  configPath;
@@ -35,7 +36,7 @@ export class StorachaClient {
35
36
  logger.warn('Failed to load Storacha config, using defaults');
36
37
  }
37
38
  // Default to enabled unless explicitly disabled
38
- const envDisabled = process.env.LSH_STORACHA_ENABLED === 'false';
39
+ const envDisabled = process.env[ENV_VARS.LSH_STORACHA_ENABLED] === 'false';
39
40
  return {
40
41
  enabled: !envDisabled,
41
42
  };
@@ -57,7 +58,7 @@ export class StorachaClient {
57
58
  */
58
59
  isEnabled() {
59
60
  // Explicitly disabled via env var
60
- if (process.env.LSH_STORACHA_ENABLED === 'false') {
61
+ if (process.env[ENV_VARS.LSH_STORACHA_ENABLED] === 'false') {
61
62
  return false;
62
63
  }
63
64
  // Use config setting (defaults to true)
@@ -3,13 +3,14 @@
3
3
  * Provides database connectivity for LSH features
4
4
  */
5
5
  import { createClient } from '@supabase/supabase-js';
6
+ import { ENV_VARS } from '../constants/index.js';
6
7
  export class SupabaseClient {
7
8
  client;
8
9
  config;
9
10
  constructor(config) {
10
- const url = config?.url || process.env.SUPABASE_URL;
11
- const anonKey = config?.anonKey || process.env.SUPABASE_ANON_KEY;
12
- const databaseUrl = config?.databaseUrl || process.env.DATABASE_URL;
11
+ const url = config?.url || process.env[ENV_VARS.SUPABASE_URL];
12
+ const anonKey = config?.anonKey || process.env[ENV_VARS.SUPABASE_ANON_KEY];
13
+ const databaseUrl = config?.databaseUrl || process.env[ENV_VARS.DATABASE_URL];
13
14
  if (!url || !anonKey) {
14
15
  throw new Error('Supabase configuration missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.');
15
16
  }
@@ -76,7 +77,7 @@ function getDefaultClient() {
76
77
  * Check if Supabase is configured and available
77
78
  */
78
79
  export function isSupabaseConfigured() {
79
- return !!(process.env.SUPABASE_URL && process.env.SUPABASE_ANON_KEY);
80
+ return !!(process.env[ENV_VARS.SUPABASE_URL] && process.env[ENV_VARS.SUPABASE_ANON_KEY]);
80
81
  }
81
82
  export const supabaseClient = {
82
83
  getClient() {
@@ -114,8 +115,8 @@ export const supabaseClient = {
114
115
  * @throws {Error} If SUPABASE_URL or SUPABASE_ANON_KEY are not set
115
116
  */
116
117
  export function getSupabaseClient() {
117
- const url = process.env.SUPABASE_URL;
118
- const key = process.env.SUPABASE_ANON_KEY;
118
+ const url = process.env[ENV_VARS.SUPABASE_URL];
119
+ const key = process.env[ENV_VARS.SUPABASE_ANON_KEY];
119
120
  if (!url || !key) {
120
121
  throw new Error('Supabase configuration missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.');
121
122
  }
@@ -7,6 +7,7 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as readline from 'readline';
9
9
  import { getGitRepoInfo } from '../../lib/git-utils.js';
10
+ import { ENV_VARS } from '../../constants/index.js';
10
11
  export async function init_secrets(program) {
11
12
  // Push secrets to cloud
12
13
  program
@@ -222,10 +223,12 @@ export async function init_secrets(program) {
222
223
  .command('create')
223
224
  .description('Create a new .env file')
224
225
  .option('-f, --file <path>', 'Path to .env file', '.env')
226
+ .option('-g, --global', 'Use global workspace ($HOME)')
225
227
  .option('-t, --template', 'Create with common template variables')
226
228
  .action(async (options) => {
227
229
  try {
228
- const envPath = path.resolve(options.file);
230
+ const manager = new SecretsManager({ globalMode: options.global });
231
+ const envPath = path.resolve(manager.resolveFilePath(options.file));
229
232
  // Check if file already exists
230
233
  if (fs.existsSync(envPath)) {
231
234
  console.log(`āŒ File already exists: ${envPath}`);
@@ -882,6 +885,7 @@ API_KEY=
882
885
  program
883
886
  .command('clear')
884
887
  .description('Clear local metadata and cache to resolve stuck registries')
888
+ .option('-g, --global', 'Use global workspace ($HOME) - default behavior')
885
889
  .option('--repo <name>', 'Clear metadata for specific repo only')
886
890
  .option('--cache', 'Also clear local encrypted secrets cache')
887
891
  .option('--storacha', 'Also delete old Storacha uploads (registries and secrets)')
@@ -889,7 +893,7 @@ API_KEY=
889
893
  .option('-y, --yes', 'Skip confirmation prompts')
890
894
  .action(async (options) => {
891
895
  try {
892
- const lshDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.lsh');
896
+ const lshDir = path.join(process.env[ENV_VARS.HOME] || process.env[ENV_VARS.USERPROFILE] || '', '.lsh');
893
897
  const metadataPath = path.join(lshDir, 'secrets-metadata.json');
894
898
  const cacheDir = path.join(lshDir, 'secrets-cache');
895
899
  // Determine what we're clearing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "Simple, cross-platform encrypted secrets manager with automatic sync, IPFS audit logs, and multi-environment support. Just run lsh sync and start managing your secrets.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "scripts": {
18
18
  "build": "tsc",
19
19
  "watch": "tsc --watch",
20
- "test": "node --experimental-vm-modules ./node_modules/.bin/jest",
20
+ "test": "node --experimental-vm-modules ./node_modules/.bin/jest --runInBand",
21
21
  "test:ci": "node --experimental-vm-modules ./node_modules/.bin/jest --runInBand",
22
22
  "test:coverage": "node --experimental-vm-modules ./node_modules/.bin/jest --coverage --runInBand",
23
23
  "clean": "rm -rf ./build; rm -rf ./bin; rm -rf ./dist",