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,599 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LSH SaaS API Routes
|
|
3
|
-
* RESTful API endpoints for the SaaS platform
|
|
4
|
-
*/
|
|
5
|
-
import rateLimit from 'express-rate-limit';
|
|
6
|
-
import { authService, verifyToken } from '../lib/saas-auth.js';
|
|
7
|
-
import { organizationService, teamService } from '../lib/saas-organizations.js';
|
|
8
|
-
import { billingService } from '../lib/saas-billing.js';
|
|
9
|
-
import { auditLogger, getIpFromRequest } from '../lib/saas-audit.js';
|
|
10
|
-
import { emailService } from '../lib/saas-email.js';
|
|
11
|
-
import { secretsService } from '../lib/saas-secrets.js';
|
|
12
|
-
import { getAuthenticatedUser } from '../lib/saas-types.js'; // eslint-disable-line no-duplicate-imports
|
|
13
|
-
import { sendSuccess, sendError, ApiErrors, extractApiErrorMessage } from '../lib/api-response.js';
|
|
14
|
-
import { ERROR_CODES } from '../constants/errors.js';
|
|
15
|
-
/**
|
|
16
|
-
* Rate Limiters for different endpoint types
|
|
17
|
-
*/
|
|
18
|
-
// Strict rate limiter for authentication endpoints (5 requests per 15 minutes)
|
|
19
|
-
const authLimiter = rateLimit({
|
|
20
|
-
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
21
|
-
max: 5,
|
|
22
|
-
message: {
|
|
23
|
-
success: false,
|
|
24
|
-
error: {
|
|
25
|
-
code: 'RATE_LIMIT_EXCEEDED',
|
|
26
|
-
message: 'Too many authentication attempts, please try again later',
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
standardHeaders: true,
|
|
30
|
-
legacyHeaders: false,
|
|
31
|
-
});
|
|
32
|
-
// Moderate rate limiter for write operations (30 requests per 15 minutes)
|
|
33
|
-
const writeLimiter = rateLimit({
|
|
34
|
-
windowMs: 15 * 60 * 1000,
|
|
35
|
-
max: 30,
|
|
36
|
-
message: {
|
|
37
|
-
success: false,
|
|
38
|
-
error: {
|
|
39
|
-
code: 'RATE_LIMIT_EXCEEDED',
|
|
40
|
-
message: 'Too many write requests, please try again later',
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
standardHeaders: true,
|
|
44
|
-
legacyHeaders: false,
|
|
45
|
-
});
|
|
46
|
-
/**
|
|
47
|
-
* Middleware: Authenticate user from JWT token
|
|
48
|
-
* Security is enforced by cryptographic JWT verification and database lookup,
|
|
49
|
-
* not by input validation alone.
|
|
50
|
-
*/
|
|
51
|
-
export async function authenticateUser(req, res, next) {
|
|
52
|
-
try {
|
|
53
|
-
const authHeader = req.headers.authorization;
|
|
54
|
-
// Early rejection for malformed requests (not a security boundary)
|
|
55
|
-
// Real security comes from JWT cryptographic verification below
|
|
56
|
-
if (typeof authHeader !== 'string' || !authHeader.startsWith('Bearer ')) {
|
|
57
|
-
return ApiErrors.unauthorized(res, 'No authorization token provided');
|
|
58
|
-
}
|
|
59
|
-
const token = authHeader.substring(7).trim();
|
|
60
|
-
// SECURITY BOUNDARY: Cryptographic JWT verification
|
|
61
|
-
// This uses server-side secret to verify token authenticity
|
|
62
|
-
// User cannot bypass this by manipulating input
|
|
63
|
-
const { userId } = verifyToken(token);
|
|
64
|
-
const user = await authService.getUserById(userId);
|
|
65
|
-
if (!user) {
|
|
66
|
-
return ApiErrors.unauthorized(res, 'Invalid token');
|
|
67
|
-
}
|
|
68
|
-
// Attach user to request (safe - req is not reassigned, only augmented)
|
|
69
|
-
// eslint-disable-next-line require-atomic-updates
|
|
70
|
-
req.user = user;
|
|
71
|
-
next();
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
return ApiErrors.unauthorized(res, extractApiErrorMessage(error) || 'Authentication failed');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Middleware: Check organization membership
|
|
79
|
-
*/
|
|
80
|
-
export async function requireOrganizationMembership(req, res, next) {
|
|
81
|
-
try {
|
|
82
|
-
// Only use organizationId from URL params to prevent bypass attacks
|
|
83
|
-
const organizationId = req.params.organizationId;
|
|
84
|
-
if (!organizationId) {
|
|
85
|
-
return ApiErrors.invalidInput(res, 'Organization ID required');
|
|
86
|
-
}
|
|
87
|
-
if (!req.user) {
|
|
88
|
-
return ApiErrors.unauthorized(res, 'User not authenticated');
|
|
89
|
-
}
|
|
90
|
-
const role = await organizationService.getUserRole(organizationId, getAuthenticatedUser(req).id);
|
|
91
|
-
if (!role) {
|
|
92
|
-
return ApiErrors.forbidden(res, 'Not a member of this organization');
|
|
93
|
-
}
|
|
94
|
-
// Safe - req is not reassigned, only augmented with organization context
|
|
95
|
-
// eslint-disable-next-line require-atomic-updates
|
|
96
|
-
req.organizationId = organizationId;
|
|
97
|
-
next();
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Setup SaaS API routes
|
|
105
|
-
*/
|
|
106
|
-
export function setupSaaSApiRoutes(app) {
|
|
107
|
-
// ============================================================================
|
|
108
|
-
// AUTH ROUTES
|
|
109
|
-
// ============================================================================
|
|
110
|
-
/**
|
|
111
|
-
* POST /api/v1/auth/signup
|
|
112
|
-
* Sign up a new user
|
|
113
|
-
*/
|
|
114
|
-
app.post('/api/v1/auth/signup', authLimiter, async (req, res) => {
|
|
115
|
-
try {
|
|
116
|
-
const { email, password, firstName, lastName } = req.body;
|
|
117
|
-
// Input sanitization (not a security boundary)
|
|
118
|
-
// Real validation happens in authService.signup()
|
|
119
|
-
if (typeof email !== 'string' || typeof password !== 'string' ||
|
|
120
|
-
email.trim().length === 0 || password.length < 8) {
|
|
121
|
-
return ApiErrors.invalidInput(res, 'Valid email and password (min 8 chars) required');
|
|
122
|
-
}
|
|
123
|
-
// Basic email format validation (fail-fast for obviously bad input)
|
|
124
|
-
// Real security: authService validates and prevents duplicate emails in database
|
|
125
|
-
if (email.length > 254 || // RFC 5321 max email length
|
|
126
|
-
!email.includes('@') ||
|
|
127
|
-
email.indexOf('@') !== email.lastIndexOf('@') || // Exactly one @
|
|
128
|
-
email.startsWith('@') ||
|
|
129
|
-
email.endsWith('@') ||
|
|
130
|
-
email.split('@')[1]?.includes('.') === false) { // Domain has a dot
|
|
131
|
-
return ApiErrors.invalidInput(res, 'Invalid email format');
|
|
132
|
-
}
|
|
133
|
-
// SECURITY BOUNDARY: authService handles:
|
|
134
|
-
// - Password hashing with bcrypt
|
|
135
|
-
// - Database uniqueness constraints
|
|
136
|
-
// - Email verification token generation
|
|
137
|
-
const { user, verificationToken } = await authService.signup({
|
|
138
|
-
email,
|
|
139
|
-
password,
|
|
140
|
-
firstName,
|
|
141
|
-
lastName,
|
|
142
|
-
});
|
|
143
|
-
// Send verification email
|
|
144
|
-
await emailService.sendVerificationEmail(user.email, verificationToken, user.firstName || undefined);
|
|
145
|
-
return sendSuccess(res, {
|
|
146
|
-
user: {
|
|
147
|
-
id: user.id,
|
|
148
|
-
email: user.email,
|
|
149
|
-
firstName: user.firstName,
|
|
150
|
-
lastName: user.lastName,
|
|
151
|
-
emailVerified: user.emailVerified,
|
|
152
|
-
},
|
|
153
|
-
message: 'Verification email sent',
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
const msg = extractApiErrorMessage(error);
|
|
158
|
-
const code = msg.includes('ALREADY_EXISTS') ? ERROR_CODES.EMAIL_ALREADY_EXISTS : ERROR_CODES.INTERNAL_ERROR;
|
|
159
|
-
return sendError(res, code, msg, 400);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
/**
|
|
163
|
-
* POST /api/v1/auth/login
|
|
164
|
-
* Login with email and password
|
|
165
|
-
*/
|
|
166
|
-
app.post('/api/v1/auth/login', authLimiter, async (req, res) => {
|
|
167
|
-
try {
|
|
168
|
-
const { email, password } = req.body;
|
|
169
|
-
// Input sanitization (not a security boundary)
|
|
170
|
-
// Real authentication happens in authService.login() via bcrypt password comparison
|
|
171
|
-
if (typeof email !== 'string' || typeof password !== 'string' ||
|
|
172
|
-
email.trim().length === 0 || password.length === 0) {
|
|
173
|
-
return ApiErrors.invalidInput(res, 'Email and password required');
|
|
174
|
-
}
|
|
175
|
-
// SECURITY BOUNDARY: authService.login() performs:
|
|
176
|
-
// - Database lookup for user by email
|
|
177
|
-
// - bcrypt password verification (cryptographic)
|
|
178
|
-
// - Session token generation with server-side secret
|
|
179
|
-
const ipAddress = getIpFromRequest(req);
|
|
180
|
-
const session = await authService.login({ email, password }, ipAddress);
|
|
181
|
-
return sendSuccess(res, session);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
const msg = extractApiErrorMessage(error);
|
|
185
|
-
const statusCode = msg.includes('EMAIL_NOT_VERIFIED') ? 403 : 401;
|
|
186
|
-
const code = msg || ERROR_CODES.INVALID_CREDENTIALS;
|
|
187
|
-
return sendError(res, code, msg, statusCode);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
/**
|
|
191
|
-
* POST /api/v1/auth/verify-email
|
|
192
|
-
* Verify email address
|
|
193
|
-
*/
|
|
194
|
-
app.post('/api/v1/auth/verify-email', authLimiter, async (req, res) => {
|
|
195
|
-
try {
|
|
196
|
-
const { token } = req.body;
|
|
197
|
-
// Input sanitization (not a security boundary)
|
|
198
|
-
// Real verification happens in authService.verifyEmail() via database lookup
|
|
199
|
-
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
200
|
-
return ApiErrors.invalidInput(res, 'Valid token required');
|
|
201
|
-
}
|
|
202
|
-
// SECURITY BOUNDARY: authService.verifyEmail() performs:
|
|
203
|
-
// - Database lookup for verification token
|
|
204
|
-
// - Token expiry validation
|
|
205
|
-
// - User status update in database
|
|
206
|
-
const user = await authService.verifyEmail(token);
|
|
207
|
-
// Send welcome email
|
|
208
|
-
await emailService.sendWelcomeEmail(user.email, user.firstName || undefined);
|
|
209
|
-
return sendSuccess(res, { user, message: 'Email verified successfully' });
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
return ApiErrors.invalidToken(res, extractApiErrorMessage(error));
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
/**
|
|
216
|
-
* POST /api/v1/auth/resend-verification
|
|
217
|
-
* Resend verification email
|
|
218
|
-
*/
|
|
219
|
-
app.post('/api/v1/auth/resend-verification', authLimiter, async (req, res) => {
|
|
220
|
-
try {
|
|
221
|
-
const { email } = req.body;
|
|
222
|
-
const token = await authService.resendVerificationEmail(email);
|
|
223
|
-
const user = await authService.getUserByEmail(email);
|
|
224
|
-
if (user) {
|
|
225
|
-
await emailService.sendVerificationEmail(email, token, user.firstName || undefined);
|
|
226
|
-
}
|
|
227
|
-
return sendSuccess(res, { message: 'Verification email sent' });
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
/**
|
|
234
|
-
* POST /api/v1/auth/refresh
|
|
235
|
-
* Refresh access token
|
|
236
|
-
*/
|
|
237
|
-
app.post('/api/v1/auth/refresh', authLimiter, async (req, res) => {
|
|
238
|
-
try {
|
|
239
|
-
const { refreshToken } = req.body;
|
|
240
|
-
if (!refreshToken) {
|
|
241
|
-
return ApiErrors.invalidInput(res, 'Refresh token required');
|
|
242
|
-
}
|
|
243
|
-
const tokens = await authService.refreshAccessToken(refreshToken);
|
|
244
|
-
return sendSuccess(res, tokens);
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
return ApiErrors.invalidToken(res, extractApiErrorMessage(error));
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
/**
|
|
251
|
-
* GET /api/v1/auth/me
|
|
252
|
-
* Get current user
|
|
253
|
-
*/
|
|
254
|
-
app.get('/api/v1/auth/me', authenticateUser, async (req, res) => {
|
|
255
|
-
return sendSuccess(res, { user: req.user });
|
|
256
|
-
});
|
|
257
|
-
// ============================================================================
|
|
258
|
-
// ORGANIZATION ROUTES
|
|
259
|
-
// ============================================================================
|
|
260
|
-
/**
|
|
261
|
-
* POST /api/v1/organizations
|
|
262
|
-
* Create a new organization
|
|
263
|
-
*/
|
|
264
|
-
app.post('/api/v1/organizations', writeLimiter, authenticateUser, async (req, res) => {
|
|
265
|
-
try {
|
|
266
|
-
const { name, slug } = req.body;
|
|
267
|
-
if (!name) {
|
|
268
|
-
return ApiErrors.invalidInput(res, 'Organization name required');
|
|
269
|
-
}
|
|
270
|
-
const organization = await organizationService.createOrganization({
|
|
271
|
-
name,
|
|
272
|
-
slug,
|
|
273
|
-
ownerId: getAuthenticatedUser(req).id,
|
|
274
|
-
});
|
|
275
|
-
return sendSuccess(res, { organization });
|
|
276
|
-
}
|
|
277
|
-
catch (error) {
|
|
278
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
/**
|
|
282
|
-
* GET /api/v1/organizations/:organizationId
|
|
283
|
-
* Get organization details
|
|
284
|
-
*/
|
|
285
|
-
app.get('/api/v1/organizations/:organizationId', authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
286
|
-
try {
|
|
287
|
-
const organization = await organizationService.getOrganizationById(req.params.organizationId);
|
|
288
|
-
if (!organization) {
|
|
289
|
-
return ApiErrors.notFound(res, 'Organization');
|
|
290
|
-
}
|
|
291
|
-
// Get usage summary
|
|
292
|
-
const usage = await organizationService.getUsageSummary(req.params.organizationId);
|
|
293
|
-
return sendSuccess(res, { organization, usage });
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
/**
|
|
300
|
-
* GET /api/v1/organizations/:organizationId/members
|
|
301
|
-
* Get organization members
|
|
302
|
-
*/
|
|
303
|
-
app.get('/api/v1/organizations/:organizationId/members', authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
304
|
-
try {
|
|
305
|
-
const members = await organizationService.getOrganizationMembers(req.params.organizationId);
|
|
306
|
-
return sendSuccess(res, { members });
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
/**
|
|
313
|
-
* POST /api/v1/organizations/:organizationId/members
|
|
314
|
-
* Add member to organization
|
|
315
|
-
*/
|
|
316
|
-
app.post('/api/v1/organizations/:organizationId/members', writeLimiter, authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
317
|
-
try {
|
|
318
|
-
// Check permission
|
|
319
|
-
const canInvite = await organizationService.hasPermission(req.params.organizationId, getAuthenticatedUser(req).id, 'canInviteMembers');
|
|
320
|
-
if (!canInvite) {
|
|
321
|
-
return ApiErrors.forbidden(res, 'No permission to invite members');
|
|
322
|
-
}
|
|
323
|
-
const { userId, role } = req.body;
|
|
324
|
-
const member = await organizationService.addMember({
|
|
325
|
-
organizationId: req.params.organizationId,
|
|
326
|
-
userId,
|
|
327
|
-
role: role || 'member',
|
|
328
|
-
invitedBy: getAuthenticatedUser(req).id,
|
|
329
|
-
});
|
|
330
|
-
return sendSuccess(res, { member });
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
// ============================================================================
|
|
337
|
-
// TEAM ROUTES
|
|
338
|
-
// ============================================================================
|
|
339
|
-
/**
|
|
340
|
-
* POST /api/v1/organizations/:organizationId/teams
|
|
341
|
-
* Create a team
|
|
342
|
-
*/
|
|
343
|
-
app.post('/api/v1/organizations/:organizationId/teams', writeLimiter, authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
344
|
-
try {
|
|
345
|
-
const { name, slug, description } = req.body;
|
|
346
|
-
if (!name) {
|
|
347
|
-
return ApiErrors.invalidInput(res, 'Team name required');
|
|
348
|
-
}
|
|
349
|
-
const team = await teamService.createTeam({
|
|
350
|
-
organizationId: req.params.organizationId,
|
|
351
|
-
name,
|
|
352
|
-
slug,
|
|
353
|
-
description,
|
|
354
|
-
}, getAuthenticatedUser(req).id);
|
|
355
|
-
return sendSuccess(res, { team });
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
/**
|
|
362
|
-
* GET /api/v1/organizations/:organizationId/teams
|
|
363
|
-
* Get organization teams
|
|
364
|
-
*/
|
|
365
|
-
app.get('/api/v1/organizations/:organizationId/teams', authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
366
|
-
try {
|
|
367
|
-
const teams = await teamService.getOrganizationTeams(req.params.organizationId);
|
|
368
|
-
return sendSuccess(res, { teams });
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
// ============================================================================
|
|
375
|
-
// SECRETS ROUTES
|
|
376
|
-
// ============================================================================
|
|
377
|
-
/**
|
|
378
|
-
* POST /api/v1/teams/:teamId/secrets
|
|
379
|
-
* Create a new secret
|
|
380
|
-
*/
|
|
381
|
-
app.post('/api/v1/teams/:teamId/secrets', writeLimiter, authenticateUser, async (req, res) => {
|
|
382
|
-
try {
|
|
383
|
-
const { environment, key, value, description, tags, rotationIntervalDays } = req.body;
|
|
384
|
-
if (!environment || !key || !value) {
|
|
385
|
-
return ApiErrors.invalidInput(res, 'Environment, key, and value required');
|
|
386
|
-
}
|
|
387
|
-
const secret = await secretsService.createSecret({
|
|
388
|
-
teamId: req.params.teamId,
|
|
389
|
-
environment,
|
|
390
|
-
key,
|
|
391
|
-
value,
|
|
392
|
-
description,
|
|
393
|
-
tags,
|
|
394
|
-
rotationIntervalDays,
|
|
395
|
-
createdBy: getAuthenticatedUser(req).id,
|
|
396
|
-
});
|
|
397
|
-
return sendSuccess(res, { secret: { ...secret, encryptedValue: '***REDACTED***' } });
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
const msg = extractApiErrorMessage(error);
|
|
401
|
-
if (msg.includes('TIER_LIMIT_EXCEEDED')) {
|
|
402
|
-
return ApiErrors.tierLimitExceeded(res, msg);
|
|
403
|
-
}
|
|
404
|
-
return ApiErrors.internalError(res, msg);
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
/**
|
|
408
|
-
* GET /api/v1/teams/:teamId/secrets
|
|
409
|
-
* Get team secrets
|
|
410
|
-
*/
|
|
411
|
-
app.get('/api/v1/teams/:teamId/secrets', authenticateUser, async (req, res) => {
|
|
412
|
-
try {
|
|
413
|
-
const { environment, decrypt } = req.query;
|
|
414
|
-
const secrets = await secretsService.getTeamSecrets(req.params.teamId, environment, decrypt === 'true');
|
|
415
|
-
// Mask encrypted values unless decryption was requested
|
|
416
|
-
const maskedSecrets = secrets.map((secret) => ({
|
|
417
|
-
...secret,
|
|
418
|
-
encryptedValue: decrypt === 'true' ? secret.encryptedValue : '***REDACTED***',
|
|
419
|
-
}));
|
|
420
|
-
return sendSuccess(res, { secrets: maskedSecrets });
|
|
421
|
-
}
|
|
422
|
-
catch (error) {
|
|
423
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
/**
|
|
427
|
-
* POST /api/v1/teams/:teamId/secrets/:secretId/retrieve
|
|
428
|
-
* Get secret by ID (POST method to prevent sensitive flags in GET logs)
|
|
429
|
-
* Request body: { decrypt: boolean }
|
|
430
|
-
*/
|
|
431
|
-
app.post('/api/v1/teams/:teamId/secrets/:secretId/retrieve', authenticateUser, async (req, res) => {
|
|
432
|
-
try {
|
|
433
|
-
// Use POST body for decrypt flag to prevent exposure in logs/URLs
|
|
434
|
-
const decrypt = req.body?.decrypt === true;
|
|
435
|
-
const secret = await secretsService.getSecretById(req.params.secretId, decrypt);
|
|
436
|
-
if (!secret) {
|
|
437
|
-
return ApiErrors.notFound(res, 'Secret');
|
|
438
|
-
}
|
|
439
|
-
return sendSuccess(res, {
|
|
440
|
-
secret: {
|
|
441
|
-
...secret,
|
|
442
|
-
encryptedValue: decrypt ? secret.encryptedValue : '***REDACTED***',
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
/**
|
|
451
|
-
* PUT /api/v1/teams/:teamId/secrets/:secretId
|
|
452
|
-
* Update secret
|
|
453
|
-
*/
|
|
454
|
-
app.put('/api/v1/teams/:teamId/secrets/:secretId', writeLimiter, authenticateUser, async (req, res) => {
|
|
455
|
-
try {
|
|
456
|
-
const { value, description, tags, rotationIntervalDays } = req.body;
|
|
457
|
-
const secret = await secretsService.updateSecret(req.params.secretId, {
|
|
458
|
-
value,
|
|
459
|
-
description,
|
|
460
|
-
tags,
|
|
461
|
-
rotationIntervalDays,
|
|
462
|
-
updatedBy: getAuthenticatedUser(req).id,
|
|
463
|
-
});
|
|
464
|
-
return sendSuccess(res, { secret: { ...secret, encryptedValue: '***REDACTED***' } });
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
/**
|
|
471
|
-
* DELETE /api/v1/teams/:teamId/secrets/:secretId
|
|
472
|
-
* Delete secret
|
|
473
|
-
*/
|
|
474
|
-
app.delete('/api/v1/teams/:teamId/secrets/:secretId', writeLimiter, authenticateUser, async (req, res) => {
|
|
475
|
-
try {
|
|
476
|
-
await secretsService.deleteSecret(req.params.secretId, getAuthenticatedUser(req).id);
|
|
477
|
-
return sendSuccess(res, { message: 'Secret deleted successfully' });
|
|
478
|
-
}
|
|
479
|
-
catch (error) {
|
|
480
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
/**
|
|
484
|
-
* GET /api/v1/teams/:teamId/secrets/export/env
|
|
485
|
-
* Export secrets to .env format
|
|
486
|
-
*/
|
|
487
|
-
app.get('/api/v1/teams/:teamId/secrets/export/env', authenticateUser, async (req, res) => {
|
|
488
|
-
try {
|
|
489
|
-
const { environment } = req.query;
|
|
490
|
-
if (!environment) {
|
|
491
|
-
return ApiErrors.invalidInput(res, 'Environment parameter required');
|
|
492
|
-
}
|
|
493
|
-
const envContent = await secretsService.exportToEnv(req.params.teamId, environment);
|
|
494
|
-
res.setHeader('Content-Type', 'text/plain');
|
|
495
|
-
res.setHeader('Content-Disposition', `attachment; filename="${environment}.env"`);
|
|
496
|
-
res.send(envContent);
|
|
497
|
-
}
|
|
498
|
-
catch (error) {
|
|
499
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
/**
|
|
503
|
-
* POST /api/v1/teams/:teamId/secrets/import/env
|
|
504
|
-
* Import secrets from .env format
|
|
505
|
-
*/
|
|
506
|
-
app.post('/api/v1/teams/:teamId/secrets/import/env', writeLimiter, authenticateUser, async (req, res) => {
|
|
507
|
-
try {
|
|
508
|
-
const { environment, content } = req.body;
|
|
509
|
-
if (!environment || !content) {
|
|
510
|
-
return ApiErrors.invalidInput(res, 'Environment and content required');
|
|
511
|
-
}
|
|
512
|
-
const result = await secretsService.importFromEnv(req.params.teamId, environment, content, getAuthenticatedUser(req).id);
|
|
513
|
-
return sendSuccess(res, result);
|
|
514
|
-
}
|
|
515
|
-
catch (error) {
|
|
516
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
// ============================================================================
|
|
520
|
-
// AUDIT LOG ROUTES
|
|
521
|
-
// ============================================================================
|
|
522
|
-
/**
|
|
523
|
-
* GET /api/v1/organizations/:organizationId/audit-logs
|
|
524
|
-
* Get organization audit logs
|
|
525
|
-
*/
|
|
526
|
-
app.get('/api/v1/organizations/:organizationId/audit-logs', authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
527
|
-
try {
|
|
528
|
-
// Check permission
|
|
529
|
-
const canView = await organizationService.hasPermission(req.params.organizationId, getAuthenticatedUser(req).id, 'canViewAuditLogs');
|
|
530
|
-
if (!canView) {
|
|
531
|
-
return ApiErrors.forbidden(res, 'No permission to view audit logs');
|
|
532
|
-
}
|
|
533
|
-
const { limit = 50, offset = 0, action, userId, teamId, startDate, endDate } = req.query;
|
|
534
|
-
const result = await auditLogger.getOrganizationLogs(req.params.organizationId, {
|
|
535
|
-
limit: parseInt(limit),
|
|
536
|
-
offset: parseInt(offset),
|
|
537
|
-
action: action,
|
|
538
|
-
userId: userId,
|
|
539
|
-
teamId: teamId,
|
|
540
|
-
startDate: startDate ? new Date(startDate) : undefined,
|
|
541
|
-
endDate: endDate ? new Date(endDate) : undefined,
|
|
542
|
-
});
|
|
543
|
-
return sendSuccess(res, result);
|
|
544
|
-
}
|
|
545
|
-
catch (error) {
|
|
546
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
// ============================================================================
|
|
550
|
-
// BILLING ROUTES
|
|
551
|
-
// ============================================================================
|
|
552
|
-
/**
|
|
553
|
-
* POST /api/v1/organizations/:organizationId/billing/checkout
|
|
554
|
-
* Create Stripe checkout session
|
|
555
|
-
*/
|
|
556
|
-
app.post('/api/v1/organizations/:organizationId/billing/checkout', writeLimiter, authenticateUser, requireOrganizationMembership, async (req, res) => {
|
|
557
|
-
try {
|
|
558
|
-
// Check permission
|
|
559
|
-
const canManage = await organizationService.hasPermission(req.params.organizationId, getAuthenticatedUser(req).id, 'canManageBilling');
|
|
560
|
-
if (!canManage) {
|
|
561
|
-
return ApiErrors.forbidden(res, 'No permission to manage billing');
|
|
562
|
-
}
|
|
563
|
-
const { tier, billingPeriod, successUrl, cancelUrl } = req.body;
|
|
564
|
-
const org = await organizationService.getOrganizationById(req.params.organizationId);
|
|
565
|
-
if (!org) {
|
|
566
|
-
return ApiErrors.notFound(res, 'Organization');
|
|
567
|
-
}
|
|
568
|
-
const checkout = await billingService.createCheckoutSession({
|
|
569
|
-
organizationId: req.params.organizationId,
|
|
570
|
-
tier,
|
|
571
|
-
billingPeriod: billingPeriod || 'monthly',
|
|
572
|
-
successUrl,
|
|
573
|
-
cancelUrl,
|
|
574
|
-
customerId: org.stripeCustomerId || undefined,
|
|
575
|
-
});
|
|
576
|
-
return sendSuccess(res, checkout);
|
|
577
|
-
}
|
|
578
|
-
catch (error) {
|
|
579
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
/**
|
|
583
|
-
* POST /api/v1/billing/webhooks
|
|
584
|
-
* Handle Stripe webhooks
|
|
585
|
-
*/
|
|
586
|
-
app.post('/api/v1/billing/webhooks', async (req, res) => {
|
|
587
|
-
try {
|
|
588
|
-
const signature = req.headers['stripe-signature'];
|
|
589
|
-
const payload = JSON.stringify(req.body);
|
|
590
|
-
await billingService.handleWebhook(payload, signature);
|
|
591
|
-
return sendSuccess(res, { received: true });
|
|
592
|
-
}
|
|
593
|
-
catch (error) {
|
|
594
|
-
console.error('Webhook error:', error);
|
|
595
|
-
return ApiErrors.internalError(res, extractApiErrorMessage(error));
|
|
596
|
-
}
|
|
597
|
-
});
|
|
598
|
-
console.log('✅ SaaS API routes registered');
|
|
599
|
-
}
|