lsh-framework 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,225 @@
1
+ /**
2
+ * LSH SaaS API Server
3
+ * Express-based RESTful API for the SaaS platform
4
+ */
5
+ import express from 'express';
6
+ import cors from 'cors';
7
+ import rateLimit from 'express-rate-limit';
8
+ import { setupSaaSApiRoutes } from './saas-api-routes.js';
9
+ /**
10
+ * SaaS API Server
11
+ */
12
+ export class SaaSApiServer {
13
+ app;
14
+ config;
15
+ server;
16
+ constructor(config) {
17
+ this.config = {
18
+ port: config?.port || parseInt(process.env.LSH_SAAS_API_PORT || '3031'),
19
+ host: config?.host || process.env.LSH_SAAS_API_HOST || '0.0.0.0',
20
+ corsOrigins: config?.corsOrigins || (process.env.LSH_CORS_ORIGINS?.split(',') || ['*']),
21
+ rateLimitWindowMs: config?.rateLimitWindowMs || 15 * 60 * 1000, // 15 minutes
22
+ rateLimitMax: config?.rateLimitMax || 100, // Max 100 requests per windowMs
23
+ };
24
+ this.app = express();
25
+ this.setupMiddleware();
26
+ this.setupRoutes();
27
+ this.setupErrorHandlers();
28
+ }
29
+ /**
30
+ * Setup middleware
31
+ */
32
+ setupMiddleware() {
33
+ // CORS
34
+ this.app.use(cors({
35
+ origin: this.config.corsOrigins,
36
+ credentials: true,
37
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
38
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
39
+ }));
40
+ // Rate limiting - global limit for all requests
41
+ const limiter = rateLimit({
42
+ windowMs: this.config.rateLimitWindowMs,
43
+ max: this.config.rateLimitMax,
44
+ message: {
45
+ success: false,
46
+ error: {
47
+ code: 'RATE_LIMIT_EXCEEDED',
48
+ message: 'Too many requests, please try again later',
49
+ },
50
+ },
51
+ standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
52
+ legacyHeaders: false, // Disable `X-RateLimit-*` headers
53
+ });
54
+ this.app.use(limiter);
55
+ // Body parsers
56
+ this.app.use(express.json({ limit: '10mb' }));
57
+ this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
58
+ // Security headers
59
+ this.app.use((req, res, next) => {
60
+ res.setHeader('X-Content-Type-Options', 'nosniff');
61
+ res.setHeader('X-Frame-Options', 'DENY');
62
+ res.setHeader('X-XSS-Protection', '1; mode=block');
63
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
64
+ // CSP
65
+ res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'");
66
+ next();
67
+ });
68
+ // Request logging
69
+ this.app.use((req, res, next) => {
70
+ const start = Date.now();
71
+ res.on('finish', () => {
72
+ const duration = Date.now() - start;
73
+ // Sanitize path to prevent log injection attacks
74
+ const sanitizedPath = req.path.replace(/[\r\n]/g, '');
75
+ const sanitizedIp = (req.ip || 'unknown').replace(/[\r\n]/g, '');
76
+ console.log(`${req.method} ${sanitizedPath} ${res.statusCode} - ${duration}ms - ${sanitizedIp}`);
77
+ });
78
+ next();
79
+ });
80
+ }
81
+ /**
82
+ * Setup routes
83
+ */
84
+ setupRoutes() {
85
+ // Health check
86
+ this.app.get('/health', (req, res) => {
87
+ res.json({
88
+ status: 'ok',
89
+ timestamp: new Date().toISOString(),
90
+ uptime: process.uptime(),
91
+ });
92
+ });
93
+ // API version
94
+ this.app.get('/api/v1', (req, res) => {
95
+ res.json({
96
+ name: 'LSH SaaS API',
97
+ version: '1.0.0',
98
+ endpoints: {
99
+ auth: '/api/v1/auth/*',
100
+ organizations: '/api/v1/organizations/*',
101
+ teams: '/api/v1/organizations/:id/teams/*',
102
+ secrets: '/api/v1/teams/:id/secrets/*',
103
+ billing: '/api/v1/organizations/:id/billing/*',
104
+ audit: '/api/v1/organizations/:id/audit-logs',
105
+ },
106
+ });
107
+ });
108
+ // Setup SaaS routes
109
+ setupSaaSApiRoutes(this.app);
110
+ // 404 handler
111
+ this.app.use((req, res) => {
112
+ res.status(404).json({
113
+ success: false,
114
+ error: {
115
+ code: 'NOT_FOUND',
116
+ message: `Endpoint ${req.method} ${req.path} not found`,
117
+ },
118
+ });
119
+ });
120
+ }
121
+ /**
122
+ * Setup error handlers
123
+ */
124
+ setupErrorHandlers() {
125
+ // Global error handler
126
+ this.app.use((err, req, res, _next) => {
127
+ console.error('API Error:', err);
128
+ // Don't leak error details in production
129
+ const isDev = process.env.NODE_ENV !== 'production';
130
+ res.status(err.status || 500).json({
131
+ success: false,
132
+ error: {
133
+ code: err.code || 'INTERNAL_ERROR',
134
+ message: isDev ? err.message : 'Internal server error',
135
+ details: isDev ? err.stack : undefined,
136
+ },
137
+ });
138
+ });
139
+ }
140
+ /**
141
+ * Start the server
142
+ */
143
+ async start() {
144
+ return new Promise((resolve, reject) => {
145
+ try {
146
+ this.server = this.app.listen(this.config.port, this.config.host, () => {
147
+ console.log(`
148
+ ╔═══════════════════════════════════════════════════════════════╗
149
+ ║ ║
150
+ ║ 🔐 LSH SaaS API Server ║
151
+ ║ ║
152
+ ║ Status: Running ║
153
+ ║ Port: ${this.config.port.toString().padEnd(49)} ║
154
+ ║ Host: ${this.config.host?.padEnd(49)} ║
155
+ ║ ║
156
+ ║ Endpoints: ║
157
+ ║ - Health: http://${this.config.host}:${this.config.port}/health${' '.repeat(26)} ║
158
+ ║ - API: http://${this.config.host}:${this.config.port}/api/v1${' '.repeat(27)} ║
159
+ ║ ║
160
+ ╚═══════════════════════════════════════════════════════════════╝
161
+ `);
162
+ resolve();
163
+ });
164
+ this.server.on('error', (error) => {
165
+ if (error.code === 'EADDRINUSE') {
166
+ reject(new Error(`Port ${this.config.port} is already in use`));
167
+ }
168
+ else {
169
+ reject(error);
170
+ }
171
+ });
172
+ }
173
+ catch (error) {
174
+ reject(error);
175
+ }
176
+ });
177
+ }
178
+ /**
179
+ * Stop the server
180
+ */
181
+ async stop() {
182
+ return new Promise((resolve, reject) => {
183
+ if (!this.server) {
184
+ resolve();
185
+ return;
186
+ }
187
+ this.server.close((err) => {
188
+ if (err) {
189
+ reject(err);
190
+ }
191
+ else {
192
+ console.log('LSH SaaS API Server stopped');
193
+ resolve();
194
+ }
195
+ });
196
+ });
197
+ }
198
+ /**
199
+ * Get Express app (for testing)
200
+ */
201
+ getApp() {
202
+ return this.app;
203
+ }
204
+ }
205
+ /**
206
+ * Start the server if run directly
207
+ */
208
+ if (import.meta.url === `file://${process.argv[1]}`) {
209
+ const server = new SaaSApiServer();
210
+ server.start().catch((error) => {
211
+ console.error('Failed to start SaaS API server:', error);
212
+ process.exit(1);
213
+ });
214
+ // Graceful shutdown
215
+ process.on('SIGINT', async () => {
216
+ console.log('\nShutting down gracefully...');
217
+ await server.stop();
218
+ process.exit(0);
219
+ });
220
+ process.on('SIGTERM', async () => {
221
+ console.log('\nShutting down gracefully...');
222
+ await server.stop();
223
+ process.exit(0);
224
+ });
225
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Configuration Manager for LSH
3
+ * Manages ~/.config/lsh/lshrc configuration file
4
+ */
5
+ import * as fs from 'fs/promises';
6
+ import * as fsSync from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ export const DEFAULT_CONFIG_DIR = path.join(os.homedir(), '.config', 'lsh');
10
+ export const DEFAULT_CONFIG_FILE = path.join(DEFAULT_CONFIG_DIR, 'lshrc');
11
+ /**
12
+ * Default configuration template
13
+ */
14
+ export const DEFAULT_CONFIG_TEMPLATE = `# LSH Configuration File
15
+ # This file is in .env format and is automatically loaded on LSH startup
16
+ # Location: ~/.config/lsh/lshrc
17
+ #
18
+ # Edit this file with: lsh config
19
+
20
+ # ============================================================================
21
+ # Storage Backend (choose one)
22
+ # ============================================================================
23
+
24
+ # Option 1: Local Storage (Default - No Configuration Needed)
25
+ # Data stored in ~/.lsh/data/storage.json
26
+ # Perfect for: Single-user, local development, getting started
27
+
28
+ # Option 2: Supabase Cloud (Team Collaboration)
29
+ # SUPABASE_URL=https://your-project.supabase.co
30
+ # SUPABASE_ANON_KEY=your-anon-key-here
31
+
32
+ # Option 3: Local PostgreSQL (via Docker)
33
+ # DATABASE_URL=postgresql://lsh_user:lsh_password@localhost:5432/lsh
34
+
35
+ # ============================================================================
36
+ # Secrets Management
37
+ # ============================================================================
38
+
39
+ # Encryption key for secrets (generate with: lsh key)
40
+ # Share this key with your team for collaboration
41
+ # LSH_SECRETS_KEY=
42
+
43
+ # Master encryption key for SaaS platform
44
+ # LSH_MASTER_KEY=
45
+
46
+ # ============================================================================
47
+ # API Server (Optional)
48
+ # ============================================================================
49
+
50
+ # LSH_API_ENABLED=false
51
+ # LSH_API_PORT=3030
52
+ # LSH_API_KEY=
53
+ # LSH_JWT_SECRET=
54
+
55
+ # ============================================================================
56
+ # Webhooks (Optional)
57
+ # ============================================================================
58
+
59
+ # LSH_ENABLE_WEBHOOKS=false
60
+ # WEBHOOK_PORT=3033
61
+ # GITHUB_WEBHOOK_SECRET=
62
+ # GITLAB_WEBHOOK_SECRET=
63
+ # JENKINS_WEBHOOK_SECRET=
64
+
65
+ # ============================================================================
66
+ # Security
67
+ # ============================================================================
68
+
69
+ # WARNING: Only enable if you fully trust all job sources
70
+ # LSH_ALLOW_DANGEROUS_COMMANDS=false
71
+
72
+ # ============================================================================
73
+ # Advanced Configuration
74
+ # ============================================================================
75
+
76
+ # Custom data directory (default: ~/.lsh/data)
77
+ # LSH_DATA_DIR=
78
+
79
+ # Node environment
80
+ # NODE_ENV=development
81
+
82
+ # Redis (for caching)
83
+ # REDIS_URL=redis://localhost:6379
84
+
85
+ # ============================================================================
86
+ # SaaS Platform (Advanced)
87
+ # ============================================================================
88
+
89
+ # LSH_SAAS_API_PORT=3031
90
+ # LSH_SAAS_API_HOST=0.0.0.0
91
+ # LSH_CORS_ORIGINS=http://localhost:3000,http://localhost:3031
92
+
93
+ # Email Service (Resend)
94
+ # RESEND_API_KEY=
95
+ # EMAIL_FROM=noreply@yourdomain.com
96
+ # BASE_URL=https://app.yourdomain.com
97
+
98
+ # Stripe Billing
99
+ # STRIPE_SECRET_KEY=
100
+ # STRIPE_WEBHOOK_SECRET=
101
+ `;
102
+ /**
103
+ * Parse .env format file into config object
104
+ */
105
+ export function parseEnvFile(content) {
106
+ const config = {};
107
+ const lines = content.split('\n');
108
+ for (const line of lines) {
109
+ const trimmed = line.trim();
110
+ // Skip comments and empty lines
111
+ if (!trimmed || trimmed.startsWith('#')) {
112
+ continue;
113
+ }
114
+ // Parse KEY=VALUE
115
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
116
+ if (match) {
117
+ const key = match[1].trim();
118
+ let value = match[2].trim();
119
+ // Remove quotes if present
120
+ if ((value.startsWith('"') && value.endsWith('"')) ||
121
+ (value.startsWith("'") && value.endsWith("'"))) {
122
+ value = value.slice(1, -1);
123
+ }
124
+ config[key] = value;
125
+ }
126
+ }
127
+ return config;
128
+ }
129
+ /**
130
+ * Serialize config object to .env format
131
+ */
132
+ export function serializeConfig(config) {
133
+ const lines = [];
134
+ for (const [key, value] of Object.entries(config)) {
135
+ if (value !== undefined && value !== '') {
136
+ // Quote values that contain spaces or special characters
137
+ const needsQuotes = /[\s#"']/.test(value);
138
+ const serializedValue = needsQuotes ? `"${value}"` : value;
139
+ lines.push(`${key}=${serializedValue}`);
140
+ }
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+ export class ConfigManager {
145
+ configFile;
146
+ config = {};
147
+ constructor(configFile = DEFAULT_CONFIG_FILE) {
148
+ this.configFile = configFile;
149
+ }
150
+ /**
151
+ * Get config file path
152
+ */
153
+ getConfigPath() {
154
+ return this.configFile;
155
+ }
156
+ /**
157
+ * Check if config file exists
158
+ */
159
+ async exists() {
160
+ try {
161
+ await fs.access(this.configFile);
162
+ return true;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ /**
169
+ * Check if config file exists (sync)
170
+ */
171
+ existsSync() {
172
+ return fsSync.existsSync(this.configFile);
173
+ }
174
+ /**
175
+ * Initialize config file with default template
176
+ */
177
+ async initialize() {
178
+ const configDir = path.dirname(this.configFile);
179
+ // Create config directory if it doesn't exist
180
+ await fs.mkdir(configDir, { recursive: true });
181
+ // Check if config file already exists
182
+ const exists = await this.exists();
183
+ if (exists) {
184
+ console.log(`Config file already exists at ${this.configFile}`);
185
+ return;
186
+ }
187
+ // Write default template
188
+ await fs.writeFile(this.configFile, DEFAULT_CONFIG_TEMPLATE, 'utf-8');
189
+ console.log(`✓ Created config file at ${this.configFile}`);
190
+ console.log(` Edit with: lsh config`);
191
+ }
192
+ /**
193
+ * Load configuration from file
194
+ */
195
+ async load() {
196
+ try {
197
+ // Initialize if doesn't exist
198
+ if (!(await this.exists())) {
199
+ await this.initialize();
200
+ }
201
+ const content = await fs.readFile(this.configFile, 'utf-8');
202
+ this.config = parseEnvFile(content);
203
+ return this.config;
204
+ }
205
+ catch (error) {
206
+ console.error(`Failed to load config from ${this.configFile}:`, error);
207
+ return {};
208
+ }
209
+ }
210
+ /**
211
+ * Load configuration from file (sync)
212
+ */
213
+ loadSync() {
214
+ try {
215
+ // Check if exists
216
+ if (!this.existsSync()) {
217
+ // Can't initialize synchronously safely, return empty config
218
+ return {};
219
+ }
220
+ const content = fsSync.readFileSync(this.configFile, 'utf-8');
221
+ this.config = parseEnvFile(content);
222
+ return this.config;
223
+ }
224
+ catch (error) {
225
+ console.error(`Failed to load config from ${this.configFile}:`, error);
226
+ return {};
227
+ }
228
+ }
229
+ /**
230
+ * Save configuration to file
231
+ */
232
+ async save(config) {
233
+ try {
234
+ const configDir = path.dirname(this.configFile);
235
+ await fs.mkdir(configDir, { recursive: true });
236
+ // Merge with existing config
237
+ this.config = { ...this.config, ...config };
238
+ const content = serializeConfig(this.config);
239
+ await fs.writeFile(this.configFile, content, 'utf-8');
240
+ console.log(`✓ Saved config to ${this.configFile}`);
241
+ }
242
+ catch (error) {
243
+ console.error(`Failed to save config to ${this.configFile}:`, error);
244
+ throw error;
245
+ }
246
+ }
247
+ /**
248
+ * Get a specific config value
249
+ */
250
+ get(key) {
251
+ return this.config[key];
252
+ }
253
+ /**
254
+ * Set a specific config value
255
+ */
256
+ async set(key, value) {
257
+ this.config[key] = value;
258
+ await this.save(this.config);
259
+ }
260
+ /**
261
+ * Delete a specific config value
262
+ */
263
+ async delete(key) {
264
+ delete this.config[key];
265
+ await this.save(this.config);
266
+ }
267
+ /**
268
+ * Get all config values
269
+ */
270
+ getAll() {
271
+ return { ...this.config };
272
+ }
273
+ /**
274
+ * Merge config with process.env
275
+ * Config file values take precedence over environment variables
276
+ */
277
+ mergeWithEnv() {
278
+ for (const [key, value] of Object.entries(this.config)) {
279
+ if (value !== undefined && value !== '') {
280
+ process.env[key] = value;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Global config manager instance
287
+ */
288
+ let _globalConfigManager = null;
289
+ /**
290
+ * Get global config manager
291
+ */
292
+ export function getConfigManager() {
293
+ if (!_globalConfigManager) {
294
+ _globalConfigManager = new ConfigManager();
295
+ }
296
+ return _globalConfigManager;
297
+ }
298
+ /**
299
+ * Load global configuration and merge with process.env
300
+ * This should be called early in the application startup
301
+ */
302
+ export async function loadGlobalConfig() {
303
+ const manager = getConfigManager();
304
+ const config = await manager.load();
305
+ manager.mergeWithEnv();
306
+ return config;
307
+ }
308
+ /**
309
+ * Load global configuration synchronously and merge with process.env
310
+ */
311
+ export function loadGlobalConfigSync() {
312
+ const manager = getConfigManager();
313
+ const config = manager.loadSync();
314
+ manager.mergeWithEnv();
315
+ return config;
316
+ }
317
+ export default ConfigManager;