lsh-framework 3.2.5 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -34
- package/dist/commands/ipfs.js +7 -12
- package/dist/commands/self.js +22 -16
- package/dist/commands/sync.js +49 -38
- package/dist/constants/config.js +3 -0
- package/dist/lib/floating-point-arithmetic.js +2 -2
- package/dist/lib/ipfs-client-manager.js +51 -13
- package/dist/lib/ipfs-secrets-storage.js +21 -16
- package/dist/lib/ipfs-sync.js +88 -14
- package/dist/lib/secrets-manager.js +117 -47
- package/dist/lib/sync-key-store.js +87 -0
- package/dist/services/secrets/secrets.js +77 -39
- package/package.json +16 -16
- package/dist/__tests__/fixtures/job-fixtures.js +0 -204
- package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
- package/dist/daemon/job-registry.js +0 -556
- package/dist/daemon/lshd.js +0 -968
- package/dist/daemon/saas-api-routes.js +0 -599
- package/dist/daemon/saas-api-server.js +0 -231
- package/dist/examples/supabase-integration.js +0 -106
- package/dist/lib/api-response.js +0 -226
- package/dist/lib/base-command-registrar.js +0 -287
- package/dist/lib/base-job-manager.js +0 -295
- package/dist/lib/cloud-config-manager.js +0 -348
- package/dist/lib/cron-job-manager.js +0 -368
- package/dist/lib/daemon-client-helper.js +0 -145
- package/dist/lib/daemon-client.js +0 -513
- package/dist/lib/database-persistence.js +0 -727
- package/dist/lib/database-schema.js +0 -259
- package/dist/lib/database-types.js +0 -90
- package/dist/lib/enhanced-history-system.js +0 -247
- package/dist/lib/history-system.js +0 -246
- package/dist/lib/job-manager.js +0 -436
- package/dist/lib/job-storage-database.js +0 -164
- package/dist/lib/job-storage-memory.js +0 -73
- package/dist/lib/local-storage-adapter.js +0 -507
- package/dist/lib/optimized-job-scheduler.js +0 -356
- package/dist/lib/saas-audit.js +0 -215
- package/dist/lib/saas-auth.js +0 -465
- package/dist/lib/saas-billing.js +0 -503
- package/dist/lib/saas-email.js +0 -403
- package/dist/lib/saas-encryption.js +0 -221
- package/dist/lib/saas-organizations.js +0 -662
- package/dist/lib/saas-secrets.js +0 -408
- package/dist/lib/saas-types.js +0 -165
- package/dist/lib/supabase-client.js +0 -125
- package/dist/lib/supabase-utils.js +0 -396
- package/dist/services/cron/cron-registrar.js +0 -240
- package/dist/services/cron/cron.js +0 -9
- package/dist/services/daemon/daemon-registrar.js +0 -585
- package/dist/services/daemon/daemon.js +0 -9
- package/dist/services/supabase/supabase-registrar.js +0 -375
- package/dist/services/supabase/supabase.js +0 -9
|
@@ -1,231 +0,0 @@
|
|
|
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
|
-
import { getErrorMessage } from '../lib/saas-types.js';
|
|
10
|
-
import { ENV_VARS, DEFAULTS } from '../constants/index.js';
|
|
11
|
-
import { ERROR_CODES } from '../constants/errors.js';
|
|
12
|
-
/**
|
|
13
|
-
* SaaS API Server
|
|
14
|
-
*/
|
|
15
|
-
export class SaaSApiServer {
|
|
16
|
-
app;
|
|
17
|
-
config;
|
|
18
|
-
server;
|
|
19
|
-
constructor(config) {
|
|
20
|
-
this.config = {
|
|
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]),
|
|
24
|
-
rateLimitWindowMs: config?.rateLimitWindowMs || 15 * 60 * 1000, // 15 minutes
|
|
25
|
-
rateLimitMax: config?.rateLimitMax || DEFAULTS.MAX_EVENTS_LIMIT, // Max 100 requests per windowMs
|
|
26
|
-
};
|
|
27
|
-
this.app = express();
|
|
28
|
-
this.setupMiddleware();
|
|
29
|
-
this.setupRoutes();
|
|
30
|
-
this.setupErrorHandlers();
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Setup middleware
|
|
34
|
-
*/
|
|
35
|
-
setupMiddleware() {
|
|
36
|
-
// CORS
|
|
37
|
-
this.app.use(cors({
|
|
38
|
-
origin: this.config.corsOrigins,
|
|
39
|
-
credentials: true,
|
|
40
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
41
|
-
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
42
|
-
}));
|
|
43
|
-
// Rate limiting - global limit for all requests
|
|
44
|
-
const limiter = rateLimit({
|
|
45
|
-
windowMs: this.config.rateLimitWindowMs,
|
|
46
|
-
max: this.config.rateLimitMax,
|
|
47
|
-
message: {
|
|
48
|
-
success: false,
|
|
49
|
-
error: {
|
|
50
|
-
code: 'RATE_LIMIT_EXCEEDED',
|
|
51
|
-
message: 'Too many requests, please try again later',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
|
|
55
|
-
legacyHeaders: false, // Disable `X-RateLimit-*` headers
|
|
56
|
-
});
|
|
57
|
-
this.app.use(limiter);
|
|
58
|
-
// Body parsers
|
|
59
|
-
this.app.use(express.json({ limit: '10mb' }));
|
|
60
|
-
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
61
|
-
// Security headers
|
|
62
|
-
this.app.use((req, res, next) => {
|
|
63
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
64
|
-
res.setHeader('X-Frame-Options', 'DENY');
|
|
65
|
-
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
66
|
-
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
67
|
-
// CSP
|
|
68
|
-
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'");
|
|
69
|
-
next();
|
|
70
|
-
});
|
|
71
|
-
// Request logging
|
|
72
|
-
this.app.use((req, res, next) => {
|
|
73
|
-
const start = Date.now();
|
|
74
|
-
res.on('finish', () => {
|
|
75
|
-
const duration = Date.now() - start;
|
|
76
|
-
// Sanitize path to prevent log injection attacks
|
|
77
|
-
const sanitizedPath = req.path.replace(/[\r\n]/g, '');
|
|
78
|
-
const sanitizedIp = (req.ip || 'unknown').replace(/[\r\n]/g, '');
|
|
79
|
-
console.log(`${req.method} ${sanitizedPath} ${res.statusCode} - ${duration}ms - ${sanitizedIp}`);
|
|
80
|
-
});
|
|
81
|
-
next();
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Setup routes
|
|
86
|
-
*/
|
|
87
|
-
setupRoutes() {
|
|
88
|
-
// Health check
|
|
89
|
-
this.app.get('/health', (req, res) => {
|
|
90
|
-
res.json({
|
|
91
|
-
status: 'ok',
|
|
92
|
-
timestamp: new Date().toISOString(),
|
|
93
|
-
uptime: process.uptime(),
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
// API version
|
|
97
|
-
this.app.get('/api/v1', (req, res) => {
|
|
98
|
-
res.json({
|
|
99
|
-
name: 'LSH SaaS API',
|
|
100
|
-
version: '1.0.0',
|
|
101
|
-
endpoints: {
|
|
102
|
-
auth: '/api/v1/auth/*',
|
|
103
|
-
organizations: '/api/v1/organizations/*',
|
|
104
|
-
teams: '/api/v1/organizations/:id/teams/*',
|
|
105
|
-
secrets: '/api/v1/teams/:id/secrets/*',
|
|
106
|
-
billing: '/api/v1/organizations/:id/billing/*',
|
|
107
|
-
audit: '/api/v1/organizations/:id/audit-logs',
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
// Setup SaaS routes
|
|
112
|
-
setupSaaSApiRoutes(this.app);
|
|
113
|
-
// 404 handler
|
|
114
|
-
this.app.use((req, res) => {
|
|
115
|
-
res.status(404).json({
|
|
116
|
-
success: false,
|
|
117
|
-
error: {
|
|
118
|
-
code: ERROR_CODES.NOT_FOUND,
|
|
119
|
-
message: `Endpoint ${req.method} ${req.path} not found`,
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Setup error handlers
|
|
126
|
-
*/
|
|
127
|
-
setupErrorHandlers() {
|
|
128
|
-
// Global error handler
|
|
129
|
-
const errorHandler = (err, _req, res, _next) => {
|
|
130
|
-
console.error('API Error:', err);
|
|
131
|
-
// Don't leak error details in production
|
|
132
|
-
const isDev = process.env[ENV_VARS.NODE_ENV] !== 'production';
|
|
133
|
-
const statusCode = err.status || 500;
|
|
134
|
-
const errorCode = err.code || ERROR_CODES.INTERNAL_ERROR;
|
|
135
|
-
res.status(statusCode).json({
|
|
136
|
-
success: false,
|
|
137
|
-
error: {
|
|
138
|
-
code: errorCode,
|
|
139
|
-
message: isDev ? getErrorMessage(err) : 'Internal server error',
|
|
140
|
-
details: isDev ? err.stack : undefined,
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
};
|
|
144
|
-
this.app.use(errorHandler);
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Start the server
|
|
148
|
-
*/
|
|
149
|
-
async start() {
|
|
150
|
-
return new Promise((resolve, reject) => {
|
|
151
|
-
try {
|
|
152
|
-
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
153
|
-
console.log(`
|
|
154
|
-
╔═══════════════════════════════════════════════════════════════╗
|
|
155
|
-
║ ║
|
|
156
|
-
║ 🔐 LSH SaaS API Server ║
|
|
157
|
-
║ ║
|
|
158
|
-
║ Status: Running ║
|
|
159
|
-
║ Port: ${this.config.port.toString().padEnd(49)} ║
|
|
160
|
-
║ Host: ${this.config.host?.padEnd(49)} ║
|
|
161
|
-
║ ║
|
|
162
|
-
║ Endpoints: ║
|
|
163
|
-
║ - Health: http://${this.config.host}:${this.config.port}/health${' '.repeat(26)} ║
|
|
164
|
-
║ - API: http://${this.config.host}:${this.config.port}/api/v1${' '.repeat(27)} ║
|
|
165
|
-
║ ║
|
|
166
|
-
╚═══════════════════════════════════════════════════════════════╝
|
|
167
|
-
`);
|
|
168
|
-
resolve();
|
|
169
|
-
});
|
|
170
|
-
this.server.on('error', (error) => {
|
|
171
|
-
if (error.code === 'EADDRINUSE') {
|
|
172
|
-
reject(new Error(`Port ${this.config.port} is already in use`));
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
reject(error);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
reject(error);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Stop the server
|
|
186
|
-
*/
|
|
187
|
-
async stop() {
|
|
188
|
-
return new Promise((resolve, reject) => {
|
|
189
|
-
if (!this.server) {
|
|
190
|
-
resolve();
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
this.server.close((err) => {
|
|
194
|
-
if (err) {
|
|
195
|
-
reject(err);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
console.log('LSH SaaS API Server stopped');
|
|
199
|
-
resolve();
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Get Express app (for testing)
|
|
206
|
-
*/
|
|
207
|
-
getApp() {
|
|
208
|
-
return this.app;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Start the server if run directly
|
|
213
|
-
*/
|
|
214
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
215
|
-
const server = new SaaSApiServer();
|
|
216
|
-
server.start().catch((error) => {
|
|
217
|
-
console.error('Failed to start SaaS API server:', error);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
});
|
|
220
|
-
// Graceful shutdown
|
|
221
|
-
process.on('SIGINT', async () => {
|
|
222
|
-
console.log('\nShutting down gracefully...');
|
|
223
|
-
await server.stop();
|
|
224
|
-
process.exit(0);
|
|
225
|
-
});
|
|
226
|
-
process.on('SIGTERM', async () => {
|
|
227
|
-
console.log('\nShutting down gracefully...');
|
|
228
|
-
await server.stop();
|
|
229
|
-
process.exit(0);
|
|
230
|
-
});
|
|
231
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supabase Integration Example
|
|
3
|
-
* Demonstrates how to use LSH with Supabase PostgreSQL
|
|
4
|
-
*/
|
|
5
|
-
import { supabaseClient } from '../lib/supabase-client.js';
|
|
6
|
-
import DatabasePersistence from '../lib/database-persistence.js';
|
|
7
|
-
import CloudConfigManager from '../lib/cloud-config-manager.js';
|
|
8
|
-
import EnhancedHistorySystem from '../lib/enhanced-history-system.js';
|
|
9
|
-
async function demonstrateSupabaseIntegration() {
|
|
10
|
-
console.log('🚀 LSH Supabase Integration Demo\n');
|
|
11
|
-
// 1. Test database connection
|
|
12
|
-
console.log('1. Testing Supabase connection...');
|
|
13
|
-
const isConnected = await supabaseClient.testConnection();
|
|
14
|
-
if (!isConnected) {
|
|
15
|
-
console.log('❌ Database connection failed');
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
console.log('✅ Database connection successful\n');
|
|
19
|
-
// 2. Initialize database persistence
|
|
20
|
-
console.log('2. Initializing database persistence...');
|
|
21
|
-
const persistence = new DatabasePersistence('demo-user');
|
|
22
|
-
console.log('✅ Database persistence initialized\n');
|
|
23
|
-
// 3. Demonstrate configuration management
|
|
24
|
-
console.log('3. Configuration management demo...');
|
|
25
|
-
const configManager = new CloudConfigManager({
|
|
26
|
-
userId: 'demo-user',
|
|
27
|
-
enableCloudSync: true,
|
|
28
|
-
});
|
|
29
|
-
// Set some configuration values
|
|
30
|
-
configManager.set('theme', 'dark', 'UI theme preference');
|
|
31
|
-
configManager.set('max_history', 1000, 'Maximum history entries');
|
|
32
|
-
configManager.set('auto_complete', true, 'Enable auto-completion');
|
|
33
|
-
console.log('Configuration set:');
|
|
34
|
-
configManager.getAll().forEach(config => {
|
|
35
|
-
console.log(` ${config.key}: ${JSON.stringify(config.value)}`);
|
|
36
|
-
});
|
|
37
|
-
console.log('✅ Configuration management working\n');
|
|
38
|
-
// 4. Demonstrate history management
|
|
39
|
-
console.log('4. History management demo...');
|
|
40
|
-
const historySystem = new EnhancedHistorySystem({
|
|
41
|
-
userId: 'demo-user',
|
|
42
|
-
enableCloudSync: true,
|
|
43
|
-
});
|
|
44
|
-
// Add some sample commands
|
|
45
|
-
historySystem.addCommand('ls -la', 0);
|
|
46
|
-
historySystem.addCommand('cd /home/user', 0);
|
|
47
|
-
historySystem.addCommand('git status', 0);
|
|
48
|
-
historySystem.addCommand('npm install', 0);
|
|
49
|
-
// Save to database
|
|
50
|
-
const entries = historySystem.getAllEntries();
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
await persistence.saveHistoryEntry({
|
|
53
|
-
session_id: persistence.getSessionId(),
|
|
54
|
-
command: entry.command,
|
|
55
|
-
working_directory: '/home/user',
|
|
56
|
-
exit_code: entry.exitCode,
|
|
57
|
-
timestamp: new Date(entry.timestamp).toISOString(),
|
|
58
|
-
hostname: 'demo-host',
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
console.log(`Added ${entries.length} history entries`);
|
|
62
|
-
console.log('✅ History management working\n');
|
|
63
|
-
// 5. Demonstrate job management
|
|
64
|
-
console.log('5. Job management demo...');
|
|
65
|
-
await persistence.saveJob({
|
|
66
|
-
session_id: persistence.getSessionId(),
|
|
67
|
-
job_id: 'job_1',
|
|
68
|
-
command: 'long-running-task',
|
|
69
|
-
status: 'running',
|
|
70
|
-
working_directory: '/home/user',
|
|
71
|
-
started_at: new Date().toISOString(),
|
|
72
|
-
});
|
|
73
|
-
const activeJobs = await persistence.getActiveJobs();
|
|
74
|
-
console.log(`Active jobs: ${activeJobs.length}`);
|
|
75
|
-
activeJobs.forEach(job => {
|
|
76
|
-
console.log(` ${job.job_id}: ${job.command} (${job.status})`);
|
|
77
|
-
});
|
|
78
|
-
console.log('✅ Job management working\n');
|
|
79
|
-
// 6. Demonstrate session management
|
|
80
|
-
console.log('6. Session management demo...');
|
|
81
|
-
await persistence.startSession('/home/user', {
|
|
82
|
-
PATH: '/usr/bin:/bin',
|
|
83
|
-
HOME: '/home/user',
|
|
84
|
-
USER: 'demo-user',
|
|
85
|
-
});
|
|
86
|
-
console.log(`Session started: ${persistence.getSessionId()}`);
|
|
87
|
-
console.log('✅ Session management working\n');
|
|
88
|
-
// 7. Show statistics
|
|
89
|
-
console.log('7. Statistics:');
|
|
90
|
-
const historyStats = await historySystem.getHistoryStats();
|
|
91
|
-
console.log('History stats:', historyStats);
|
|
92
|
-
const configStats = configManager.getStats();
|
|
93
|
-
console.log('Config stats:', configStats);
|
|
94
|
-
// 8. Cleanup
|
|
95
|
-
console.log('\n8. Cleanup...');
|
|
96
|
-
await persistence.endSession();
|
|
97
|
-
historySystem.destroy();
|
|
98
|
-
configManager.destroy();
|
|
99
|
-
console.log('✅ Cleanup completed');
|
|
100
|
-
console.log('\n🎉 Supabase integration demo completed successfully!');
|
|
101
|
-
}
|
|
102
|
-
// Run the demo if this file is executed directly
|
|
103
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
104
|
-
demonstrateSupabaseIntegration().catch(console.error);
|
|
105
|
-
}
|
|
106
|
-
export default demonstrateSupabaseIntegration;
|
package/dist/lib/api-response.js
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API Response Builder
|
|
3
|
-
*
|
|
4
|
-
* Standardized response utilities for the SaaS API.
|
|
5
|
-
* Centralizes response formatting and error handling.
|
|
6
|
-
*
|
|
7
|
-
* @module api-response
|
|
8
|
-
* @example
|
|
9
|
-
* ```typescript
|
|
10
|
-
* import { sendSuccess, ApiErrors } from './api-response.js';
|
|
11
|
-
*
|
|
12
|
-
* // Success response
|
|
13
|
-
* app.get('/users/:id', async (req, res) => {
|
|
14
|
-
* const user = await getUser(req.params.id);
|
|
15
|
-
* return sendSuccess(res, { user });
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* // Error response
|
|
19
|
-
* app.get('/protected', async (req, res) => {
|
|
20
|
-
* if (!req.user) {
|
|
21
|
-
* return ApiErrors.unauthorized(res);
|
|
22
|
-
* }
|
|
23
|
-
* });
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
import { ERROR_CODES } from '../constants/errors.js';
|
|
27
|
-
/**
|
|
28
|
-
* HTTP status codes mapped to error codes
|
|
29
|
-
* @internal
|
|
30
|
-
*/
|
|
31
|
-
const ERROR_STATUS_MAP = {
|
|
32
|
-
[ERROR_CODES.UNAUTHORIZED]: 401,
|
|
33
|
-
[ERROR_CODES.INVALID_CREDENTIALS]: 401,
|
|
34
|
-
[ERROR_CODES.INVALID_TOKEN]: 401,
|
|
35
|
-
[ERROR_CODES.EMAIL_NOT_VERIFIED]: 401,
|
|
36
|
-
[ERROR_CODES.FORBIDDEN]: 403,
|
|
37
|
-
[ERROR_CODES.NOT_FOUND]: 404,
|
|
38
|
-
[ERROR_CODES.INVALID_INPUT]: 400,
|
|
39
|
-
[ERROR_CODES.ALREADY_EXISTS]: 409,
|
|
40
|
-
[ERROR_CODES.EMAIL_ALREADY_EXISTS]: 409,
|
|
41
|
-
[ERROR_CODES.PAYMENT_REQUIRED]: 402,
|
|
42
|
-
[ERROR_CODES.TIER_LIMIT_EXCEEDED]: 403,
|
|
43
|
-
[ERROR_CODES.INTERNAL_ERROR]: 500,
|
|
44
|
-
RATE_LIMIT_EXCEEDED: 429,
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Send a successful API response
|
|
48
|
-
*
|
|
49
|
-
* @template T - The type of the data payload
|
|
50
|
-
* @param res - Express response object
|
|
51
|
-
* @param data - The data to send in the response
|
|
52
|
-
* @param statusCode - HTTP status code (default: 200)
|
|
53
|
-
* @returns The Express response object
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```typescript
|
|
57
|
-
* return sendSuccess(res, { user: { id: '123', name: 'John' } });
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
export function sendSuccess(res, data, statusCode = 200) {
|
|
61
|
-
return res.status(statusCode).json({
|
|
62
|
-
success: true,
|
|
63
|
-
data,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Send a created (201) response for newly created resources
|
|
68
|
-
*
|
|
69
|
-
* @template T - The type of the data payload
|
|
70
|
-
* @param res - Express response object
|
|
71
|
-
* @param data - The created resource data
|
|
72
|
-
* @returns The Express response object
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```typescript
|
|
76
|
-
* const newUser = await createUser(userData);
|
|
77
|
-
* return sendCreated(res, { user: newUser });
|
|
78
|
-
* ```
|
|
79
|
-
*/
|
|
80
|
-
export function sendCreated(res, data) {
|
|
81
|
-
return sendSuccess(res, data, 201);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Send an error response with automatic status code mapping
|
|
85
|
-
*
|
|
86
|
-
* @param res - Express response object
|
|
87
|
-
* @param code - Error code (e.g., 'UNAUTHORIZED', 'NOT_FOUND')
|
|
88
|
-
* @param message - Human-readable error message
|
|
89
|
-
* @param statusCode - Optional explicit HTTP status code (auto-mapped if not provided)
|
|
90
|
-
* @returns The Express response object
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* ```typescript
|
|
94
|
-
* return sendError(res, 'CUSTOM_ERROR', 'Something went wrong', 422);
|
|
95
|
-
* ```
|
|
96
|
-
*/
|
|
97
|
-
export function sendError(res, code, message, statusCode) {
|
|
98
|
-
const status = statusCode ?? ERROR_STATUS_MAP[code] ?? 500;
|
|
99
|
-
return res.status(status).json({
|
|
100
|
-
success: false,
|
|
101
|
-
error: { code, message },
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Common error response helpers with pre-configured error codes and messages.
|
|
106
|
-
* Each method returns the appropriate HTTP status code automatically.
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* ```typescript
|
|
110
|
-
* // 401 Unauthorized
|
|
111
|
-
* return ApiErrors.unauthorized(res);
|
|
112
|
-
*
|
|
113
|
-
* // 404 Not Found with custom resource name
|
|
114
|
-
* return ApiErrors.notFound(res, 'User');
|
|
115
|
-
*
|
|
116
|
-
* // 400 Bad Request with validation message
|
|
117
|
-
* return ApiErrors.invalidInput(res, 'Email is required');
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
export const ApiErrors = {
|
|
121
|
-
/**
|
|
122
|
-
* Send a 401 Unauthorized response
|
|
123
|
-
* @param res - Express response object
|
|
124
|
-
* @param message - Custom error message (default: 'Authentication required')
|
|
125
|
-
*/
|
|
126
|
-
unauthorized(res, message = 'Authentication required') {
|
|
127
|
-
return sendError(res, ERROR_CODES.UNAUTHORIZED, message);
|
|
128
|
-
},
|
|
129
|
-
/**
|
|
130
|
-
* Send a 401 Invalid Token response
|
|
131
|
-
* @param res - Express response object
|
|
132
|
-
* @param message - Custom error message (default: 'Invalid or expired token')
|
|
133
|
-
*/
|
|
134
|
-
invalidToken(res, message = 'Invalid or expired token') {
|
|
135
|
-
return sendError(res, ERROR_CODES.INVALID_TOKEN, message);
|
|
136
|
-
},
|
|
137
|
-
/**
|
|
138
|
-
* Send a 403 Forbidden response
|
|
139
|
-
* @param res - Express response object
|
|
140
|
-
* @param message - Custom error message (default: 'Access denied')
|
|
141
|
-
*/
|
|
142
|
-
forbidden(res, message = 'Access denied') {
|
|
143
|
-
return sendError(res, ERROR_CODES.FORBIDDEN, message);
|
|
144
|
-
},
|
|
145
|
-
/**
|
|
146
|
-
* Send a 404 Not Found response
|
|
147
|
-
* @param res - Express response object
|
|
148
|
-
* @param resource - Resource name for the error message (default: 'Resource')
|
|
149
|
-
*/
|
|
150
|
-
notFound(res, resource = 'Resource') {
|
|
151
|
-
return sendError(res, ERROR_CODES.NOT_FOUND, `${resource} not found`);
|
|
152
|
-
},
|
|
153
|
-
/**
|
|
154
|
-
* Send a 400 Bad Request response for invalid input
|
|
155
|
-
* @param res - Express response object
|
|
156
|
-
* @param message - Validation error message
|
|
157
|
-
*/
|
|
158
|
-
invalidInput(res, message) {
|
|
159
|
-
return sendError(res, ERROR_CODES.INVALID_INPUT, message);
|
|
160
|
-
},
|
|
161
|
-
/**
|
|
162
|
-
* Send a 409 Conflict response for duplicate resources
|
|
163
|
-
* @param res - Express response object
|
|
164
|
-
* @param resource - Resource name for the error message (default: 'Resource')
|
|
165
|
-
*/
|
|
166
|
-
alreadyExists(res, resource = 'Resource') {
|
|
167
|
-
return sendError(res, ERROR_CODES.ALREADY_EXISTS, `${resource} already exists`);
|
|
168
|
-
},
|
|
169
|
-
/**
|
|
170
|
-
* Send a 500 Internal Server Error response
|
|
171
|
-
* @param res - Express response object
|
|
172
|
-
* @param message - Custom error message (default: 'An internal error occurred')
|
|
173
|
-
*/
|
|
174
|
-
internalError(res, message = 'An internal error occurred') {
|
|
175
|
-
return sendError(res, ERROR_CODES.INTERNAL_ERROR, message);
|
|
176
|
-
},
|
|
177
|
-
/**
|
|
178
|
-
* Send a 402 Payment Required response
|
|
179
|
-
* @param res - Express response object
|
|
180
|
-
* @param message - Custom error message (default: 'Payment required')
|
|
181
|
-
*/
|
|
182
|
-
paymentRequired(res, message = 'Payment required') {
|
|
183
|
-
return sendError(res, ERROR_CODES.PAYMENT_REQUIRED, message);
|
|
184
|
-
},
|
|
185
|
-
/**
|
|
186
|
-
* Send a 403 Tier Limit Exceeded response
|
|
187
|
-
* @param res - Express response object
|
|
188
|
-
* @param message - Custom error message (default: 'Plan limit exceeded')
|
|
189
|
-
*/
|
|
190
|
-
tierLimitExceeded(res, message = 'Plan limit exceeded') {
|
|
191
|
-
return sendError(res, ERROR_CODES.TIER_LIMIT_EXCEEDED, message);
|
|
192
|
-
},
|
|
193
|
-
/**
|
|
194
|
-
* Send a 429 Rate Limit Exceeded response
|
|
195
|
-
* @param res - Express response object
|
|
196
|
-
* @param message - Custom error message (default: 'Too many requests')
|
|
197
|
-
*/
|
|
198
|
-
rateLimitExceeded(res, message = 'Too many requests') {
|
|
199
|
-
return sendError(res, 'RATE_LIMIT_EXCEEDED', message, 429);
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
/**
|
|
203
|
-
* Extract a human-readable error message from an unknown error type.
|
|
204
|
-
* Useful in catch blocks where the error type is unknown.
|
|
205
|
-
*
|
|
206
|
-
* @param error - The caught error (unknown type)
|
|
207
|
-
* @returns A string error message
|
|
208
|
-
*
|
|
209
|
-
* @example
|
|
210
|
-
* ```typescript
|
|
211
|
-
* try {
|
|
212
|
-
* await riskyOperation();
|
|
213
|
-
* } catch (error) {
|
|
214
|
-
* return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
215
|
-
* }
|
|
216
|
-
* ```
|
|
217
|
-
*/
|
|
218
|
-
export function extractApiErrorMessage(error) {
|
|
219
|
-
if (error instanceof Error) {
|
|
220
|
-
return error.message;
|
|
221
|
-
}
|
|
222
|
-
if (typeof error === 'string') {
|
|
223
|
-
return error;
|
|
224
|
-
}
|
|
225
|
-
return 'An unexpected error occurred';
|
|
226
|
-
}
|