@stackbe/sdk 0.1.0 → 0.2.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.
package/dist/index.mjs CHANGED
@@ -13,6 +13,9 @@ var HttpClient = class {
13
13
  constructor(config) {
14
14
  this.config = config;
15
15
  }
16
+ get baseUrl() {
17
+ return this.config.baseUrl;
18
+ }
16
19
  async request(method, path, options = {}) {
17
20
  const url = new URL(path, this.config.baseUrl);
18
21
  if (options.params) {
@@ -379,6 +382,379 @@ var CustomersClient = class {
379
382
  }
380
383
  };
381
384
 
385
+ // src/checkout.ts
386
+ var CheckoutClient = class {
387
+ constructor(http, appId) {
388
+ this.http = http;
389
+ this.appId = appId;
390
+ }
391
+ /**
392
+ * Create a checkout session for a customer to subscribe to a plan.
393
+ * Returns a URL to redirect the customer to Stripe checkout.
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * // With existing customer ID
398
+ * const { url } = await stackbe.checkout.createSession({
399
+ * customer: 'cust_123',
400
+ * planId: 'plan_pro_monthly',
401
+ * successUrl: 'https://myapp.com/success',
402
+ * cancelUrl: 'https://myapp.com/pricing',
403
+ * });
404
+ *
405
+ * // Redirect to checkout
406
+ * res.redirect(url);
407
+ * ```
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * // With new customer (will be created)
412
+ * const { url } = await stackbe.checkout.createSession({
413
+ * customer: { email: 'user@example.com', name: 'John' },
414
+ * planId: 'plan_pro_monthly',
415
+ * successUrl: 'https://myapp.com/success',
416
+ * trialDays: 14,
417
+ * });
418
+ * ```
419
+ */
420
+ async createSession(options) {
421
+ const body = {
422
+ planId: options.planId,
423
+ successUrl: options.successUrl,
424
+ cancelUrl: options.cancelUrl,
425
+ allowPromotionCodes: options.allowPromotionCodes,
426
+ trialDays: options.trialDays,
427
+ metadata: options.metadata
428
+ };
429
+ if (typeof options.customer === "string") {
430
+ body.customerId = options.customer;
431
+ } else {
432
+ body.customerEmail = options.customer.email;
433
+ body.customerName = options.customer.name;
434
+ }
435
+ return this.http.post("/v1/checkout/session", body);
436
+ }
437
+ /**
438
+ * Get an existing checkout session by ID.
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * const session = await stackbe.checkout.getSession('cs_123');
443
+ * if (session.status === 'complete') {
444
+ * // Payment successful
445
+ * }
446
+ * ```
447
+ */
448
+ async getSession(sessionId) {
449
+ return this.http.get(`/v1/checkout/session/${sessionId}`);
450
+ }
451
+ /**
452
+ * Generate a checkout URL for a plan.
453
+ * Convenience method that creates a session and returns just the URL.
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * const checkoutUrl = await stackbe.checkout.getCheckoutUrl({
458
+ * customer: 'cust_123',
459
+ * planId: 'plan_pro_monthly',
460
+ * successUrl: 'https://myapp.com/success',
461
+ * });
462
+ *
463
+ * // Send to frontend
464
+ * res.json({ checkoutUrl });
465
+ * ```
466
+ */
467
+ async getCheckoutUrl(options) {
468
+ const session = await this.createSession(options);
469
+ return session.url;
470
+ }
471
+ };
472
+
473
+ // src/subscriptions.ts
474
+ var SubscriptionsClient = class {
475
+ constructor(http) {
476
+ this.http = http;
477
+ }
478
+ /**
479
+ * Get a customer's current active subscription.
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * const subscription = await stackbe.subscriptions.get('cust_123');
484
+ *
485
+ * if (subscription) {
486
+ * console.log(`Plan: ${subscription.plan.name}`);
487
+ * console.log(`Status: ${subscription.status}`);
488
+ * console.log(`Renews: ${subscription.currentPeriodEnd}`);
489
+ * } else {
490
+ * console.log('No active subscription');
491
+ * }
492
+ * ```
493
+ */
494
+ async get(customerId) {
495
+ try {
496
+ return await this.http.get(
497
+ `/v1/subscriptions/current`,
498
+ { customerId }
499
+ );
500
+ } catch (error) {
501
+ if (error instanceof Error && "statusCode" in error && error.statusCode === 404) {
502
+ return null;
503
+ }
504
+ throw error;
505
+ }
506
+ }
507
+ /**
508
+ * Get a subscription by ID.
509
+ *
510
+ * @example
511
+ * ```typescript
512
+ * const subscription = await stackbe.subscriptions.getById('sub_123');
513
+ * ```
514
+ */
515
+ async getById(subscriptionId) {
516
+ return this.http.get(`/v1/subscriptions/${subscriptionId}`);
517
+ }
518
+ /**
519
+ * List all subscriptions for a customer.
520
+ *
521
+ * @example
522
+ * ```typescript
523
+ * const subscriptions = await stackbe.subscriptions.list('cust_123');
524
+ * ```
525
+ */
526
+ async list(customerId) {
527
+ return this.http.get("/v1/subscriptions", { customerId });
528
+ }
529
+ /**
530
+ * Cancel a subscription.
531
+ *
532
+ * @example
533
+ * ```typescript
534
+ * // Cancel at end of billing period (default)
535
+ * await stackbe.subscriptions.cancel('sub_123');
536
+ *
537
+ * // Cancel immediately
538
+ * await stackbe.subscriptions.cancel('sub_123', { immediate: true });
539
+ *
540
+ * // Cancel with reason
541
+ * await stackbe.subscriptions.cancel('sub_123', {
542
+ * reason: 'Too expensive'
543
+ * });
544
+ * ```
545
+ */
546
+ async cancel(subscriptionId, options = {}) {
547
+ return this.http.post(
548
+ `/v1/subscriptions/${subscriptionId}/cancel`,
549
+ {
550
+ immediate: options.immediate ?? false,
551
+ reason: options.reason
552
+ }
553
+ );
554
+ }
555
+ /**
556
+ * Update a subscription (change plan).
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * // Upgrade/downgrade to a different plan
561
+ * await stackbe.subscriptions.update('sub_123', {
562
+ * planId: 'plan_enterprise_monthly',
563
+ * prorate: true,
564
+ * });
565
+ * ```
566
+ */
567
+ async update(subscriptionId, options) {
568
+ return this.http.patch(
569
+ `/v1/subscriptions/${subscriptionId}`,
570
+ options
571
+ );
572
+ }
573
+ /**
574
+ * Reactivate a canceled subscription (before it ends).
575
+ *
576
+ * @example
577
+ * ```typescript
578
+ * // Customer changed their mind
579
+ * await stackbe.subscriptions.reactivate('sub_123');
580
+ * ```
581
+ */
582
+ async reactivate(subscriptionId) {
583
+ return this.http.post(
584
+ `/v1/subscriptions/${subscriptionId}/reactivate`
585
+ );
586
+ }
587
+ /**
588
+ * Check if a customer has an active subscription.
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * const isActive = await stackbe.subscriptions.isActive('cust_123');
593
+ * if (!isActive) {
594
+ * return res.redirect('/pricing');
595
+ * }
596
+ * ```
597
+ */
598
+ async isActive(customerId) {
599
+ const subscription = await this.get(customerId);
600
+ return subscription !== null && (subscription.status === "active" || subscription.status === "trialing");
601
+ }
602
+ };
603
+
604
+ // src/auth.ts
605
+ var AuthClient = class {
606
+ constructor(http, appId) {
607
+ this.http = http;
608
+ this.appId = appId;
609
+ }
610
+ /**
611
+ * Send a magic link email to a customer for passwordless authentication.
612
+ *
613
+ * @example
614
+ * ```typescript
615
+ * // Send magic link
616
+ * await stackbe.auth.sendMagicLink('user@example.com');
617
+ *
618
+ * // With redirect URL
619
+ * await stackbe.auth.sendMagicLink('user@example.com', {
620
+ * redirectUrl: 'https://myapp.com/dashboard',
621
+ * });
622
+ *
623
+ * // For localhost development
624
+ * await stackbe.auth.sendMagicLink('user@example.com', {
625
+ * useDev: true,
626
+ * });
627
+ * ```
628
+ */
629
+ async sendMagicLink(email, options = {}) {
630
+ return this.http.post(
631
+ `/v1/apps/${this.appId}/auth/magic-link`,
632
+ {
633
+ email,
634
+ redirectUrl: options.redirectUrl,
635
+ useDev: options.useDev
636
+ }
637
+ );
638
+ }
639
+ /**
640
+ * Verify a magic link token and get a session token.
641
+ * Call this when the user clicks the magic link.
642
+ *
643
+ * @example
644
+ * ```typescript
645
+ * // In your /verify route handler
646
+ * const { token } = req.query;
647
+ *
648
+ * const result = await stackbe.auth.verifyToken(token);
649
+ *
650
+ * if (result.valid) {
651
+ * // Set session cookie
652
+ * res.cookie('session', result.token, { httpOnly: true });
653
+ * res.redirect('/dashboard');
654
+ * } else {
655
+ * res.redirect('/login?error=invalid_token');
656
+ * }
657
+ * ```
658
+ */
659
+ async verifyToken(token) {
660
+ return this.http.post(
661
+ `/v1/apps/${this.appId}/auth/verify`,
662
+ { token }
663
+ );
664
+ }
665
+ /**
666
+ * Get the current session for an authenticated customer.
667
+ * Use this to validate session tokens and get customer data.
668
+ *
669
+ * @example
670
+ * ```typescript
671
+ * // Validate session on each request
672
+ * const sessionToken = req.cookies.session;
673
+ *
674
+ * const session = await stackbe.auth.getSession(sessionToken);
675
+ *
676
+ * if (session) {
677
+ * req.customer = session.customer;
678
+ * req.subscription = session.subscription;
679
+ * req.entitlements = session.entitlements;
680
+ * }
681
+ * ```
682
+ */
683
+ async getSession(sessionToken) {
684
+ try {
685
+ const response = await fetch(
686
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session?appId=${this.appId}`,
687
+ {
688
+ headers: {
689
+ "Authorization": `Bearer ${sessionToken}`,
690
+ "Content-Type": "application/json"
691
+ }
692
+ }
693
+ );
694
+ if (!response.ok) {
695
+ if (response.status === 401) {
696
+ return null;
697
+ }
698
+ throw new Error("Failed to get session");
699
+ }
700
+ return await response.json();
701
+ } catch {
702
+ return null;
703
+ }
704
+ }
705
+ /**
706
+ * Create Express middleware that validates session tokens.
707
+ *
708
+ * @example
709
+ * ```typescript
710
+ * // Protect routes with authentication
711
+ * app.use('/dashboard', stackbe.auth.middleware({
712
+ * getToken: (req) => req.cookies.session,
713
+ * onUnauthenticated: (req, res) => res.redirect('/login'),
714
+ * }));
715
+ *
716
+ * app.get('/dashboard', (req, res) => {
717
+ * // req.customer is available
718
+ * res.json({ email: req.customer.email });
719
+ * });
720
+ * ```
721
+ */
722
+ middleware(options) {
723
+ return async (req, res, next) => {
724
+ const token = options.getToken(req);
725
+ if (!token) {
726
+ if (options.onUnauthenticated) {
727
+ return options.onUnauthenticated(req, res);
728
+ }
729
+ return res.status(401).json({ error: "Not authenticated" });
730
+ }
731
+ const session = await this.getSession(token);
732
+ if (!session) {
733
+ if (options.onUnauthenticated) {
734
+ return options.onUnauthenticated(req, res);
735
+ }
736
+ return res.status(401).json({ error: "Invalid session" });
737
+ }
738
+ req.customer = session.customer;
739
+ req.subscription = session.subscription;
740
+ req.entitlements = session.entitlements;
741
+ next();
742
+ };
743
+ }
744
+ /**
745
+ * Check if a session token is valid.
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * const isValid = await stackbe.auth.isAuthenticated(sessionToken);
750
+ * ```
751
+ */
752
+ async isAuthenticated(sessionToken) {
753
+ const session = await this.getSession(sessionToken);
754
+ return session !== null;
755
+ }
756
+ };
757
+
382
758
  // src/client.ts
383
759
  var DEFAULT_BASE_URL = "https://api.stackbe.io";
384
760
  var DEFAULT_TIMEOUT = 3e4;
@@ -401,8 +777,18 @@ var StackBE = class {
401
777
  * // Check entitlements
402
778
  * const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium');
403
779
  *
404
- * // Get customer
405
- * const customer = await stackbe.customers.get('customer_123');
780
+ * // Create checkout session
781
+ * const { url } = await stackbe.checkout.createSession({
782
+ * customer: 'cust_123',
783
+ * planId: 'plan_pro',
784
+ * successUrl: 'https://myapp.com/success',
785
+ * });
786
+ *
787
+ * // Get subscription
788
+ * const subscription = await stackbe.subscriptions.get('cust_123');
789
+ *
790
+ * // Send magic link
791
+ * await stackbe.auth.sendMagicLink('user@example.com');
406
792
  * ```
407
793
  */
408
794
  constructor(config) {
@@ -412,6 +798,7 @@ var StackBE = class {
412
798
  if (!config.appId) {
413
799
  throw new StackBEError("appId is required", 400, "INVALID_CONFIG");
414
800
  }
801
+ this.appId = config.appId;
415
802
  this.http = new HttpClient({
416
803
  baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
417
804
  apiKey: config.apiKey,
@@ -421,6 +808,9 @@ var StackBE = class {
421
808
  this.usage = new UsageClient(this.http);
422
809
  this.entitlements = new EntitlementsClient(this.http);
423
810
  this.customers = new CustomersClient(this.http);
811
+ this.checkout = new CheckoutClient(this.http, config.appId);
812
+ this.subscriptions = new SubscriptionsClient(this.http);
813
+ this.auth = new AuthClient(this.http, config.appId);
424
814
  }
425
815
  /**
426
816
  * Create a middleware for Express that tracks usage automatically.
@@ -553,9 +943,12 @@ var StackBE = class {
553
943
  }
554
944
  };
555
945
  export {
946
+ AuthClient,
947
+ CheckoutClient,
556
948
  CustomersClient,
557
949
  EntitlementsClient,
558
950
  StackBE,
559
951
  StackBEError,
952
+ SubscriptionsClient,
560
953
  UsageClient
561
954
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbe/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Official JavaScript/TypeScript SDK for StackBE - the billing backend for your side project",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",