lsh-framework 3.1.6 → 3.1.8

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.
@@ -154,7 +154,22 @@ export class BillingService {
154
154
  }
155
155
  }
156
156
  /**
157
- * Verify webhook signature
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
158
173
  */
159
174
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe event structure
160
175
  verifyWebhookSignature(payload, _signature) {
@@ -168,7 +183,16 @@ export class BillingService {
168
183
  }
169
184
  }
170
185
  /**
171
- * Handle checkout completed
186
+ * Handle Stripe checkout.session.completed webhook event.
187
+ *
188
+ * Called when a customer completes checkout. The actual subscription
189
+ * creation is handled by the customer.subscription.created event.
190
+ *
191
+ * Extracts organization_id from session.metadata to link the checkout
192
+ * to the correct organization.
193
+ *
194
+ * @param session - Stripe checkout session object
195
+ * @see StripeCheckoutSession in database-types.ts for partial type
172
196
  */
173
197
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe checkout session object
174
198
  async handleCheckoutCompleted(session) {
@@ -181,7 +205,20 @@ export class BillingService {
181
205
  console.log(`Checkout completed for organization ${organizationId}`);
182
206
  }
183
207
  /**
184
- * Handle subscription updated
208
+ * Handle Stripe customer.subscription.created/updated webhook events.
209
+ *
210
+ * Creates or updates subscription record in database and syncs tier
211
+ * to the organization. Key operations:
212
+ * 1. Extracts organization_id from subscription.metadata
213
+ * 2. Determines tier from price ID (maps Stripe price → 'free' | 'pro' | 'enterprise')
214
+ * 3. Upserts subscription record with all billing details
215
+ * 4. Updates organization's subscription_tier and subscription_status
216
+ * 5. Logs audit event
217
+ *
218
+ * Timestamps from Stripe are Unix timestamps (seconds), converted to ISO strings.
219
+ *
220
+ * @param subscription - Stripe subscription object
221
+ * @see StripeSubscriptionEvent in database-types.ts for partial type
185
222
  */
186
223
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe subscription object
187
224
  async handleSubscriptionUpdated(subscription) {
@@ -235,7 +272,17 @@ export class BillingService {
235
272
  });
236
273
  }
237
274
  /**
238
- * Handle subscription deleted
275
+ * Handle Stripe customer.subscription.deleted webhook event.
276
+ *
277
+ * Called when a subscription is canceled (immediate or at period end).
278
+ * Operations:
279
+ * 1. Marks subscription as 'canceled' with canceled_at timestamp
280
+ * 2. Downgrades organization to 'free' tier
281
+ * 3. Updates organization subscription_status to 'canceled'
282
+ * 4. Logs audit event
283
+ *
284
+ * @param subscription - Stripe subscription object
285
+ * @see StripeSubscriptionEvent in database-types.ts for partial type
239
286
  */
240
287
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe subscription object
241
288
  async handleSubscriptionDeleted(subscription) {
@@ -268,7 +315,15 @@ export class BillingService {
268
315
  });
269
316
  }
270
317
  /**
271
- * Handle invoice paid
318
+ * Handle Stripe invoice.paid webhook event.
319
+ *
320
+ * Records successful payment in the invoices table. Extracts organization_id
321
+ * from invoice.subscription_metadata (set during checkout).
322
+ *
323
+ * Amounts are in cents (e.g., 1000 = $10.00). Currency is uppercased.
324
+ *
325
+ * @param invoice - Stripe invoice object
326
+ * @see StripeInvoiceEvent in database-types.ts for partial type
272
327
  */
273
328
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe invoice object
274
329
  async handleInvoicePaid(invoice) {
@@ -291,7 +346,16 @@ export class BillingService {
291
346
  }, { onConflict: 'stripe_invoice_id' });
292
347
  }
293
348
  /**
294
- * Handle invoice payment failed
349
+ * Handle Stripe invoice.payment_failed webhook event.
350
+ *
351
+ * Called when payment fails (declined card, insufficient funds, etc.).
352
+ * Updates organization subscription_status to 'past_due' and logs audit event.
353
+ *
354
+ * Note: Does not immediately downgrade tier. Stripe will retry payment
355
+ * according to your dunning settings. Downgrade happens on subscription.deleted.
356
+ *
357
+ * @param invoice - Stripe invoice object
358
+ * @see StripeInvoiceEvent in database-types.ts for partial type
295
359
  */
296
360
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe invoice object
297
361
  async handleInvoicePaymentFailed(invoice) {
@@ -358,7 +422,26 @@ export class BillingService {
358
422
  return (data || []).map(this.mapDbInvoiceToInvoice);
359
423
  }
360
424
  /**
361
- * Map database subscription to Subscription type
425
+ * Transform Supabase subscription record to domain model.
426
+ *
427
+ * Maps database snake_case columns to TypeScript camelCase properties:
428
+ * - `organization_id` → `organizationId`
429
+ * - `stripe_subscription_id` → `stripeSubscriptionId` (Stripe sub_xxx ID)
430
+ * - `stripe_price_id` → `stripePriceId` (Stripe price_xxx ID)
431
+ * - `stripe_product_id` → `stripeProductId` (Stripe prod_xxx ID)
432
+ * - `tier` → `tier` (SubscriptionTier: 'free' | 'pro' | 'enterprise')
433
+ * - `status` → `status` (SubscriptionStatus)
434
+ * - `current_period_start` → `currentPeriodStart` (nullable Date)
435
+ * - `current_period_end` → `currentPeriodEnd` (nullable Date)
436
+ * - `cancel_at_period_end` → `cancelAtPeriodEnd` (boolean)
437
+ * - `trial_start` → `trialStart` (nullable Date)
438
+ * - `trial_end` → `trialEnd` (nullable Date)
439
+ * - `canceled_at` → `canceledAt` (nullable Date)
440
+ *
441
+ * @param dbSub - Supabase record from 'subscriptions' table
442
+ * @returns Domain Subscription object
443
+ * @see DbSubscriptionRecord in database-types.ts for input shape
444
+ * @see Subscription in saas-types.ts for output shape
362
445
  */
363
446
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
364
447
  mapDbSubscriptionToSubscription(dbSub) {
@@ -383,7 +466,22 @@ export class BillingService {
383
466
  };
384
467
  }
385
468
  /**
386
- * Map database invoice to Invoice type
469
+ * Transform Supabase invoice record to domain model.
470
+ *
471
+ * Maps database snake_case columns to TypeScript camelCase properties:
472
+ * - `organization_id` → `organizationId`
473
+ * - `stripe_invoice_id` → `stripeInvoiceId` (Stripe in_xxx ID)
474
+ * - `amount_due` → `amountDue` (in cents, e.g., 1000 = $10.00)
475
+ * - `amount_paid` → `amountPaid` (in cents)
476
+ * - `invoice_date` → `invoiceDate` (Date)
477
+ * - `due_date` → `dueDate` (nullable Date)
478
+ * - `paid_at` → `paidAt` (nullable Date)
479
+ * - `invoice_pdf_url` → `invoicePdfUrl` (Stripe-hosted PDF URL)
480
+ *
481
+ * @param dbInvoice - Supabase record from 'invoices' table
482
+ * @returns Domain Invoice object
483
+ * @see DbInvoiceRecord in database-types.ts for input shape
484
+ * @see Invoice in saas-types.ts for output shape
387
485
  */
388
486
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
389
487
  mapDbInvoiceToInvoice(dbInvoice) {
@@ -331,7 +331,22 @@ export class OrganizationService {
331
331
  }
332
332
  }
333
333
  /**
334
- * Map database org to Organization type
334
+ * Transform Supabase organization record to domain model.
335
+ *
336
+ * Maps database snake_case columns to TypeScript camelCase properties:
337
+ * - `created_at` (ISO string) → `createdAt` (Date)
338
+ * - `subscription_tier` (string) → `subscriptionTier` (SubscriptionTier type)
339
+ * - `subscription_status` (string) → `subscriptionStatus` (SubscriptionStatus type)
340
+ * - `stripe_customer_id` → `stripeCustomerId`
341
+ * - `subscription_expires_at` → `subscriptionExpiresAt` (nullable Date)
342
+ * - `deleted_at` → `deletedAt` (nullable Date, for soft delete filtering)
343
+ *
344
+ * Settings are passed through as-is (JSONB column).
345
+ *
346
+ * @param dbOrg - Supabase record from 'organizations' table
347
+ * @returns Domain Organization object with validated types
348
+ * @see DbOrganizationRecord in database-types.ts for input shape
349
+ * @see Organization in saas-types.ts for output shape
335
350
  */
336
351
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
337
352
  mapDbOrgToOrg(dbOrg) {
@@ -352,7 +367,20 @@ export class OrganizationService {
352
367
  };
353
368
  }
354
369
  /**
355
- * Map database member to OrganizationMember type
370
+ * Transform Supabase organization member record to domain model.
371
+ *
372
+ * Maps database snake_case columns to TypeScript camelCase properties:
373
+ * - `organization_id` → `organizationId`
374
+ * - `user_id` → `userId`
375
+ * - `role` (string) → `role` (OrganizationRole type)
376
+ * - `invited_by` → `invitedBy` (nullable, FK to users.id)
377
+ * - `invited_at` (ISO string) → `invitedAt` (Date)
378
+ * - `accepted_at` (ISO string) → `acceptedAt` (nullable Date)
379
+ *
380
+ * @param dbMember - Supabase record from 'organization_members' table
381
+ * @returns Domain OrganizationMember object
382
+ * @see DbOrganizationMemberRecord in database-types.ts for input shape
383
+ * @see OrganizationMember in saas-types.ts for output shape
356
384
  */
357
385
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
358
386
  mapDbMemberToMember(dbMember) {
@@ -369,7 +397,24 @@ export class OrganizationService {
369
397
  };
370
398
  }
371
399
  /**
372
- * Map database member detailed to OrganizationMemberDetailed type
400
+ * Transform Supabase organization member detailed view record to domain model.
401
+ *
402
+ * This mapper handles records from the 'organization_members_detailed' view,
403
+ * which joins organization_members with users and organizations tables.
404
+ *
405
+ * Maps all OrganizationMember fields plus:
406
+ * - `email` (from users table)
407
+ * - `first_name` → `firstName` (from users table)
408
+ * - `last_name` → `lastName` (from users table)
409
+ * - `avatar_url` → `avatarUrl` (from users table)
410
+ * - `last_login_at` → `lastLoginAt` (from users table, nullable Date)
411
+ * - `organization_name` → `organizationName` (from organizations table)
412
+ * - `organization_slug` → `organizationSlug` (from organizations table)
413
+ *
414
+ * @param dbMember - Supabase record from 'organization_members_detailed' view
415
+ * @returns Domain OrganizationMemberDetailed object
416
+ * @see DbOrganizationMemberDetailedRecord in database-types.ts for input shape
417
+ * @see OrganizationMemberDetailed in saas-types.ts for output shape
373
418
  */
374
419
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row with joined user data
375
420
  mapDbMemberDetailedToMemberDetailed(dbMember) {
@@ -560,7 +605,19 @@ export class TeamService {
560
605
  return data || [];
561
606
  }
562
607
  /**
563
- * Map database team to Team type
608
+ * Transform Supabase team record to domain model.
609
+ *
610
+ * Maps database snake_case columns to TypeScript camelCase properties:
611
+ * - `organization_id` → `organizationId`
612
+ * - `encryption_key_id` → `encryptionKeyId` (FK to active team encryption key)
613
+ * - `created_at` (ISO string) → `createdAt` (Date)
614
+ * - `updated_at` (ISO string) → `updatedAt` (Date)
615
+ * - `deleted_at` (ISO string) → `deletedAt` (nullable Date, for soft delete)
616
+ *
617
+ * @param dbTeam - Supabase record from 'teams' table
618
+ * @returns Domain Team object
619
+ * @see DbTeamRecord in database-types.ts for input shape
620
+ * @see Team in saas-types.ts for output shape
564
621
  */
565
622
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
566
623
  mapDbTeamToTeam(dbTeam) {
@@ -577,7 +634,19 @@ export class TeamService {
577
634
  };
578
635
  }
579
636
  /**
580
- * Map database team member to TeamMember type
637
+ * Transform Supabase team member record to domain model.
638
+ *
639
+ * Maps database snake_case columns to TypeScript camelCase properties:
640
+ * - `team_id` → `teamId`
641
+ * - `user_id` → `userId`
642
+ * - `role` (string) → `role` (TeamRole type: 'admin' | 'member' | 'viewer')
643
+ * - `created_at` (ISO string) → `createdAt` (Date)
644
+ * - `updated_at` (ISO string) → `updatedAt` (Date)
645
+ *
646
+ * @param dbMember - Supabase record from 'team_members' table
647
+ * @returns Domain TeamMember object
648
+ * @see DbTeamMemberRecord in database-types.ts for input shape
649
+ * @see TeamMember in saas-types.ts for output shape
581
650
  */
582
651
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
583
652
  mapDbTeamMemberToTeamMember(dbMember) {
@@ -341,7 +341,14 @@ export class SecretsService {
341
341
  }
342
342
  }
343
343
  /**
344
- * Helper to get team
344
+ * Helper to get team record from database.
345
+ *
346
+ * Fetches raw team record from 'teams' table. Used internally to get
347
+ * organization_id for audit logging and tier limit checks.
348
+ *
349
+ * @param teamId - UUID of team to fetch
350
+ * @returns Raw Supabase team record or null if not found
351
+ * @see DbTeamRecord in database-types.ts for return shape
345
352
  */
346
353
  async getTeamById(teamId) {
347
354
  const { data } = await this.supabase
@@ -352,7 +359,28 @@ export class SecretsService {
352
359
  return data;
353
360
  }
354
361
  /**
355
- * Map database secret to Secret type
362
+ * Transform Supabase secret record to domain model.
363
+ *
364
+ * Maps database snake_case columns to TypeScript camelCase properties:
365
+ * - `team_id` → `teamId`
366
+ * - `encrypted_value` → `encryptedValue` (AES-256 encrypted)
367
+ * - `encryption_key_id` → `encryptionKeyId` (FK to team's encryption key)
368
+ * - `last_rotated_at` → `lastRotatedAt` (nullable Date)
369
+ * - `rotation_interval_days` → `rotationIntervalDays` (nullable number)
370
+ * - `created_by` → `createdBy` (FK to users.id)
371
+ * - `updated_by` → `updatedBy` (FK to users.id)
372
+ * - `deleted_by` → `deletedBy` (FK to users.id, for soft delete audit)
373
+ *
374
+ * Special handling:
375
+ * - `tags`: Parses JSON string to string[] if stored as string, passes through if already array
376
+ *
377
+ * Note: The `encryptedValue` field contains the encrypted secret. Use
378
+ * `encryptionService.decryptForTeam()` to decrypt it when needed.
379
+ *
380
+ * @param dbSecret - Supabase record from 'secrets' table
381
+ * @returns Domain Secret object with parsed tags
382
+ * @see DbSecretRecord in database-types.ts for input shape
383
+ * @see Secret in saas-types.ts for output shape
356
384
  */
357
385
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- DB row type varies by schema
358
386
  mapDbSecretToSecret(dbSecret) {
@@ -458,6 +458,7 @@ API_KEY=
458
458
  .option('--export', 'Output in export format for shell evaluation (alias for --format export)')
459
459
  .option('--format <type>', 'Output format: env, json, yaml, toml, export', 'env')
460
460
  .option('--exact', 'Require exact key match (disable fuzzy matching)')
461
+ .option('--no-mask', 'Show full values in fuzzy match results')
461
462
  .action(async (key, options) => {
462
463
  try {
463
464
  const manager = new SecretsManager({ globalMode: options.global });
@@ -546,11 +547,13 @@ API_KEY=
546
547
  // Multiple matches - show all matches for user to choose
547
548
  console.error(`🔍 Found ${matches.length} matches for '${key}':\n`);
548
549
  for (const match of matches) {
549
- // Mask value for display
550
- const maskedValue = match.value.length > 4
551
- ? match.value.substring(0, 4) + '*'.repeat(Math.min(match.value.length - 4, 10))
552
- : '****';
553
- console.error(` ${match.key}=${maskedValue}`);
550
+ // Mask value for display unless --no-mask is set
551
+ const displayValue = options.mask === false
552
+ ? match.value
553
+ : (match.value.length > 4
554
+ ? match.value.substring(0, 4) + '*'.repeat(Math.min(match.value.length - 4, 10))
555
+ : '****');
556
+ console.error(` ${match.key}=${displayValue}`);
554
557
  }
555
558
  console.error('');
556
559
  console.error('💡 Please specify the exact key name or use one of:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "3.1.6",
3
+ "version": "3.1.8",
4
4
  "description": "Simple, cross-platform encrypted secrets manager with automatic sync, IPFS audit logs, and multi-environment support. Just run lsh sync and start managing your secrets.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {
@@ -65,13 +65,14 @@
65
65
  "dependencies": {
66
66
  "@storacha/client": "^1.8.18",
67
67
  "@supabase/supabase-js": "^2.57.4",
68
- "bcrypt": "^5.1.1",
68
+ "@types/proper-lockfile": "^4.1.4",
69
+ "bcrypt": "^6.0.0",
69
70
  "chalk": "^5.3.0",
70
71
  "chokidar": "^5.0.0",
71
72
  "commander": "^14.0.2",
72
73
  "cors": "^2.8.5",
73
74
  "dotenv": "^17.2.3",
74
- "express": "^4.18.2",
75
+ "express": "^4.22.1",
75
76
  "express-rate-limit": "^8.2.1",
76
77
  "glob": "^13.0.0",
77
78
  "inquirer": "^9.2.12",
@@ -80,6 +81,7 @@
80
81
  "node-cron": "^4.2.1",
81
82
  "ora": "^9.0.0",
82
83
  "pg": "^8.16.3",
84
+ "proper-lockfile": "^4.1.2",
83
85
  "smol-toml": "^1.3.1",
84
86
  "uuid": "^13.0.0"
85
87
  },
@@ -90,7 +92,7 @@
90
92
  "@types/jest": "^30.0.0",
91
93
  "@types/js-yaml": "^4.0.9",
92
94
  "@types/jsonwebtoken": "^9.0.5",
93
- "@types/node": "^20.12.7",
95
+ "@types/node": "^20.19.27",
94
96
  "@typescript-eslint/eslint-plugin": "^8.44.1",
95
97
  "@typescript-eslint/parser": "^8.44.1",
96
98
  "eslint": "^9.36.0",