lsh-framework 3.1.1 → 3.1.3

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.
Files changed (36) hide show
  1. package/dist/commands/config.js +2 -1
  2. package/dist/commands/self.js +4 -3
  3. package/dist/constants/api.js +34 -1
  4. package/dist/constants/config.js +68 -1
  5. package/dist/constants/database.js +30 -0
  6. package/dist/constants/errors.js +24 -0
  7. package/dist/constants/paths.js +3 -0
  8. package/dist/constants/ui.js +80 -0
  9. package/dist/daemon/job-registry.js +7 -6
  10. package/dist/daemon/lshd.js +14 -15
  11. package/dist/daemon/saas-api-server.js +9 -7
  12. package/dist/lib/base-job-manager.js +2 -1
  13. package/dist/lib/cloud-config-manager.js +2 -1
  14. package/dist/lib/cron-job-manager.js +5 -4
  15. package/dist/lib/daemon-client.js +3 -2
  16. package/dist/lib/database-persistence.js +2 -1
  17. package/dist/lib/enhanced-history-system.js +3 -2
  18. package/dist/lib/history-system.js +2 -1
  19. package/dist/lib/ipfs-secrets-storage.js +2 -1
  20. package/dist/lib/ipfs-sync-logger.js +3 -2
  21. package/dist/lib/logger.js +5 -4
  22. package/dist/lib/lsh-config.js +2 -1
  23. package/dist/lib/lshrc-init.js +3 -2
  24. package/dist/lib/platform-utils.js +5 -4
  25. package/dist/lib/saas-auth.js +4 -3
  26. package/dist/lib/saas-billing.js +7 -6
  27. package/dist/lib/saas-email.js +4 -3
  28. package/dist/lib/saas-encryption.js +2 -1
  29. package/dist/lib/saas-organizations.js +4 -3
  30. package/dist/lib/saas-secrets.js +2 -1
  31. package/dist/lib/secrets-manager.js +13 -11
  32. package/dist/lib/storacha-client.js +3 -2
  33. package/dist/lib/supabase-client.js +7 -6
  34. package/dist/services/secrets/secrets.js +2 -1
  35. package/dist/services/supabase/supabase-registrar.js +4 -3
  36. package/package.json +1 -1
@@ -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
@@ -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,72 @@ 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
+ // Cloud sync intervals
113
+ CLOUD_CONFIG_SYNC_INTERVAL_MS: 60000, // 1 minute
114
+ HISTORY_SYNC_INTERVAL_MS: 30000, // 30 seconds
115
+ // Job registry
116
+ MAX_RECORDS_PER_JOB: 1000,
117
+ MAX_TOTAL_RECORDS: 50000,
118
+ METRICS_RETENTION_DAYS: 90,
119
+ // Shell defaults
120
+ DEFAULT_SHELL_UNIX: '/bin/sh',
121
+ DEFAULT_SHELL_WIN: 'cmd.exe',
122
+ DEFAULT_EDITOR: 'vi',
56
123
  };
@@ -14,8 +14,38 @@ 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',
20
41
  DATA_PULL_JOBS: 'data_pull_jobs',
42
+ // ML tables
43
+ ML_TRAINING_JOBS: 'ml_training_jobs',
44
+ ML_MODELS: 'ml_models',
45
+ ML_FEATURES: 'ml_features',
46
+ // Views (read-only aggregations)
47
+ ORGANIZATION_MEMBERS_DETAILED: 'organization_members_detailed',
48
+ ORGANIZATION_USAGE_SUMMARY: 'organization_usage_summary',
49
+ TEAM_MEMBERS_DETAILED: 'team_members_detailed',
50
+ SECRETS_SUMMARY: 'secrets_summary',
21
51
  };
@@ -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, DEFAULTS, PATHS } from '../constants/index.js';
14
15
  export class JobRegistry extends BaseJobManager {
15
16
  config;
16
17
  records = new Map(); // jobId -> execution records
@@ -19,12 +20,12 @@ export class JobRegistry extends BaseJobManager {
19
20
  constructor(config) {
20
21
  super(new MemoryJobStorage(), 'JobRegistry');
21
22
  this.config = {
22
- registryFile: '/tmp/lsh-job-registry.json',
23
- maxRecordsPerJob: 1000,
24
- maxTotalRecords: 50000,
23
+ registryFile: PATHS.JOB_REGISTRY_FILE,
24
+ maxRecordsPerJob: DEFAULTS.MAX_RECORDS_PER_JOB,
25
+ maxTotalRecords: DEFAULTS.MAX_TOTAL_RECORDS,
25
26
  compressionEnabled: true,
26
- metricsRetentionDays: 90,
27
- outputLogDir: '/tmp/lsh-job-logs',
27
+ metricsRetentionDays: DEFAULTS.METRICS_RETENTION_DAYS,
28
+ outputLogDir: PATHS.JOB_LOGS_DIR,
28
29
  indexingEnabled: true,
29
30
  ...config
30
31
  };
@@ -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,
@@ -6,6 +6,7 @@ import DatabasePersistence from './database-persistence.js';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as os from 'os';
9
+ import { DEFAULTS } from '../constants/index.js';
9
10
  export class CloudConfigManager {
10
11
  databasePersistence;
11
12
  options;
@@ -17,7 +18,7 @@ export class CloudConfigManager {
17
18
  userId: undefined,
18
19
  enableCloudSync: true,
19
20
  localConfigPath: path.join(os.homedir(), '.lshrc'),
20
- syncInterval: 60000, // 1 minute
21
+ syncInterval: DEFAULTS.CLOUD_CONFIG_SYNC_INTERVAL_MS,
21
22
  ...options,
22
23
  };
23
24
  this.databasePersistence = new DatabasePersistence(this.options.userId);
@@ -8,6 +8,7 @@ import { BaseJobManager, } from './base-job-manager.js';
8
8
  import DatabaseJobStorage from './job-storage-database.js';
9
9
  import DaemonClient from './daemon-client.js';
10
10
  import DatabasePersistence from './database-persistence.js';
11
+ import { DEFAULTS } from '../constants/index.js';
11
12
  export class CronJobManager extends BaseJobManager {
12
13
  daemonClient;
13
14
  databasePersistence;
@@ -36,7 +37,7 @@ export class CronJobManager extends BaseJobManager {
36
37
  workingDirectory: '/backups',
37
38
  priority: 8,
38
39
  maxRetries: 3,
39
- timeout: 3600000, // 1 hour
40
+ timeout: DEFAULTS.JOB_TIMEOUT_1H_MS,
40
41
  },
41
42
  {
42
43
  id: 'log-cleanup',
@@ -48,7 +49,7 @@ export class CronJobManager extends BaseJobManager {
48
49
  tags: ['logs', 'cleanup', 'weekly'],
49
50
  priority: 3,
50
51
  maxRetries: 2,
51
- timeout: 300000, // 5 minutes
52
+ timeout: DEFAULTS.JOB_TIMEOUT_5M_MS,
52
53
  },
53
54
  {
54
55
  id: 'disk-monitor',
@@ -60,7 +61,7 @@ export class CronJobManager extends BaseJobManager {
60
61
  tags: ['monitoring', 'disk', 'alert'],
61
62
  priority: 7,
62
63
  maxRetries: 1,
63
- timeout: 60000, // 1 minute
64
+ timeout: DEFAULTS.JOB_TIMEOUT_1M_MS,
64
65
  },
65
66
  {
66
67
  id: 'data-sync',
@@ -73,7 +74,7 @@ export class CronJobManager extends BaseJobManager {
73
74
  workingDirectory: '/data',
74
75
  priority: 6,
75
76
  maxRetries: 5,
76
- timeout: 7200000, // 2 hours
77
+ timeout: DEFAULTS.JOB_TIMEOUT_2H_MS,
77
78
  },
78
79
  ];
79
80
  templates.forEach(template => {
@@ -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, DEFAULTS } from '../constants/index.js';
11
12
  export class DaemonClient extends EventEmitter {
12
13
  socketPath;
13
14
  socket;
@@ -66,7 +67,7 @@ export class DaemonClient extends EventEmitter {
66
67
  resolve(true);
67
68
  });
68
69
  let buffer = '';
69
- const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB limit
70
+ const MAX_BUFFER_SIZE = DEFAULTS.MAX_BUFFER_SIZE_BYTES;
70
71
  this.socket.on('data', (data) => {
71
72
  try {
72
73
  buffer += data.toString();
@@ -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);
@@ -6,6 +6,7 @@ import HistorySystem from './history-system.js';
6
6
  import DatabasePersistence from './database-persistence.js';
7
7
  import * as os from 'os';
8
8
  import * as path from 'path';
9
+ import { DEFAULTS } from '../constants/index.js';
9
10
  export class EnhancedHistorySystem extends HistorySystem {
10
11
  databasePersistence;
11
12
  enhancedConfig;
@@ -13,7 +14,7 @@ export class EnhancedHistorySystem extends HistorySystem {
13
14
  pendingSync = false;
14
15
  constructor(config = {}) {
15
16
  const defaultConfig = {
16
- maxSize: 10000,
17
+ maxSize: DEFAULTS.MAX_HISTORY_SIZE,
17
18
  filePath: path.join(os.homedir(), '.lsh_history'),
18
19
  shareHistory: true,
19
20
  ignoreDups: true,
@@ -21,7 +22,7 @@ export class EnhancedHistorySystem extends HistorySystem {
21
22
  expireDuplicatesFirst: true,
22
23
  enableCloudSync: true,
23
24
  userId: undefined,
24
- syncInterval: 30000, // 30 seconds
25
+ syncInterval: DEFAULTS.HISTORY_SYNC_INTERVAL_MS,
25
26
  ...config,
26
27
  };
27
28
  super(defaultConfig);
@@ -5,6 +5,7 @@
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import * as os from 'os';
8
+ import { DEFAULTS } from '../constants/index.js';
8
9
  export class HistorySystem {
9
10
  entries = [];
10
11
  currentIndex = -1;
@@ -12,7 +13,7 @@ export class HistorySystem {
12
13
  isEnabled = true;
13
14
  constructor(config) {
14
15
  this.config = {
15
- maxSize: 10000,
16
+ maxSize: DEFAULTS.MAX_HISTORY_SIZE,
16
17
  filePath: path.join(os.homedir(), '.lsh_history'),
17
18
  shareHistory: false,
18
19
  ignoreDups: true,
@@ -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
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { getSupabaseClient } from './supabase-client.js';
6
6
  import { auditLogger } from './saas-audit.js';
7
+ import { TABLES } from '../constants/index.js';
7
8
  /**
8
9
  * Generate URL-friendly slug from name
9
10
  */
@@ -232,7 +233,7 @@ export class OrganizationService {
232
233
  */
233
234
  async getOrganizationMembers(organizationId) {
234
235
  const { data, error } = await this.supabase
235
- .from('organization_members_detailed')
236
+ .from(TABLES.ORGANIZATION_MEMBERS_DETAILED)
236
237
  .select('*')
237
238
  .eq('organization_id', organizationId);
238
239
  if (error) {
@@ -270,7 +271,7 @@ export class OrganizationService {
270
271
  */
271
272
  async getUsageSummary(organizationId) {
272
273
  const { data, error } = await this.supabase
273
- .from('organization_usage_summary')
274
+ .from(TABLES.ORGANIZATION_USAGE_SUMMARY)
274
275
  .select('*')
275
276
  .eq('organization_id', organizationId)
276
277
  .single();
@@ -550,7 +551,7 @@ export class TeamService {
550
551
  */
551
552
  async getTeamMembers(teamId) {
552
553
  const { data, error } = await this.supabase
553
- .from('team_members_detailed')
554
+ .from(TABLES.TEAM_MEMBERS_DETAILED)
554
555
  .select('*')
555
556
  .eq('team_id', teamId);
556
557
  if (error) {
@@ -7,6 +7,7 @@ import { getSupabaseClient } from './supabase-client.js';
7
7
  import { encryptionService } from './saas-encryption.js';
8
8
  import { auditLogger } from './saas-audit.js';
9
9
  import { organizationService } from './saas-organizations.js';
10
+ import { TABLES } from '../constants/index.js';
10
11
  /**
11
12
  * Secrets Service
12
13
  */
@@ -211,7 +212,7 @@ export class SecretsService {
211
212
  */
212
213
  async getSecretsSummary(teamId) {
213
214
  const { data, error } = await this.supabase
214
- .from('secrets_summary')
215
+ .from(TABLES.SECRETS_SUMMARY)
215
216
  .select('*')
216
217
  .eq('team_id', teamId);
217
218
  if (error) {
@@ -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
@@ -892,7 +893,7 @@ API_KEY=
892
893
  .option('-y, --yes', 'Skip confirmation prompts')
893
894
  .action(async (options) => {
894
895
  try {
895
- 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');
896
897
  const metadataPath = path.join(lshDir, 'secrets-metadata.json');
897
898
  const cacheDir = path.join(lshDir, 'secrets-cache');
898
899
  // Determine what we're clearing
@@ -7,6 +7,7 @@ import { supabaseClient } from '../../lib/supabase-client.js';
7
7
  import DatabasePersistence from '../../lib/database-persistence.js';
8
8
  import CloudConfigManager from '../../lib/cloud-config-manager.js';
9
9
  import { CREATE_TABLES_SQL } from '../../lib/database-schema.js';
10
+ import { TABLES } from '../../constants/index.js';
10
11
  export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
11
12
  constructor() {
12
13
  super('SupabaseService');
@@ -259,7 +260,7 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
259
260
  const opts = options;
260
261
  if (opts.list) {
261
262
  let query = supabaseClient.getClient()
262
- .from('ml_training_jobs')
263
+ .from(TABLES.ML_TRAINING_JOBS)
263
264
  .select('*')
264
265
  .order('created_at', { ascending: false });
265
266
  if (opts.status) {
@@ -315,7 +316,7 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
315
316
  const opts = options;
316
317
  if (opts.list) {
317
318
  let query = supabaseClient.getClient()
318
- .from('ml_models')
319
+ .from(TABLES.ML_MODELS)
319
320
  .select('*')
320
321
  .order('created_at', { ascending: false });
321
322
  if (opts.deployed) {
@@ -351,7 +352,7 @@ export class SupabaseCommandRegistrar extends BaseCommandRegistrar {
351
352
  const opts = options;
352
353
  if (opts.list) {
353
354
  const { data: features, error } = await supabaseClient.getClient()
354
- .from('ml_features')
355
+ .from(TABLES.ML_FEATURES)
355
356
  .select('*')
356
357
  .order('created_at', { ascending: false })
357
358
  .limit(20);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
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": {