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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -34
  3. package/dist/commands/ipfs.js +7 -12
  4. package/dist/commands/self.js +22 -16
  5. package/dist/commands/sync.js +49 -38
  6. package/dist/constants/config.js +3 -0
  7. package/dist/lib/floating-point-arithmetic.js +2 -2
  8. package/dist/lib/ipfs-client-manager.js +51 -13
  9. package/dist/lib/ipfs-secrets-storage.js +21 -16
  10. package/dist/lib/ipfs-sync.js +88 -14
  11. package/dist/lib/secrets-manager.js +117 -47
  12. package/dist/lib/sync-key-store.js +87 -0
  13. package/dist/services/secrets/secrets.js +77 -39
  14. package/package.json +16 -16
  15. package/dist/__tests__/fixtures/job-fixtures.js +0 -204
  16. package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
  17. package/dist/daemon/job-registry.js +0 -556
  18. package/dist/daemon/lshd.js +0 -968
  19. package/dist/daemon/saas-api-routes.js +0 -599
  20. package/dist/daemon/saas-api-server.js +0 -231
  21. package/dist/examples/supabase-integration.js +0 -106
  22. package/dist/lib/api-response.js +0 -226
  23. package/dist/lib/base-command-registrar.js +0 -287
  24. package/dist/lib/base-job-manager.js +0 -295
  25. package/dist/lib/cloud-config-manager.js +0 -348
  26. package/dist/lib/cron-job-manager.js +0 -368
  27. package/dist/lib/daemon-client-helper.js +0 -145
  28. package/dist/lib/daemon-client.js +0 -513
  29. package/dist/lib/database-persistence.js +0 -727
  30. package/dist/lib/database-schema.js +0 -259
  31. package/dist/lib/database-types.js +0 -90
  32. package/dist/lib/enhanced-history-system.js +0 -247
  33. package/dist/lib/history-system.js +0 -246
  34. package/dist/lib/job-manager.js +0 -436
  35. package/dist/lib/job-storage-database.js +0 -164
  36. package/dist/lib/job-storage-memory.js +0 -73
  37. package/dist/lib/local-storage-adapter.js +0 -507
  38. package/dist/lib/optimized-job-scheduler.js +0 -356
  39. package/dist/lib/saas-audit.js +0 -215
  40. package/dist/lib/saas-auth.js +0 -465
  41. package/dist/lib/saas-billing.js +0 -503
  42. package/dist/lib/saas-email.js +0 -403
  43. package/dist/lib/saas-encryption.js +0 -221
  44. package/dist/lib/saas-organizations.js +0 -662
  45. package/dist/lib/saas-secrets.js +0 -408
  46. package/dist/lib/saas-types.js +0 -165
  47. package/dist/lib/supabase-client.js +0 -125
  48. package/dist/lib/supabase-utils.js +0 -396
  49. package/dist/services/cron/cron-registrar.js +0 -240
  50. package/dist/services/cron/cron.js +0 -9
  51. package/dist/services/daemon/daemon-registrar.js +0 -585
  52. package/dist/services/daemon/daemon.js +0 -9
  53. package/dist/services/supabase/supabase-registrar.js +0 -375
  54. 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
- }