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