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,503 +0,0 @@
1
- /**
2
- * LSH SaaS Billing Service
3
- * Stripe integration for subscriptions and billing
4
- */
5
- import { getSupabaseClient } from './supabase-client.js';
6
- import { auditLogger } from './saas-audit.js';
7
- import { ENV_VARS } from '../constants/index.js';
8
- /**
9
- * Stripe Pricing IDs (set via environment variables)
10
- */
11
- export const STRIPE_PRICE_IDS = {
12
- pro_monthly: process.env[ENV_VARS.STRIPE_PRICE_PRO_MONTHLY] || '',
13
- pro_yearly: process.env[ENV_VARS.STRIPE_PRICE_PRO_YEARLY] || '',
14
- enterprise_monthly: process.env[ENV_VARS.STRIPE_PRICE_ENTERPRISE_MONTHLY] || '',
15
- enterprise_yearly: process.env[ENV_VARS.STRIPE_PRICE_ENTERPRISE_YEARLY] || '',
16
- };
17
- /**
18
- * Billing Service
19
- */
20
- export class BillingService {
21
- supabase = getSupabaseClient();
22
- stripeSecretKey = process.env[ENV_VARS.STRIPE_SECRET_KEY] || '';
23
- stripeWebhookSecret = process.env[ENV_VARS.STRIPE_WEBHOOK_SECRET] || '';
24
- stripeApiUrl = 'https://api.stripe.com/v1';
25
- /**
26
- * Create Stripe customer
27
- */
28
- async createStripeCustomer(params) {
29
- if (!this.stripeSecretKey) {
30
- throw new Error('STRIPE_SECRET_KEY not configured');
31
- }
32
- const formData = new URLSearchParams();
33
- formData.append('email', params.email);
34
- if (params.name) {
35
- formData.append('name', params.name);
36
- }
37
- formData.append('metadata[organization_id]', params.organizationId);
38
- const response = await fetch(`${this.stripeApiUrl}/customers`, {
39
- method: 'POST',
40
- headers: {
41
- Authorization: `Bearer ${this.stripeSecretKey}`,
42
- 'Content-Type': 'application/x-www-form-urlencoded',
43
- },
44
- body: formData,
45
- });
46
- if (!response.ok) {
47
- const error = await response.text();
48
- throw new Error(`Failed to create Stripe customer: ${error}`);
49
- }
50
- const customer = await response.json();
51
- // Update organization with Stripe customer ID
52
- await this.supabase
53
- .from('organizations')
54
- .update({ stripe_customer_id: customer.id })
55
- .eq('id', params.organizationId);
56
- return customer.id;
57
- }
58
- /**
59
- * Create checkout session
60
- */
61
- async createCheckoutSession(params) {
62
- if (!this.stripeSecretKey) {
63
- throw new Error('STRIPE_SECRET_KEY not configured');
64
- }
65
- // Get price ID
66
- const priceKey = `${params.tier}_${params.billingPeriod}`;
67
- const priceId = STRIPE_PRICE_IDS[priceKey];
68
- if (!priceId) {
69
- throw new Error(`No Stripe price configured for ${params.tier} ${params.billingPeriod}`);
70
- }
71
- const formData = new URLSearchParams();
72
- formData.append('mode', 'subscription');
73
- formData.append('line_items[0][price]', priceId);
74
- formData.append('line_items[0][quantity]', '1');
75
- formData.append('success_url', params.successUrl);
76
- formData.append('cancel_url', params.cancelUrl);
77
- formData.append('metadata[organization_id]', params.organizationId);
78
- formData.append('subscription_data[metadata][organization_id]', params.organizationId);
79
- if (params.customerId) {
80
- formData.append('customer', params.customerId);
81
- }
82
- const response = await fetch(`${this.stripeApiUrl}/checkout/sessions`, {
83
- method: 'POST',
84
- headers: {
85
- Authorization: `Bearer ${this.stripeSecretKey}`,
86
- 'Content-Type': 'application/x-www-form-urlencoded',
87
- },
88
- body: formData,
89
- });
90
- if (!response.ok) {
91
- const error = await response.text();
92
- throw new Error(`Failed to create checkout session: ${error}`);
93
- }
94
- const session = await response.json();
95
- return {
96
- sessionId: session.id,
97
- url: session.url,
98
- };
99
- }
100
- /**
101
- * Create billing portal session
102
- */
103
- async createPortalSession(customerId, returnUrl) {
104
- if (!this.stripeSecretKey) {
105
- throw new Error('STRIPE_SECRET_KEY not configured');
106
- }
107
- const formData = new URLSearchParams();
108
- formData.append('customer', customerId);
109
- formData.append('return_url', returnUrl);
110
- const response = await fetch(`${this.stripeApiUrl}/billing_portal/sessions`, {
111
- method: 'POST',
112
- headers: {
113
- Authorization: `Bearer ${this.stripeSecretKey}`,
114
- 'Content-Type': 'application/x-www-form-urlencoded',
115
- },
116
- body: formData,
117
- });
118
- if (!response.ok) {
119
- const error = await response.text();
120
- throw new Error(`Failed to create portal session: ${error}`);
121
- }
122
- const session = await response.json();
123
- return session.url;
124
- }
125
- /**
126
- * Handle Stripe webhook
127
- */
128
- async handleWebhook(payload, signature) {
129
- if (!this.stripeWebhookSecret) {
130
- throw new Error('STRIPE_WEBHOOK_SECRET not configured');
131
- }
132
- // Verify webhook signature
133
- const event = this.verifyWebhookSignature(payload, signature);
134
- // Handle different event types
135
- switch (event.type) {
136
- case 'checkout.session.completed':
137
- await this.handleCheckoutCompleted(event.data.object);
138
- break;
139
- case 'customer.subscription.created':
140
- case 'customer.subscription.updated':
141
- await this.handleSubscriptionUpdated(event.data.object);
142
- break;
143
- case 'customer.subscription.deleted':
144
- await this.handleSubscriptionDeleted(event.data.object);
145
- break;
146
- case 'invoice.paid':
147
- await this.handleInvoicePaid(event.data.object);
148
- break;
149
- case 'invoice.payment_failed':
150
- await this.handleInvoicePaymentFailed(event.data.object);
151
- break;
152
- default:
153
- console.log(`Unhandled webhook event: ${event.type}`);
154
- }
155
- }
156
- /**
157
- * Verify Stripe webhook signature and parse event payload.
158
- *
159
- * In production, this should use Stripe's signature verification:
160
- * ```typescript
161
- * const event = stripe.webhooks.constructEvent(
162
- * payload, signature, this.stripeWebhookSecret
163
- * );
164
- * ```
165
- *
166
- * Current implementation parses JSON without verification (TODO: implement proper verification).
167
- *
168
- * @param payload - Raw webhook body as string
169
- * @param _signature - Stripe-Signature header value (not yet used)
170
- * @returns Parsed Stripe event object
171
- * @throws Error if payload is not valid JSON
172
- * @see https://stripe.com/docs/webhooks/signatures
173
- */
174
- verifyWebhookSignature(payload, _signature) {
175
- // In production, use Stripe's webhook signature verification
176
- // For now, just parse the payload
177
- try {
178
- return JSON.parse(payload);
179
- }
180
- catch (_error) {
181
- throw new Error('Invalid webhook payload');
182
- }
183
- }
184
- /**
185
- * Handle Stripe checkout.session.completed webhook event.
186
- *
187
- * Called when a customer completes checkout. The actual subscription
188
- * creation is handled by the customer.subscription.created event.
189
- *
190
- * Extracts organization_id from session.metadata to link the checkout
191
- * to the correct organization.
192
- *
193
- * @param session - Stripe checkout session object
194
- * @see StripeCheckoutSession in database-types.ts for partial type
195
- */
196
- async handleCheckoutCompleted(session) {
197
- const organizationId = session.metadata?.organization_id;
198
- if (!organizationId) {
199
- console.error('No organization_id in checkout session metadata');
200
- return;
201
- }
202
- // Subscription will be created via customer.subscription.created event
203
- console.log(`Checkout completed for organization ${organizationId}`);
204
- }
205
- /**
206
- * Handle Stripe customer.subscription.created/updated webhook events.
207
- *
208
- * Creates or updates subscription record in database and syncs tier
209
- * to the organization. Key operations:
210
- * 1. Extracts organization_id from subscription.metadata
211
- * 2. Determines tier from price ID (maps Stripe price → 'free' | 'pro' | 'enterprise')
212
- * 3. Upserts subscription record with all billing details
213
- * 4. Updates organization's subscription_tier and subscription_status
214
- * 5. Logs audit event
215
- *
216
- * Timestamps from Stripe are Unix timestamps (seconds), converted to ISO strings.
217
- *
218
- * @param subscription - Stripe subscription object
219
- * @see StripeSubscriptionEvent in database-types.ts for partial type
220
- */
221
- async handleSubscriptionUpdated(subscription) {
222
- const organizationId = subscription.metadata?.organization_id;
223
- if (!organizationId) {
224
- console.error('No organization_id in subscription metadata');
225
- return;
226
- }
227
- // Determine tier from price ID
228
- const priceId = subscription.items?.data[0]?.price?.id;
229
- const tier = this.getTierFromPriceId(priceId);
230
- // Upsert subscription
231
- const { error } = await this.supabase
232
- .from('subscriptions')
233
- .upsert({
234
- organization_id: organizationId,
235
- stripe_subscription_id: subscription.id,
236
- stripe_price_id: priceId,
237
- stripe_product_id: subscription.items?.data[0]?.price?.product,
238
- tier,
239
- status: subscription.status,
240
- current_period_start: new Date(subscription.current_period_start * 1000).toISOString(),
241
- current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
242
- cancel_at_period_end: subscription.cancel_at_period_end,
243
- trial_start: subscription.trial_start
244
- ? new Date(subscription.trial_start * 1000).toISOString()
245
- : null,
246
- trial_end: subscription.trial_end
247
- ? new Date(subscription.trial_end * 1000).toISOString()
248
- : null,
249
- }, { onConflict: 'stripe_subscription_id' });
250
- if (error) {
251
- console.error('Failed to upsert subscription:', error);
252
- return;
253
- }
254
- // Update organization tier
255
- await this.supabase
256
- .from('organizations')
257
- .update({
258
- subscription_tier: tier,
259
- subscription_status: subscription.status,
260
- subscription_expires_at: new Date(subscription.current_period_end * 1000).toISOString(),
261
- })
262
- .eq('id', organizationId);
263
- await auditLogger.log({
264
- organizationId,
265
- action: 'billing.subscription_updated',
266
- resourceType: 'subscription',
267
- resourceId: subscription.id,
268
- newValue: { tier, status: subscription.status },
269
- });
270
- }
271
- /**
272
- * Handle Stripe customer.subscription.deleted webhook event.
273
- *
274
- * Called when a subscription is canceled (immediate or at period end).
275
- * Operations:
276
- * 1. Marks subscription as 'canceled' with canceled_at timestamp
277
- * 2. Downgrades organization to 'free' tier
278
- * 3. Updates organization subscription_status to 'canceled'
279
- * 4. Logs audit event
280
- *
281
- * @param subscription - Stripe subscription object
282
- * @see StripeSubscriptionEvent in database-types.ts for partial type
283
- */
284
- async handleSubscriptionDeleted(subscription) {
285
- const organizationId = subscription.metadata?.organization_id;
286
- if (!organizationId) {
287
- console.error('No organization_id in subscription metadata');
288
- return;
289
- }
290
- // Mark subscription as canceled
291
- await this.supabase
292
- .from('subscriptions')
293
- .update({
294
- status: 'canceled',
295
- canceled_at: new Date().toISOString(),
296
- })
297
- .eq('stripe_subscription_id', subscription.id);
298
- // Downgrade organization to free tier
299
- await this.supabase
300
- .from('organizations')
301
- .update({
302
- subscription_tier: 'free',
303
- subscription_status: 'canceled',
304
- })
305
- .eq('id', organizationId);
306
- await auditLogger.log({
307
- organizationId,
308
- action: 'billing.subscription_canceled',
309
- resourceType: 'subscription',
310
- resourceId: subscription.id,
311
- });
312
- }
313
- /**
314
- * Handle Stripe invoice.paid webhook event.
315
- *
316
- * Records successful payment in the invoices table. Extracts organization_id
317
- * from invoice.subscription_metadata (set during checkout).
318
- *
319
- * Amounts are in cents (e.g., 1000 = $10.00). Currency is uppercased.
320
- *
321
- * @param invoice - Stripe invoice object
322
- * @see StripeInvoiceEvent in database-types.ts for partial type
323
- */
324
- async handleInvoicePaid(invoice) {
325
- const organizationId = invoice.subscription_metadata?.organization_id;
326
- if (!organizationId) {
327
- return;
328
- }
329
- // Record invoice
330
- await this.supabase.from('invoices').upsert({
331
- organization_id: organizationId,
332
- stripe_invoice_id: invoice.id,
333
- number: invoice.number,
334
- amount_due: invoice.amount_due,
335
- amount_paid: invoice.amount_paid,
336
- currency: invoice.currency?.toUpperCase() || 'USD',
337
- status: 'paid',
338
- invoice_date: new Date(invoice.created * 1000).toISOString(),
339
- paid_at: invoice.status_transitions?.paid_at ? new Date(invoice.status_transitions.paid_at * 1000).toISOString() : null,
340
- invoice_pdf_url: invoice.invoice_pdf,
341
- }, { onConflict: 'stripe_invoice_id' });
342
- }
343
- /**
344
- * Handle Stripe invoice.payment_failed webhook event.
345
- *
346
- * Called when payment fails (declined card, insufficient funds, etc.).
347
- * Updates organization subscription_status to 'past_due' and logs audit event.
348
- *
349
- * Note: Does not immediately downgrade tier. Stripe will retry payment
350
- * according to your dunning settings. Downgrade happens on subscription.deleted.
351
- *
352
- * @param invoice - Stripe invoice object
353
- * @see StripeInvoiceEvent in database-types.ts for partial type
354
- */
355
- async handleInvoicePaymentFailed(invoice) {
356
- const organizationId = invoice.subscription_metadata?.organization_id;
357
- if (!organizationId) {
358
- return;
359
- }
360
- // Update organization status
361
- await this.supabase
362
- .from('organizations')
363
- .update({
364
- subscription_status: 'past_due',
365
- })
366
- .eq('id', organizationId);
367
- await auditLogger.log({
368
- organizationId,
369
- action: 'billing.payment_failed',
370
- resourceType: 'subscription',
371
- metadata: { invoice_id: invoice.id },
372
- });
373
- }
374
- /**
375
- * Get tier from Stripe price ID
376
- */
377
- getTierFromPriceId(priceId) {
378
- if (!priceId)
379
- return 'free';
380
- if (priceId === STRIPE_PRICE_IDS.pro_monthly ||
381
- priceId === STRIPE_PRICE_IDS.pro_yearly) {
382
- return 'pro';
383
- }
384
- if (priceId === STRIPE_PRICE_IDS.enterprise_monthly ||
385
- priceId === STRIPE_PRICE_IDS.enterprise_yearly) {
386
- return 'enterprise';
387
- }
388
- return 'free';
389
- }
390
- /**
391
- * Get subscription for organization
392
- */
393
- async getOrganizationSubscription(organizationId) {
394
- const { data, error } = await this.supabase
395
- .from('subscriptions')
396
- .select('*')
397
- .eq('organization_id', organizationId)
398
- .order('created_at', { ascending: false })
399
- .limit(1)
400
- .single();
401
- if (error || !data) {
402
- return null;
403
- }
404
- return this.mapDbSubscriptionToSubscription(data);
405
- }
406
- /**
407
- * Get invoices for organization
408
- */
409
- async getOrganizationInvoices(organizationId) {
410
- const { data, error } = await this.supabase
411
- .from('invoices')
412
- .select('*')
413
- .eq('organization_id', organizationId)
414
- .order('invoice_date', { ascending: false });
415
- if (error) {
416
- throw new Error(`Failed to get invoices: ${error.message}`);
417
- }
418
- return (data || []).map(this.mapDbInvoiceToInvoice);
419
- }
420
- /**
421
- * Transform Supabase subscription record to domain model.
422
- *
423
- * Maps database snake_case columns to TypeScript camelCase properties:
424
- * - `organization_id` → `organizationId`
425
- * - `stripe_subscription_id` → `stripeSubscriptionId` (Stripe sub_xxx ID)
426
- * - `stripe_price_id` → `stripePriceId` (Stripe price_xxx ID)
427
- * - `stripe_product_id` → `stripeProductId` (Stripe prod_xxx ID)
428
- * - `tier` → `tier` (SubscriptionTier: 'free' | 'pro' | 'enterprise')
429
- * - `status` → `status` (SubscriptionStatus)
430
- * - `current_period_start` → `currentPeriodStart` (nullable Date)
431
- * - `current_period_end` → `currentPeriodEnd` (nullable Date)
432
- * - `cancel_at_period_end` → `cancelAtPeriodEnd` (boolean)
433
- * - `trial_start` → `trialStart` (nullable Date)
434
- * - `trial_end` → `trialEnd` (nullable Date)
435
- * - `canceled_at` → `canceledAt` (nullable Date)
436
- *
437
- * @param dbSub - Supabase record from 'subscriptions' table
438
- * @returns Domain Subscription object
439
- * @see DbSubscriptionRecord in database-types.ts for input shape
440
- * @see Subscription in saas-types.ts for output shape
441
- */
442
- mapDbSubscriptionToSubscription(dbSub) {
443
- return {
444
- id: dbSub.id,
445
- organizationId: dbSub.organization_id,
446
- stripeSubscriptionId: dbSub.stripe_subscription_id,
447
- stripePriceId: dbSub.stripe_price_id,
448
- stripeProductId: dbSub.stripe_product_id,
449
- tier: dbSub.tier,
450
- status: dbSub.status,
451
- currentPeriodStart: dbSub.current_period_start
452
- ? new Date(dbSub.current_period_start)
453
- : null,
454
- currentPeriodEnd: dbSub.current_period_end ? new Date(dbSub.current_period_end) : null,
455
- cancelAtPeriodEnd: dbSub.cancel_at_period_end,
456
- trialStart: dbSub.trial_start ? new Date(dbSub.trial_start) : null,
457
- trialEnd: dbSub.trial_end ? new Date(dbSub.trial_end) : null,
458
- createdAt: new Date(dbSub.created_at),
459
- updatedAt: new Date(dbSub.updated_at),
460
- canceledAt: dbSub.canceled_at ? new Date(dbSub.canceled_at) : null,
461
- };
462
- }
463
- /**
464
- * Transform Supabase invoice record to domain model.
465
- *
466
- * Maps database snake_case columns to TypeScript camelCase properties:
467
- * - `organization_id` → `organizationId`
468
- * - `stripe_invoice_id` → `stripeInvoiceId` (Stripe in_xxx ID)
469
- * - `amount_due` → `amountDue` (in cents, e.g., 1000 = $10.00)
470
- * - `amount_paid` → `amountPaid` (in cents)
471
- * - `invoice_date` → `invoiceDate` (Date)
472
- * - `due_date` → `dueDate` (nullable Date)
473
- * - `paid_at` → `paidAt` (nullable Date)
474
- * - `invoice_pdf_url` → `invoicePdfUrl` (Stripe-hosted PDF URL)
475
- *
476
- * @param dbInvoice - Supabase record from 'invoices' table
477
- * @returns Domain Invoice object
478
- * @see DbInvoiceRecord in database-types.ts for input shape
479
- * @see Invoice in saas-types.ts for output shape
480
- */
481
- mapDbInvoiceToInvoice(dbInvoice) {
482
- return {
483
- id: dbInvoice.id,
484
- organizationId: dbInvoice.organization_id,
485
- stripeInvoiceId: dbInvoice.stripe_invoice_id,
486
- number: dbInvoice.number,
487
- amountDue: dbInvoice.amount_due,
488
- amountPaid: dbInvoice.amount_paid,
489
- currency: dbInvoice.currency,
490
- status: dbInvoice.status,
491
- invoiceDate: new Date(dbInvoice.invoice_date),
492
- dueDate: dbInvoice.due_date ? new Date(dbInvoice.due_date) : null,
493
- paidAt: dbInvoice.paid_at ? new Date(dbInvoice.paid_at) : null,
494
- invoicePdfUrl: dbInvoice.invoice_pdf_url,
495
- createdAt: new Date(dbInvoice.created_at),
496
- updatedAt: new Date(dbInvoice.updated_at),
497
- };
498
- }
499
- }
500
- /**
501
- * Singleton instance
502
- */
503
- export const billingService = new BillingService();