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,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();