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