@stackbe/sdk 0.1.0 → 0.3.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/README.md +263 -77
- package/dist/index.d.mts +479 -10
- package/dist/index.d.ts +479 -10
- package/dist/index.js +538 -7
- package/dist/index.mjs +535 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,10 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AuthClient: () => AuthClient,
|
|
24
|
+
CheckoutClient: () => CheckoutClient,
|
|
23
25
|
CustomersClient: () => CustomersClient,
|
|
24
26
|
EntitlementsClient: () => EntitlementsClient,
|
|
25
27
|
StackBE: () => StackBE,
|
|
26
28
|
StackBEError: () => StackBEError,
|
|
29
|
+
SubscriptionsClient: () => SubscriptionsClient,
|
|
27
30
|
UsageClient: () => UsageClient
|
|
28
31
|
});
|
|
29
32
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -36,13 +39,69 @@ var StackBEError = class extends Error {
|
|
|
36
39
|
this.statusCode = statusCode;
|
|
37
40
|
this.code = code;
|
|
38
41
|
}
|
|
42
|
+
/** Check if this is a specific error type */
|
|
43
|
+
is(code) {
|
|
44
|
+
return this.code === code;
|
|
45
|
+
}
|
|
46
|
+
/** Check if this is an auth-related error */
|
|
47
|
+
isAuthError() {
|
|
48
|
+
return [
|
|
49
|
+
"TOKEN_EXPIRED",
|
|
50
|
+
"TOKEN_ALREADY_USED",
|
|
51
|
+
"TOKEN_INVALID",
|
|
52
|
+
"SESSION_EXPIRED",
|
|
53
|
+
"SESSION_INVALID",
|
|
54
|
+
"UNAUTHORIZED"
|
|
55
|
+
].includes(this.code);
|
|
56
|
+
}
|
|
57
|
+
/** Check if this is a "not found" error */
|
|
58
|
+
isNotFoundError() {
|
|
59
|
+
return [
|
|
60
|
+
"NOT_FOUND",
|
|
61
|
+
"CUSTOMER_NOT_FOUND",
|
|
62
|
+
"SUBSCRIPTION_NOT_FOUND",
|
|
63
|
+
"PLAN_NOT_FOUND",
|
|
64
|
+
"APP_NOT_FOUND"
|
|
65
|
+
].includes(this.code);
|
|
66
|
+
}
|
|
39
67
|
};
|
|
40
68
|
|
|
41
69
|
// src/http.ts
|
|
70
|
+
function mapErrorCode(status, message, errorType) {
|
|
71
|
+
if (errorType && errorType !== "Error") {
|
|
72
|
+
const codeFromError = errorType.toUpperCase().replace(/\s+/g, "_");
|
|
73
|
+
if (codeFromError.includes("NOT_FOUND")) return "NOT_FOUND";
|
|
74
|
+
if (codeFromError.includes("UNAUTHORIZED")) return "UNAUTHORIZED";
|
|
75
|
+
}
|
|
76
|
+
const lowerMessage = message.toLowerCase();
|
|
77
|
+
if (lowerMessage.includes("token") && lowerMessage.includes("expired")) return "TOKEN_EXPIRED";
|
|
78
|
+
if (lowerMessage.includes("already been used")) return "TOKEN_ALREADY_USED";
|
|
79
|
+
if (lowerMessage.includes("invalid") && lowerMessage.includes("token")) return "TOKEN_INVALID";
|
|
80
|
+
if (lowerMessage.includes("session") && lowerMessage.includes("expired")) return "SESSION_EXPIRED";
|
|
81
|
+
if (lowerMessage.includes("session") && lowerMessage.includes("invalid")) return "SESSION_INVALID";
|
|
82
|
+
if (lowerMessage.includes("customer") && lowerMessage.includes("not found")) return "CUSTOMER_NOT_FOUND";
|
|
83
|
+
if (lowerMessage.includes("subscription") && lowerMessage.includes("not found")) return "SUBSCRIPTION_NOT_FOUND";
|
|
84
|
+
if (lowerMessage.includes("plan") && lowerMessage.includes("not found")) return "PLAN_NOT_FOUND";
|
|
85
|
+
if (lowerMessage.includes("app") && lowerMessage.includes("not found")) return "APP_NOT_FOUND";
|
|
86
|
+
if (lowerMessage.includes("not found")) return "NOT_FOUND";
|
|
87
|
+
if (lowerMessage.includes("limit") && lowerMessage.includes("exceeded")) return "USAGE_LIMIT_EXCEEDED";
|
|
88
|
+
if (lowerMessage.includes("metric") && lowerMessage.includes("not found")) return "METRIC_NOT_FOUND";
|
|
89
|
+
if (lowerMessage.includes("feature") && lowerMessage.includes("not available")) return "FEATURE_NOT_AVAILABLE";
|
|
90
|
+
if (lowerMessage.includes("no active subscription")) return "NO_ACTIVE_SUBSCRIPTION";
|
|
91
|
+
if (lowerMessage.includes("required")) return "MISSING_REQUIRED_FIELD";
|
|
92
|
+
if (lowerMessage.includes("invalid") && lowerMessage.includes("email")) return "INVALID_EMAIL";
|
|
93
|
+
if (status === 400) return "VALIDATION_ERROR";
|
|
94
|
+
if (status === 401) return "UNAUTHORIZED";
|
|
95
|
+
if (status === 404) return "NOT_FOUND";
|
|
96
|
+
return "UNKNOWN_ERROR";
|
|
97
|
+
}
|
|
42
98
|
var HttpClient = class {
|
|
43
99
|
constructor(config) {
|
|
44
100
|
this.config = config;
|
|
45
101
|
}
|
|
102
|
+
get baseUrl() {
|
|
103
|
+
return this.config.baseUrl;
|
|
104
|
+
}
|
|
46
105
|
async request(method, path, options = {}) {
|
|
47
106
|
const url = new URL(path, this.config.baseUrl);
|
|
48
107
|
if (options.params) {
|
|
@@ -73,11 +132,9 @@ var HttpClient = class {
|
|
|
73
132
|
const data = await response.json();
|
|
74
133
|
if (!response.ok) {
|
|
75
134
|
const errorData = data;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
errorData.error || "UNKNOWN_ERROR"
|
|
80
|
-
);
|
|
135
|
+
const message = errorData.message || "Unknown error";
|
|
136
|
+
const code = errorData.code || mapErrorCode(response.status, message, errorData.error || "");
|
|
137
|
+
throw new StackBEError(message, errorData.statusCode || response.status, code);
|
|
81
138
|
}
|
|
82
139
|
return data;
|
|
83
140
|
} catch (error) {
|
|
@@ -409,6 +466,463 @@ var CustomersClient = class {
|
|
|
409
466
|
}
|
|
410
467
|
};
|
|
411
468
|
|
|
469
|
+
// src/checkout.ts
|
|
470
|
+
var CheckoutClient = class {
|
|
471
|
+
constructor(http, appId) {
|
|
472
|
+
this.http = http;
|
|
473
|
+
this.appId = appId;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Create a checkout session for a customer to subscribe to a plan.
|
|
477
|
+
* Returns a URL to redirect the customer to Stripe checkout.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```typescript
|
|
481
|
+
* // With existing customer ID
|
|
482
|
+
* const { url } = await stackbe.checkout.createSession({
|
|
483
|
+
* customer: 'cust_123',
|
|
484
|
+
* planId: 'plan_pro_monthly',
|
|
485
|
+
* successUrl: 'https://myapp.com/success',
|
|
486
|
+
* cancelUrl: 'https://myapp.com/pricing',
|
|
487
|
+
* });
|
|
488
|
+
*
|
|
489
|
+
* // Redirect to checkout
|
|
490
|
+
* res.redirect(url);
|
|
491
|
+
* ```
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```typescript
|
|
495
|
+
* // With new customer (will be created)
|
|
496
|
+
* const { url } = await stackbe.checkout.createSession({
|
|
497
|
+
* customer: { email: 'user@example.com', name: 'John' },
|
|
498
|
+
* planId: 'plan_pro_monthly',
|
|
499
|
+
* successUrl: 'https://myapp.com/success',
|
|
500
|
+
* trialDays: 14,
|
|
501
|
+
* });
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
async createSession(options) {
|
|
505
|
+
const body = {
|
|
506
|
+
planId: options.planId,
|
|
507
|
+
successUrl: options.successUrl,
|
|
508
|
+
cancelUrl: options.cancelUrl,
|
|
509
|
+
allowPromotionCodes: options.allowPromotionCodes,
|
|
510
|
+
trialDays: options.trialDays,
|
|
511
|
+
metadata: options.metadata
|
|
512
|
+
};
|
|
513
|
+
if (typeof options.customer === "string") {
|
|
514
|
+
body.customerId = options.customer;
|
|
515
|
+
} else {
|
|
516
|
+
body.customerEmail = options.customer.email;
|
|
517
|
+
body.customerName = options.customer.name;
|
|
518
|
+
}
|
|
519
|
+
return this.http.post("/v1/checkout/session", body);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get an existing checkout session by ID.
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```typescript
|
|
526
|
+
* const session = await stackbe.checkout.getSession('cs_123');
|
|
527
|
+
* if (session.status === 'complete') {
|
|
528
|
+
* // Payment successful
|
|
529
|
+
* }
|
|
530
|
+
* ```
|
|
531
|
+
*/
|
|
532
|
+
async getSession(sessionId) {
|
|
533
|
+
return this.http.get(`/v1/checkout/session/${sessionId}`);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Generate a checkout URL for a plan.
|
|
537
|
+
* Convenience method that creates a session and returns just the URL.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* const checkoutUrl = await stackbe.checkout.getCheckoutUrl({
|
|
542
|
+
* customer: 'cust_123',
|
|
543
|
+
* planId: 'plan_pro_monthly',
|
|
544
|
+
* successUrl: 'https://myapp.com/success',
|
|
545
|
+
* });
|
|
546
|
+
*
|
|
547
|
+
* // Send to frontend
|
|
548
|
+
* res.json({ checkoutUrl });
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
async getCheckoutUrl(options) {
|
|
552
|
+
const session = await this.createSession(options);
|
|
553
|
+
return session.url;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// src/subscriptions.ts
|
|
558
|
+
var SubscriptionsClient = class {
|
|
559
|
+
constructor(http) {
|
|
560
|
+
this.http = http;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Get a customer's current active subscription.
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```typescript
|
|
567
|
+
* const subscription = await stackbe.subscriptions.get('cust_123');
|
|
568
|
+
*
|
|
569
|
+
* if (subscription) {
|
|
570
|
+
* console.log(`Plan: ${subscription.plan.name}`);
|
|
571
|
+
* console.log(`Status: ${subscription.status}`);
|
|
572
|
+
* console.log(`Renews: ${subscription.currentPeriodEnd}`);
|
|
573
|
+
* } else {
|
|
574
|
+
* console.log('No active subscription');
|
|
575
|
+
* }
|
|
576
|
+
* ```
|
|
577
|
+
*/
|
|
578
|
+
async get(customerId) {
|
|
579
|
+
try {
|
|
580
|
+
return await this.http.get(
|
|
581
|
+
`/v1/subscriptions/current`,
|
|
582
|
+
{ customerId }
|
|
583
|
+
);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (error instanceof Error && "statusCode" in error && error.statusCode === 404) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Get a subscription by ID.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```typescript
|
|
596
|
+
* const subscription = await stackbe.subscriptions.getById('sub_123');
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
async getById(subscriptionId) {
|
|
600
|
+
return this.http.get(`/v1/subscriptions/${subscriptionId}`);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* List all subscriptions for a customer.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```typescript
|
|
607
|
+
* const subscriptions = await stackbe.subscriptions.list('cust_123');
|
|
608
|
+
* ```
|
|
609
|
+
*/
|
|
610
|
+
async list(customerId) {
|
|
611
|
+
return this.http.get("/v1/subscriptions", { customerId });
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Cancel a subscription.
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* ```typescript
|
|
618
|
+
* // Cancel at end of billing period (default)
|
|
619
|
+
* await stackbe.subscriptions.cancel('sub_123');
|
|
620
|
+
*
|
|
621
|
+
* // Cancel immediately
|
|
622
|
+
* await stackbe.subscriptions.cancel('sub_123', { immediate: true });
|
|
623
|
+
*
|
|
624
|
+
* // Cancel with reason
|
|
625
|
+
* await stackbe.subscriptions.cancel('sub_123', {
|
|
626
|
+
* reason: 'Too expensive'
|
|
627
|
+
* });
|
|
628
|
+
* ```
|
|
629
|
+
*/
|
|
630
|
+
async cancel(subscriptionId, options = {}) {
|
|
631
|
+
return this.http.post(
|
|
632
|
+
`/v1/subscriptions/${subscriptionId}/cancel`,
|
|
633
|
+
{
|
|
634
|
+
immediate: options.immediate ?? false,
|
|
635
|
+
reason: options.reason
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Update a subscription (change plan).
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ```typescript
|
|
644
|
+
* // Upgrade/downgrade to a different plan
|
|
645
|
+
* await stackbe.subscriptions.update('sub_123', {
|
|
646
|
+
* planId: 'plan_enterprise_monthly',
|
|
647
|
+
* prorate: true,
|
|
648
|
+
* });
|
|
649
|
+
* ```
|
|
650
|
+
*/
|
|
651
|
+
async update(subscriptionId, options) {
|
|
652
|
+
return this.http.patch(
|
|
653
|
+
`/v1/subscriptions/${subscriptionId}`,
|
|
654
|
+
options
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Reactivate a canceled subscription (before it ends).
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* // Customer changed their mind
|
|
663
|
+
* await stackbe.subscriptions.reactivate('sub_123');
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
async reactivate(subscriptionId) {
|
|
667
|
+
return this.http.post(
|
|
668
|
+
`/v1/subscriptions/${subscriptionId}/reactivate`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Check if a customer has an active subscription.
|
|
673
|
+
*
|
|
674
|
+
* @example
|
|
675
|
+
* ```typescript
|
|
676
|
+
* const isActive = await stackbe.subscriptions.isActive('cust_123');
|
|
677
|
+
* if (!isActive) {
|
|
678
|
+
* return res.redirect('/pricing');
|
|
679
|
+
* }
|
|
680
|
+
* ```
|
|
681
|
+
*/
|
|
682
|
+
async isActive(customerId) {
|
|
683
|
+
const subscription = await this.get(customerId);
|
|
684
|
+
return subscription !== null && (subscription.status === "active" || subscription.status === "trialing");
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/auth.ts
|
|
689
|
+
function decodeJwt(token) {
|
|
690
|
+
try {
|
|
691
|
+
const parts = token.split(".");
|
|
692
|
+
if (parts.length !== 3) return null;
|
|
693
|
+
const payload = Buffer.from(parts[1], "base64").toString("utf-8");
|
|
694
|
+
return JSON.parse(payload);
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
var AuthClient = class {
|
|
700
|
+
constructor(http, appId) {
|
|
701
|
+
this.http = http;
|
|
702
|
+
this.appId = appId;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Send a magic link email to a customer for passwordless authentication.
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* ```typescript
|
|
709
|
+
* // Send magic link
|
|
710
|
+
* await stackbe.auth.sendMagicLink('user@example.com');
|
|
711
|
+
*
|
|
712
|
+
* // With redirect URL
|
|
713
|
+
* await stackbe.auth.sendMagicLink('user@example.com', {
|
|
714
|
+
* redirectUrl: 'https://myapp.com/dashboard',
|
|
715
|
+
* });
|
|
716
|
+
*
|
|
717
|
+
* // For localhost development
|
|
718
|
+
* await stackbe.auth.sendMagicLink('user@example.com', {
|
|
719
|
+
* useDev: true,
|
|
720
|
+
* });
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
async sendMagicLink(email, options = {}) {
|
|
724
|
+
return this.http.post(
|
|
725
|
+
`/v1/apps/${this.appId}/auth/magic-link`,
|
|
726
|
+
{
|
|
727
|
+
email,
|
|
728
|
+
redirectUrl: options.redirectUrl,
|
|
729
|
+
useDev: options.useDev
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Verify a magic link token and get a session token.
|
|
735
|
+
* Call this when the user clicks the magic link.
|
|
736
|
+
*
|
|
737
|
+
* Returns the session token along with tenant and organization context.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```typescript
|
|
741
|
+
* // In your /verify route handler
|
|
742
|
+
* const { token } = req.query;
|
|
743
|
+
*
|
|
744
|
+
* try {
|
|
745
|
+
* const result = await stackbe.auth.verifyToken(token);
|
|
746
|
+
*
|
|
747
|
+
* // result includes: customerId, email, sessionToken, tenantId, organizationId
|
|
748
|
+
* res.cookie('session', result.sessionToken, { httpOnly: true });
|
|
749
|
+
* res.redirect('/dashboard');
|
|
750
|
+
* } catch (error) {
|
|
751
|
+
* if (error instanceof StackBEError) {
|
|
752
|
+
* if (error.code === 'TOKEN_EXPIRED') {
|
|
753
|
+
* res.redirect('/login?error=expired');
|
|
754
|
+
* } else if (error.code === 'TOKEN_ALREADY_USED') {
|
|
755
|
+
* res.redirect('/login?error=used');
|
|
756
|
+
* }
|
|
757
|
+
* }
|
|
758
|
+
* }
|
|
759
|
+
* ```
|
|
760
|
+
*/
|
|
761
|
+
async verifyToken(token) {
|
|
762
|
+
const response = await fetch(
|
|
763
|
+
`${this.http.baseUrl}/v1/apps/${this.appId}/auth/verify?token=${encodeURIComponent(token)}`,
|
|
764
|
+
{
|
|
765
|
+
headers: {
|
|
766
|
+
"Content-Type": "application/json"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
if (!response.ok) {
|
|
771
|
+
const errorData = await response.json().catch(() => ({}));
|
|
772
|
+
const message = errorData.message || "Token verification failed";
|
|
773
|
+
let code = "TOKEN_INVALID";
|
|
774
|
+
if (message.includes("expired")) {
|
|
775
|
+
code = "TOKEN_EXPIRED";
|
|
776
|
+
} else if (message.includes("already been used")) {
|
|
777
|
+
code = "TOKEN_ALREADY_USED";
|
|
778
|
+
}
|
|
779
|
+
throw new StackBEError(message, response.status, code);
|
|
780
|
+
}
|
|
781
|
+
const data = await response.json();
|
|
782
|
+
let tenantId;
|
|
783
|
+
let organizationId;
|
|
784
|
+
let orgRole;
|
|
785
|
+
if (data.sessionToken) {
|
|
786
|
+
const payload = decodeJwt(data.sessionToken);
|
|
787
|
+
if (payload) {
|
|
788
|
+
tenantId = payload.tenantId;
|
|
789
|
+
organizationId = payload.organizationId;
|
|
790
|
+
orgRole = payload.orgRole;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
success: data.success ?? true,
|
|
795
|
+
valid: true,
|
|
796
|
+
customerId: data.customerId,
|
|
797
|
+
email: data.email,
|
|
798
|
+
sessionToken: data.sessionToken,
|
|
799
|
+
tenantId,
|
|
800
|
+
organizationId,
|
|
801
|
+
orgRole,
|
|
802
|
+
redirectUrl: data.redirectUrl
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Get the current session for an authenticated customer.
|
|
807
|
+
* Use this to validate session tokens and get customer data.
|
|
808
|
+
*
|
|
809
|
+
* Returns session info including tenant and organization context extracted from the JWT.
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```typescript
|
|
813
|
+
* // Validate session on each request
|
|
814
|
+
* const sessionToken = req.cookies.session;
|
|
815
|
+
*
|
|
816
|
+
* const session = await stackbe.auth.getSession(sessionToken);
|
|
817
|
+
*
|
|
818
|
+
* if (session) {
|
|
819
|
+
* console.log(session.customerId);
|
|
820
|
+
* console.log(session.tenantId); // Tenant context
|
|
821
|
+
* console.log(session.organizationId); // Org context (if applicable)
|
|
822
|
+
* console.log(session.subscription);
|
|
823
|
+
* console.log(session.entitlements);
|
|
824
|
+
* }
|
|
825
|
+
* ```
|
|
826
|
+
*/
|
|
827
|
+
async getSession(sessionToken) {
|
|
828
|
+
try {
|
|
829
|
+
const response = await fetch(
|
|
830
|
+
`${this.http.baseUrl}/v1/apps/${this.appId}/auth/session`,
|
|
831
|
+
{
|
|
832
|
+
headers: {
|
|
833
|
+
"Authorization": `Bearer ${sessionToken}`,
|
|
834
|
+
"Content-Type": "application/json"
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
if (!response.ok) {
|
|
839
|
+
if (response.status === 401) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
throw new StackBEError("Failed to get session", response.status, "SESSION_INVALID");
|
|
843
|
+
}
|
|
844
|
+
const data = await response.json();
|
|
845
|
+
let tenantId;
|
|
846
|
+
let organizationId;
|
|
847
|
+
let orgRole;
|
|
848
|
+
const payload = decodeJwt(sessionToken);
|
|
849
|
+
if (payload) {
|
|
850
|
+
tenantId = payload.tenantId;
|
|
851
|
+
organizationId = payload.organizationId;
|
|
852
|
+
orgRole = payload.orgRole;
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
valid: data.valid ?? true,
|
|
856
|
+
customerId: data.customerId,
|
|
857
|
+
email: data.email,
|
|
858
|
+
expiresAt: data.expiresAt,
|
|
859
|
+
tenantId: tenantId ?? data.tenantId,
|
|
860
|
+
organizationId: data.organizationId ?? organizationId,
|
|
861
|
+
orgRole: data.orgRole ?? orgRole,
|
|
862
|
+
customer: data.customer,
|
|
863
|
+
subscription: data.subscription,
|
|
864
|
+
entitlements: data.entitlements
|
|
865
|
+
};
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (error instanceof StackBEError) {
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Create Express middleware that validates session tokens.
|
|
875
|
+
*
|
|
876
|
+
* @example
|
|
877
|
+
* ```typescript
|
|
878
|
+
* // Protect routes with authentication
|
|
879
|
+
* app.use('/dashboard', stackbe.auth.middleware({
|
|
880
|
+
* getToken: (req) => req.cookies.session,
|
|
881
|
+
* onUnauthenticated: (req, res) => res.redirect('/login'),
|
|
882
|
+
* }));
|
|
883
|
+
*
|
|
884
|
+
* app.get('/dashboard', (req, res) => {
|
|
885
|
+
* // req.customer is available
|
|
886
|
+
* res.json({ email: req.customer.email });
|
|
887
|
+
* });
|
|
888
|
+
* ```
|
|
889
|
+
*/
|
|
890
|
+
middleware(options) {
|
|
891
|
+
return async (req, res, next) => {
|
|
892
|
+
const token = options.getToken(req);
|
|
893
|
+
if (!token) {
|
|
894
|
+
if (options.onUnauthenticated) {
|
|
895
|
+
return options.onUnauthenticated(req, res);
|
|
896
|
+
}
|
|
897
|
+
return res.status(401).json({ error: "Not authenticated" });
|
|
898
|
+
}
|
|
899
|
+
const session = await this.getSession(token);
|
|
900
|
+
if (!session) {
|
|
901
|
+
if (options.onUnauthenticated) {
|
|
902
|
+
return options.onUnauthenticated(req, res);
|
|
903
|
+
}
|
|
904
|
+
return res.status(401).json({ error: "Invalid session" });
|
|
905
|
+
}
|
|
906
|
+
req.customer = session.customer;
|
|
907
|
+
req.subscription = session.subscription;
|
|
908
|
+
req.entitlements = session.entitlements;
|
|
909
|
+
next();
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Check if a session token is valid.
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* ```typescript
|
|
917
|
+
* const isValid = await stackbe.auth.isAuthenticated(sessionToken);
|
|
918
|
+
* ```
|
|
919
|
+
*/
|
|
920
|
+
async isAuthenticated(sessionToken) {
|
|
921
|
+
const session = await this.getSession(sessionToken);
|
|
922
|
+
return session !== null;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
412
926
|
// src/client.ts
|
|
413
927
|
var DEFAULT_BASE_URL = "https://api.stackbe.io";
|
|
414
928
|
var DEFAULT_TIMEOUT = 3e4;
|
|
@@ -431,8 +945,18 @@ var StackBE = class {
|
|
|
431
945
|
* // Check entitlements
|
|
432
946
|
* const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium');
|
|
433
947
|
*
|
|
434
|
-
* //
|
|
435
|
-
* const
|
|
948
|
+
* // Create checkout session
|
|
949
|
+
* const { url } = await stackbe.checkout.createSession({
|
|
950
|
+
* customer: 'cust_123',
|
|
951
|
+
* planId: 'plan_pro',
|
|
952
|
+
* successUrl: 'https://myapp.com/success',
|
|
953
|
+
* });
|
|
954
|
+
*
|
|
955
|
+
* // Get subscription
|
|
956
|
+
* const subscription = await stackbe.subscriptions.get('cust_123');
|
|
957
|
+
*
|
|
958
|
+
* // Send magic link
|
|
959
|
+
* await stackbe.auth.sendMagicLink('user@example.com');
|
|
436
960
|
* ```
|
|
437
961
|
*/
|
|
438
962
|
constructor(config) {
|
|
@@ -442,6 +966,7 @@ var StackBE = class {
|
|
|
442
966
|
if (!config.appId) {
|
|
443
967
|
throw new StackBEError("appId is required", 400, "INVALID_CONFIG");
|
|
444
968
|
}
|
|
969
|
+
this.appId = config.appId;
|
|
445
970
|
this.http = new HttpClient({
|
|
446
971
|
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
447
972
|
apiKey: config.apiKey,
|
|
@@ -451,6 +976,9 @@ var StackBE = class {
|
|
|
451
976
|
this.usage = new UsageClient(this.http);
|
|
452
977
|
this.entitlements = new EntitlementsClient(this.http);
|
|
453
978
|
this.customers = new CustomersClient(this.http);
|
|
979
|
+
this.checkout = new CheckoutClient(this.http, config.appId);
|
|
980
|
+
this.subscriptions = new SubscriptionsClient(this.http);
|
|
981
|
+
this.auth = new AuthClient(this.http, config.appId);
|
|
454
982
|
}
|
|
455
983
|
/**
|
|
456
984
|
* Create a middleware for Express that tracks usage automatically.
|
|
@@ -584,9 +1112,12 @@ var StackBE = class {
|
|
|
584
1112
|
};
|
|
585
1113
|
// Annotate the CommonJS export names for ESM import in node:
|
|
586
1114
|
0 && (module.exports = {
|
|
1115
|
+
AuthClient,
|
|
1116
|
+
CheckoutClient,
|
|
587
1117
|
CustomersClient,
|
|
588
1118
|
EntitlementsClient,
|
|
589
1119
|
StackBE,
|
|
590
1120
|
StackBEError,
|
|
1121
|
+
SubscriptionsClient,
|
|
591
1122
|
UsageClient
|
|
592
1123
|
});
|