@stackbe/sdk 0.3.0 → 0.5.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 +48 -0
- package/dist/index.d.mts +200 -2
- package/dist/index.d.ts +200 -2
- package/dist/index.js +182 -6
- package/dist/index.mjs +182 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -392,6 +392,54 @@ const stackbe = new StackBE({
|
|
|
392
392
|
appId: 'app_...', // Required: Your App ID
|
|
393
393
|
baseUrl: 'https://api.stackbe.io', // Optional: API base URL
|
|
394
394
|
timeout: 30000, // Optional: Request timeout in ms
|
|
395
|
+
|
|
396
|
+
// Session caching (reduces API calls)
|
|
397
|
+
sessionCacheTTL: 120, // Optional: Cache sessions for 2 minutes
|
|
398
|
+
|
|
399
|
+
// Environment-aware redirects
|
|
400
|
+
devCallbackUrl: 'http://localhost:3000/auth/callback', // Optional: Auto-use in dev
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Session Caching
|
|
405
|
+
|
|
406
|
+
Enable session caching to reduce API calls on every request:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
const stackbe = new StackBE({
|
|
410
|
+
apiKey,
|
|
411
|
+
appId,
|
|
412
|
+
sessionCacheTTL: 120, // Cache for 2 minutes
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Cached calls won't hit the API
|
|
416
|
+
const session1 = await stackbe.auth.getSession(token); // API call
|
|
417
|
+
const session2 = await stackbe.auth.getSession(token); // From cache
|
|
418
|
+
|
|
419
|
+
// Manually invalidate cache
|
|
420
|
+
stackbe.auth.invalidateSession(token);
|
|
421
|
+
stackbe.auth.clearCache(); // Clear all
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Environment-Aware Redirects
|
|
425
|
+
|
|
426
|
+
Auto-detect development environment for magic link redirects:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
const stackbe = new StackBE({
|
|
430
|
+
apiKey,
|
|
431
|
+
appId,
|
|
432
|
+
devCallbackUrl: 'http://localhost:3000/auth/callback',
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// In development (NODE_ENV !== 'production'), uses devCallbackUrl automatically
|
|
436
|
+
await stackbe.auth.sendMagicLink('user@example.com');
|
|
437
|
+
// Redirects to: http://localhost:3000/auth/callback
|
|
438
|
+
|
|
439
|
+
// In production, uses your configured auth callback URL
|
|
440
|
+
// Or pass explicit redirectUrl to override
|
|
441
|
+
await stackbe.auth.sendMagicLink('user@example.com', {
|
|
442
|
+
redirectUrl: 'https://myapp.com/custom-callback',
|
|
395
443
|
});
|
|
396
444
|
```
|
|
397
445
|
|
package/dist/index.d.mts
CHANGED
|
@@ -11,6 +11,7 @@ declare class HttpClient {
|
|
|
11
11
|
private request;
|
|
12
12
|
get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
13
13
|
post<T>(path: string, body?: unknown, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
14
|
+
put<T>(path: string, body?: unknown, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
14
15
|
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
15
16
|
delete<T>(path: string): Promise<T>;
|
|
16
17
|
}
|
|
@@ -24,6 +25,30 @@ interface StackBEConfig {
|
|
|
24
25
|
baseUrl?: string;
|
|
25
26
|
/** Request timeout in milliseconds (default: 30000) */
|
|
26
27
|
timeout?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Session cache TTL in seconds (default: 0 = disabled).
|
|
30
|
+
* When set, getSession() results are cached to reduce API calls.
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* new StackBE({ apiKey, appId, sessionCacheTTL: 120 }) // Cache for 2 minutes
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
sessionCacheTTL?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Development callback URL for magic link redirects.
|
|
39
|
+
* When set, this URL is used automatically when:
|
|
40
|
+
* - NODE_ENV !== 'production', or
|
|
41
|
+
* - useDev: true is passed to sendMagicLink()
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* new StackBE({
|
|
45
|
+
* apiKey,
|
|
46
|
+
* appId,
|
|
47
|
+
* devCallbackUrl: 'http://localhost:3000/auth/callback'
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
devCallbackUrl?: string;
|
|
27
52
|
}
|
|
28
53
|
interface TrackUsageOptions {
|
|
29
54
|
/** Quantity to track (default: 1) */
|
|
@@ -65,6 +90,69 @@ interface CustomerUsageResponse {
|
|
|
65
90
|
billingPeriod: string;
|
|
66
91
|
metrics: UsageMetric[];
|
|
67
92
|
}
|
|
93
|
+
interface SyncUsageOptions {
|
|
94
|
+
/** Idempotency key to prevent duplicate syncs */
|
|
95
|
+
idempotencyKey?: string;
|
|
96
|
+
}
|
|
97
|
+
interface SyncUsageResponse {
|
|
98
|
+
success: boolean;
|
|
99
|
+
currentUsage: number;
|
|
100
|
+
billingPeriod: string;
|
|
101
|
+
limit: number | null;
|
|
102
|
+
remaining: number | null;
|
|
103
|
+
}
|
|
104
|
+
interface CheckUsageWithAddonsResponse extends CheckUsageResponse {
|
|
105
|
+
/** Limit from plan (before addons) */
|
|
106
|
+
planLimit: number | null;
|
|
107
|
+
/** Total addon credits available */
|
|
108
|
+
addonCredits: number;
|
|
109
|
+
/** Total effective limit (plan + addons) */
|
|
110
|
+
effectiveLimit: number | null;
|
|
111
|
+
}
|
|
112
|
+
interface TrackUsageWithAddonsResponse extends TrackUsageResponse {
|
|
113
|
+
/** Usage deducted from plan allocation */
|
|
114
|
+
planUsage: number;
|
|
115
|
+
/** Usage deducted from addon credits */
|
|
116
|
+
addonUsage: number;
|
|
117
|
+
}
|
|
118
|
+
interface UsageAddon {
|
|
119
|
+
id: string;
|
|
120
|
+
appId: string;
|
|
121
|
+
metricId: string;
|
|
122
|
+
metricName: string;
|
|
123
|
+
name: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
quantity: number;
|
|
126
|
+
priceCents: number;
|
|
127
|
+
currency: string;
|
|
128
|
+
status: 'active' | 'archived';
|
|
129
|
+
stripePriceId?: string;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
}
|
|
132
|
+
interface UsageAddonPurchase {
|
|
133
|
+
id: string;
|
|
134
|
+
addonId: string;
|
|
135
|
+
addonName: string;
|
|
136
|
+
customerId: string;
|
|
137
|
+
metricId: string;
|
|
138
|
+
metricName: string;
|
|
139
|
+
quantity: number;
|
|
140
|
+
usedQuantity: number;
|
|
141
|
+
remainingQuantity: number;
|
|
142
|
+
priceCents: number;
|
|
143
|
+
currency: string;
|
|
144
|
+
billingPeriod: string;
|
|
145
|
+
expiresAt?: string;
|
|
146
|
+
createdAt: string;
|
|
147
|
+
}
|
|
148
|
+
interface CustomerCreditsResponse {
|
|
149
|
+
customerId: string;
|
|
150
|
+
metricName: string;
|
|
151
|
+
totalPurchased: number;
|
|
152
|
+
totalUsed: number;
|
|
153
|
+
remaining: number;
|
|
154
|
+
purchases: UsageAddonPurchase[];
|
|
155
|
+
}
|
|
68
156
|
interface CheckEntitlementResponse {
|
|
69
157
|
/** Whether the customer has access to this feature */
|
|
70
158
|
hasAccess: boolean;
|
|
@@ -385,6 +473,89 @@ declare class UsageClient {
|
|
|
385
473
|
trackAndCheck(customerId: string, metric: string, options?: TrackUsageOptions): Promise<TrackUsageResponse & {
|
|
386
474
|
allowed: boolean;
|
|
387
475
|
}>;
|
|
476
|
+
/**
|
|
477
|
+
* Sync usage to an absolute value (instead of incrementing).
|
|
478
|
+
* Use this when your app tracks usage internally and reports the final count periodically.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* // Your app tracks 847 billable tickets internally
|
|
483
|
+
* await stackbe.usage.sync('cust_123', 'tickets', 847);
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
sync(customerId: string, metric: string, value: number, options?: SyncUsageOptions): Promise<SyncUsageResponse>;
|
|
487
|
+
/**
|
|
488
|
+
* Check usage limits including purchased add-on credits.
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```typescript
|
|
492
|
+
* const result = await stackbe.usage.checkWithAddons('cust_123', 'tickets');
|
|
493
|
+
*
|
|
494
|
+
* console.log(`Plan limit: ${result.planLimit}`);
|
|
495
|
+
* console.log(`Addon credits: ${result.addonCredits}`);
|
|
496
|
+
* console.log(`Effective limit: ${result.effectiveLimit}`);
|
|
497
|
+
*
|
|
498
|
+
* if (!result.allowed) {
|
|
499
|
+
* // Prompt to purchase more credits
|
|
500
|
+
* }
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
checkWithAddons(customerId: string, metric: string): Promise<CheckUsageWithAddonsResponse>;
|
|
504
|
+
/**
|
|
505
|
+
* Track usage with automatic add-on credit deduction.
|
|
506
|
+
* When plan limit is exceeded, usage is deducted from purchased add-on credits.
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* ```typescript
|
|
510
|
+
* const result = await stackbe.usage.trackWithAddons('cust_123', 'tickets');
|
|
511
|
+
*
|
|
512
|
+
* if (result.addonUsage > 0) {
|
|
513
|
+
* console.log(`Used ${result.addonUsage} from addon credits`);
|
|
514
|
+
* }
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
trackWithAddons(customerId: string, metric: string, options?: TrackUsageOptions): Promise<TrackUsageWithAddonsResponse>;
|
|
518
|
+
/**
|
|
519
|
+
* List available add-on packs for purchase.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```typescript
|
|
523
|
+
* const addons = await stackbe.usage.listAddons();
|
|
524
|
+
*
|
|
525
|
+
* for (const addon of addons) {
|
|
526
|
+
* console.log(`${addon.name}: ${addon.quantity} ${addon.metricName} for $${addon.priceCents / 100}`);
|
|
527
|
+
* }
|
|
528
|
+
* ```
|
|
529
|
+
*/
|
|
530
|
+
listAddons(): Promise<UsageAddon[]>;
|
|
531
|
+
/**
|
|
532
|
+
* Purchase an add-on pack for a customer.
|
|
533
|
+
* Credits are immediately available after purchase.
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* ```typescript
|
|
537
|
+
* // Purchase a 10k ticket pack
|
|
538
|
+
* const purchase = await stackbe.usage.purchaseAddon('cust_123', 'addon_xyz');
|
|
539
|
+
* console.log(`Purchased ${purchase.quantity} credits`);
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
purchaseAddon(customerId: string, addonId: string): Promise<UsageAddonPurchase>;
|
|
543
|
+
/**
|
|
544
|
+
* Get customer's purchased add-on credits for a metric.
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```typescript
|
|
548
|
+
* const credits = await stackbe.usage.getCredits('cust_123', 'tickets');
|
|
549
|
+
*
|
|
550
|
+
* console.log(`Total purchased: ${credits.totalPurchased}`);
|
|
551
|
+
* console.log(`Remaining: ${credits.remaining}`);
|
|
552
|
+
*
|
|
553
|
+
* for (const purchase of credits.purchases) {
|
|
554
|
+
* console.log(`${purchase.addonName}: ${purchase.remainingQuantity} left`);
|
|
555
|
+
* }
|
|
556
|
+
* ```
|
|
557
|
+
*/
|
|
558
|
+
getCredits(customerId: string, metric: string): Promise<CustomerCreditsResponse>;
|
|
388
559
|
}
|
|
389
560
|
|
|
390
561
|
declare class EntitlementsClient {
|
|
@@ -691,13 +862,37 @@ declare class SubscriptionsClient {
|
|
|
691
862
|
isActive(customerId: string): Promise<boolean>;
|
|
692
863
|
}
|
|
693
864
|
|
|
865
|
+
interface AuthClientOptions {
|
|
866
|
+
/** Session cache TTL in seconds (0 = disabled) */
|
|
867
|
+
sessionCacheTTL?: number;
|
|
868
|
+
/** Development callback URL for magic links */
|
|
869
|
+
devCallbackUrl?: string;
|
|
870
|
+
}
|
|
694
871
|
declare class AuthClient {
|
|
695
872
|
private http;
|
|
696
873
|
private appId;
|
|
697
|
-
|
|
874
|
+
private sessionCache;
|
|
875
|
+
private sessionCacheTTL;
|
|
876
|
+
private devCallbackUrl?;
|
|
877
|
+
constructor(http: HttpClient, appId: string, options?: AuthClientOptions);
|
|
878
|
+
/**
|
|
879
|
+
* Check if we're in a development environment.
|
|
880
|
+
*/
|
|
881
|
+
private isDev;
|
|
882
|
+
/**
|
|
883
|
+
* Clear the session cache (useful for testing or logout).
|
|
884
|
+
*/
|
|
885
|
+
clearCache(): void;
|
|
886
|
+
/**
|
|
887
|
+
* Remove a specific session from the cache.
|
|
888
|
+
*/
|
|
889
|
+
invalidateSession(sessionToken: string): void;
|
|
698
890
|
/**
|
|
699
891
|
* Send a magic link email to a customer for passwordless authentication.
|
|
700
892
|
*
|
|
893
|
+
* If `devCallbackUrl` is configured and we're in a dev environment (NODE_ENV !== 'production'),
|
|
894
|
+
* the dev callback URL will be used automatically.
|
|
895
|
+
*
|
|
701
896
|
* @example
|
|
702
897
|
* ```typescript
|
|
703
898
|
* // Send magic link
|
|
@@ -708,7 +903,7 @@ declare class AuthClient {
|
|
|
708
903
|
* redirectUrl: 'https://myapp.com/dashboard',
|
|
709
904
|
* });
|
|
710
905
|
*
|
|
711
|
-
* // For localhost development
|
|
906
|
+
* // For localhost development (auto-detected if devCallbackUrl is configured)
|
|
712
907
|
* await stackbe.auth.sendMagicLink('user@example.com', {
|
|
713
908
|
* useDev: true,
|
|
714
909
|
* });
|
|
@@ -748,6 +943,9 @@ declare class AuthClient {
|
|
|
748
943
|
* Get the current session for an authenticated customer.
|
|
749
944
|
* Use this to validate session tokens and get customer data.
|
|
750
945
|
*
|
|
946
|
+
* If `sessionCacheTTL` is configured, results are cached to reduce API calls.
|
|
947
|
+
* Use `invalidateSession()` or `clearCache()` to manually clear cached sessions.
|
|
948
|
+
*
|
|
751
949
|
* Returns session info including tenant and organization context extracted from the JWT.
|
|
752
950
|
*
|
|
753
951
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ declare class HttpClient {
|
|
|
11
11
|
private request;
|
|
12
12
|
get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
13
13
|
post<T>(path: string, body?: unknown, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
14
|
+
put<T>(path: string, body?: unknown, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
14
15
|
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
15
16
|
delete<T>(path: string): Promise<T>;
|
|
16
17
|
}
|
|
@@ -24,6 +25,30 @@ interface StackBEConfig {
|
|
|
24
25
|
baseUrl?: string;
|
|
25
26
|
/** Request timeout in milliseconds (default: 30000) */
|
|
26
27
|
timeout?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Session cache TTL in seconds (default: 0 = disabled).
|
|
30
|
+
* When set, getSession() results are cached to reduce API calls.
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* new StackBE({ apiKey, appId, sessionCacheTTL: 120 }) // Cache for 2 minutes
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
sessionCacheTTL?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Development callback URL for magic link redirects.
|
|
39
|
+
* When set, this URL is used automatically when:
|
|
40
|
+
* - NODE_ENV !== 'production', or
|
|
41
|
+
* - useDev: true is passed to sendMagicLink()
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* new StackBE({
|
|
45
|
+
* apiKey,
|
|
46
|
+
* appId,
|
|
47
|
+
* devCallbackUrl: 'http://localhost:3000/auth/callback'
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
devCallbackUrl?: string;
|
|
27
52
|
}
|
|
28
53
|
interface TrackUsageOptions {
|
|
29
54
|
/** Quantity to track (default: 1) */
|
|
@@ -65,6 +90,69 @@ interface CustomerUsageResponse {
|
|
|
65
90
|
billingPeriod: string;
|
|
66
91
|
metrics: UsageMetric[];
|
|
67
92
|
}
|
|
93
|
+
interface SyncUsageOptions {
|
|
94
|
+
/** Idempotency key to prevent duplicate syncs */
|
|
95
|
+
idempotencyKey?: string;
|
|
96
|
+
}
|
|
97
|
+
interface SyncUsageResponse {
|
|
98
|
+
success: boolean;
|
|
99
|
+
currentUsage: number;
|
|
100
|
+
billingPeriod: string;
|
|
101
|
+
limit: number | null;
|
|
102
|
+
remaining: number | null;
|
|
103
|
+
}
|
|
104
|
+
interface CheckUsageWithAddonsResponse extends CheckUsageResponse {
|
|
105
|
+
/** Limit from plan (before addons) */
|
|
106
|
+
planLimit: number | null;
|
|
107
|
+
/** Total addon credits available */
|
|
108
|
+
addonCredits: number;
|
|
109
|
+
/** Total effective limit (plan + addons) */
|
|
110
|
+
effectiveLimit: number | null;
|
|
111
|
+
}
|
|
112
|
+
interface TrackUsageWithAddonsResponse extends TrackUsageResponse {
|
|
113
|
+
/** Usage deducted from plan allocation */
|
|
114
|
+
planUsage: number;
|
|
115
|
+
/** Usage deducted from addon credits */
|
|
116
|
+
addonUsage: number;
|
|
117
|
+
}
|
|
118
|
+
interface UsageAddon {
|
|
119
|
+
id: string;
|
|
120
|
+
appId: string;
|
|
121
|
+
metricId: string;
|
|
122
|
+
metricName: string;
|
|
123
|
+
name: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
quantity: number;
|
|
126
|
+
priceCents: number;
|
|
127
|
+
currency: string;
|
|
128
|
+
status: 'active' | 'archived';
|
|
129
|
+
stripePriceId?: string;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
}
|
|
132
|
+
interface UsageAddonPurchase {
|
|
133
|
+
id: string;
|
|
134
|
+
addonId: string;
|
|
135
|
+
addonName: string;
|
|
136
|
+
customerId: string;
|
|
137
|
+
metricId: string;
|
|
138
|
+
metricName: string;
|
|
139
|
+
quantity: number;
|
|
140
|
+
usedQuantity: number;
|
|
141
|
+
remainingQuantity: number;
|
|
142
|
+
priceCents: number;
|
|
143
|
+
currency: string;
|
|
144
|
+
billingPeriod: string;
|
|
145
|
+
expiresAt?: string;
|
|
146
|
+
createdAt: string;
|
|
147
|
+
}
|
|
148
|
+
interface CustomerCreditsResponse {
|
|
149
|
+
customerId: string;
|
|
150
|
+
metricName: string;
|
|
151
|
+
totalPurchased: number;
|
|
152
|
+
totalUsed: number;
|
|
153
|
+
remaining: number;
|
|
154
|
+
purchases: UsageAddonPurchase[];
|
|
155
|
+
}
|
|
68
156
|
interface CheckEntitlementResponse {
|
|
69
157
|
/** Whether the customer has access to this feature */
|
|
70
158
|
hasAccess: boolean;
|
|
@@ -385,6 +473,89 @@ declare class UsageClient {
|
|
|
385
473
|
trackAndCheck(customerId: string, metric: string, options?: TrackUsageOptions): Promise<TrackUsageResponse & {
|
|
386
474
|
allowed: boolean;
|
|
387
475
|
}>;
|
|
476
|
+
/**
|
|
477
|
+
* Sync usage to an absolute value (instead of incrementing).
|
|
478
|
+
* Use this when your app tracks usage internally and reports the final count periodically.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* // Your app tracks 847 billable tickets internally
|
|
483
|
+
* await stackbe.usage.sync('cust_123', 'tickets', 847);
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
sync(customerId: string, metric: string, value: number, options?: SyncUsageOptions): Promise<SyncUsageResponse>;
|
|
487
|
+
/**
|
|
488
|
+
* Check usage limits including purchased add-on credits.
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```typescript
|
|
492
|
+
* const result = await stackbe.usage.checkWithAddons('cust_123', 'tickets');
|
|
493
|
+
*
|
|
494
|
+
* console.log(`Plan limit: ${result.planLimit}`);
|
|
495
|
+
* console.log(`Addon credits: ${result.addonCredits}`);
|
|
496
|
+
* console.log(`Effective limit: ${result.effectiveLimit}`);
|
|
497
|
+
*
|
|
498
|
+
* if (!result.allowed) {
|
|
499
|
+
* // Prompt to purchase more credits
|
|
500
|
+
* }
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
checkWithAddons(customerId: string, metric: string): Promise<CheckUsageWithAddonsResponse>;
|
|
504
|
+
/**
|
|
505
|
+
* Track usage with automatic add-on credit deduction.
|
|
506
|
+
* When plan limit is exceeded, usage is deducted from purchased add-on credits.
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* ```typescript
|
|
510
|
+
* const result = await stackbe.usage.trackWithAddons('cust_123', 'tickets');
|
|
511
|
+
*
|
|
512
|
+
* if (result.addonUsage > 0) {
|
|
513
|
+
* console.log(`Used ${result.addonUsage} from addon credits`);
|
|
514
|
+
* }
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
trackWithAddons(customerId: string, metric: string, options?: TrackUsageOptions): Promise<TrackUsageWithAddonsResponse>;
|
|
518
|
+
/**
|
|
519
|
+
* List available add-on packs for purchase.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```typescript
|
|
523
|
+
* const addons = await stackbe.usage.listAddons();
|
|
524
|
+
*
|
|
525
|
+
* for (const addon of addons) {
|
|
526
|
+
* console.log(`${addon.name}: ${addon.quantity} ${addon.metricName} for $${addon.priceCents / 100}`);
|
|
527
|
+
* }
|
|
528
|
+
* ```
|
|
529
|
+
*/
|
|
530
|
+
listAddons(): Promise<UsageAddon[]>;
|
|
531
|
+
/**
|
|
532
|
+
* Purchase an add-on pack for a customer.
|
|
533
|
+
* Credits are immediately available after purchase.
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* ```typescript
|
|
537
|
+
* // Purchase a 10k ticket pack
|
|
538
|
+
* const purchase = await stackbe.usage.purchaseAddon('cust_123', 'addon_xyz');
|
|
539
|
+
* console.log(`Purchased ${purchase.quantity} credits`);
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
purchaseAddon(customerId: string, addonId: string): Promise<UsageAddonPurchase>;
|
|
543
|
+
/**
|
|
544
|
+
* Get customer's purchased add-on credits for a metric.
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```typescript
|
|
548
|
+
* const credits = await stackbe.usage.getCredits('cust_123', 'tickets');
|
|
549
|
+
*
|
|
550
|
+
* console.log(`Total purchased: ${credits.totalPurchased}`);
|
|
551
|
+
* console.log(`Remaining: ${credits.remaining}`);
|
|
552
|
+
*
|
|
553
|
+
* for (const purchase of credits.purchases) {
|
|
554
|
+
* console.log(`${purchase.addonName}: ${purchase.remainingQuantity} left`);
|
|
555
|
+
* }
|
|
556
|
+
* ```
|
|
557
|
+
*/
|
|
558
|
+
getCredits(customerId: string, metric: string): Promise<CustomerCreditsResponse>;
|
|
388
559
|
}
|
|
389
560
|
|
|
390
561
|
declare class EntitlementsClient {
|
|
@@ -691,13 +862,37 @@ declare class SubscriptionsClient {
|
|
|
691
862
|
isActive(customerId: string): Promise<boolean>;
|
|
692
863
|
}
|
|
693
864
|
|
|
865
|
+
interface AuthClientOptions {
|
|
866
|
+
/** Session cache TTL in seconds (0 = disabled) */
|
|
867
|
+
sessionCacheTTL?: number;
|
|
868
|
+
/** Development callback URL for magic links */
|
|
869
|
+
devCallbackUrl?: string;
|
|
870
|
+
}
|
|
694
871
|
declare class AuthClient {
|
|
695
872
|
private http;
|
|
696
873
|
private appId;
|
|
697
|
-
|
|
874
|
+
private sessionCache;
|
|
875
|
+
private sessionCacheTTL;
|
|
876
|
+
private devCallbackUrl?;
|
|
877
|
+
constructor(http: HttpClient, appId: string, options?: AuthClientOptions);
|
|
878
|
+
/**
|
|
879
|
+
* Check if we're in a development environment.
|
|
880
|
+
*/
|
|
881
|
+
private isDev;
|
|
882
|
+
/**
|
|
883
|
+
* Clear the session cache (useful for testing or logout).
|
|
884
|
+
*/
|
|
885
|
+
clearCache(): void;
|
|
886
|
+
/**
|
|
887
|
+
* Remove a specific session from the cache.
|
|
888
|
+
*/
|
|
889
|
+
invalidateSession(sessionToken: string): void;
|
|
698
890
|
/**
|
|
699
891
|
* Send a magic link email to a customer for passwordless authentication.
|
|
700
892
|
*
|
|
893
|
+
* If `devCallbackUrl` is configured and we're in a dev environment (NODE_ENV !== 'production'),
|
|
894
|
+
* the dev callback URL will be used automatically.
|
|
895
|
+
*
|
|
701
896
|
* @example
|
|
702
897
|
* ```typescript
|
|
703
898
|
* // Send magic link
|
|
@@ -708,7 +903,7 @@ declare class AuthClient {
|
|
|
708
903
|
* redirectUrl: 'https://myapp.com/dashboard',
|
|
709
904
|
* });
|
|
710
905
|
*
|
|
711
|
-
* // For localhost development
|
|
906
|
+
* // For localhost development (auto-detected if devCallbackUrl is configured)
|
|
712
907
|
* await stackbe.auth.sendMagicLink('user@example.com', {
|
|
713
908
|
* useDev: true,
|
|
714
909
|
* });
|
|
@@ -748,6 +943,9 @@ declare class AuthClient {
|
|
|
748
943
|
* Get the current session for an authenticated customer.
|
|
749
944
|
* Use this to validate session tokens and get customer data.
|
|
750
945
|
*
|
|
946
|
+
* If `sessionCacheTTL` is configured, results are cached to reduce API calls.
|
|
947
|
+
* Use `invalidateSession()` or `clearCache()` to manually clear cached sessions.
|
|
948
|
+
*
|
|
751
949
|
* Returns session info including tenant and organization context extracted from the JWT.
|
|
752
950
|
*
|
|
753
951
|
* @example
|
package/dist/index.js
CHANGED
|
@@ -157,6 +157,9 @@ var HttpClient = class {
|
|
|
157
157
|
async post(path, body, params) {
|
|
158
158
|
return this.request("POST", path, { body, params });
|
|
159
159
|
}
|
|
160
|
+
async put(path, body, params) {
|
|
161
|
+
return this.request("PUT", path, { body, params });
|
|
162
|
+
}
|
|
160
163
|
async patch(path, body) {
|
|
161
164
|
return this.request("PATCH", path, { body });
|
|
162
165
|
}
|
|
@@ -256,6 +259,122 @@ var UsageClient = class {
|
|
|
256
259
|
allowed
|
|
257
260
|
};
|
|
258
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Sync usage to an absolute value (instead of incrementing).
|
|
264
|
+
* Use this when your app tracks usage internally and reports the final count periodically.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* // Your app tracks 847 billable tickets internally
|
|
269
|
+
* await stackbe.usage.sync('cust_123', 'tickets', 847);
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
async sync(customerId, metric, value, options = {}) {
|
|
273
|
+
const headers = {};
|
|
274
|
+
if (options.idempotencyKey) {
|
|
275
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
276
|
+
}
|
|
277
|
+
return this.http.put("/v1/usage/sync", {
|
|
278
|
+
customerId,
|
|
279
|
+
metric,
|
|
280
|
+
value
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Check usage limits including purchased add-on credits.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* const result = await stackbe.usage.checkWithAddons('cust_123', 'tickets');
|
|
289
|
+
*
|
|
290
|
+
* console.log(`Plan limit: ${result.planLimit}`);
|
|
291
|
+
* console.log(`Addon credits: ${result.addonCredits}`);
|
|
292
|
+
* console.log(`Effective limit: ${result.effectiveLimit}`);
|
|
293
|
+
*
|
|
294
|
+
* if (!result.allowed) {
|
|
295
|
+
* // Prompt to purchase more credits
|
|
296
|
+
* }
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
async checkWithAddons(customerId, metric) {
|
|
300
|
+
return this.http.get(
|
|
301
|
+
`/v1/customers/${customerId}/usage/check-with-addons`,
|
|
302
|
+
{ metric }
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Track usage with automatic add-on credit deduction.
|
|
307
|
+
* When plan limit is exceeded, usage is deducted from purchased add-on credits.
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* const result = await stackbe.usage.trackWithAddons('cust_123', 'tickets');
|
|
312
|
+
*
|
|
313
|
+
* if (result.addonUsage > 0) {
|
|
314
|
+
* console.log(`Used ${result.addonUsage} from addon credits`);
|
|
315
|
+
* }
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
async trackWithAddons(customerId, metric, options = {}) {
|
|
319
|
+
return this.http.post("/v1/usage/addons/track", {
|
|
320
|
+
customerId,
|
|
321
|
+
metric,
|
|
322
|
+
quantity: options.quantity ?? 1
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* List available add-on packs for purchase.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```typescript
|
|
330
|
+
* const addons = await stackbe.usage.listAddons();
|
|
331
|
+
*
|
|
332
|
+
* for (const addon of addons) {
|
|
333
|
+
* console.log(`${addon.name}: ${addon.quantity} ${addon.metricName} for $${addon.priceCents / 100}`);
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
async listAddons() {
|
|
338
|
+
return this.http.get("/v1/usage/addons");
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Purchase an add-on pack for a customer.
|
|
342
|
+
* Credits are immediately available after purchase.
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```typescript
|
|
346
|
+
* // Purchase a 10k ticket pack
|
|
347
|
+
* const purchase = await stackbe.usage.purchaseAddon('cust_123', 'addon_xyz');
|
|
348
|
+
* console.log(`Purchased ${purchase.quantity} credits`);
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
async purchaseAddon(customerId, addonId) {
|
|
352
|
+
return this.http.post("/v1/usage/addons/purchase", {
|
|
353
|
+
customerId,
|
|
354
|
+
addonId
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get customer's purchased add-on credits for a metric.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* const credits = await stackbe.usage.getCredits('cust_123', 'tickets');
|
|
363
|
+
*
|
|
364
|
+
* console.log(`Total purchased: ${credits.totalPurchased}`);
|
|
365
|
+
* console.log(`Remaining: ${credits.remaining}`);
|
|
366
|
+
*
|
|
367
|
+
* for (const purchase of credits.purchases) {
|
|
368
|
+
* console.log(`${purchase.addonName}: ${purchase.remainingQuantity} left`);
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
async getCredits(customerId, metric) {
|
|
373
|
+
return this.http.get(
|
|
374
|
+
`/v1/customers/${customerId}/usage/credits`,
|
|
375
|
+
{ metric }
|
|
376
|
+
);
|
|
377
|
+
}
|
|
259
378
|
};
|
|
260
379
|
|
|
261
380
|
// src/entitlements.ts
|
|
@@ -697,13 +816,40 @@ function decodeJwt(token) {
|
|
|
697
816
|
}
|
|
698
817
|
}
|
|
699
818
|
var AuthClient = class {
|
|
700
|
-
constructor(http, appId) {
|
|
819
|
+
constructor(http, appId, options = {}) {
|
|
701
820
|
this.http = http;
|
|
702
821
|
this.appId = appId;
|
|
822
|
+
this.sessionCache = /* @__PURE__ */ new Map();
|
|
823
|
+
this.sessionCacheTTL = (options.sessionCacheTTL ?? 0) * 1e3;
|
|
824
|
+
this.devCallbackUrl = options.devCallbackUrl;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Check if we're in a development environment.
|
|
828
|
+
*/
|
|
829
|
+
isDev() {
|
|
830
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Clear the session cache (useful for testing or logout).
|
|
837
|
+
*/
|
|
838
|
+
clearCache() {
|
|
839
|
+
this.sessionCache.clear();
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Remove a specific session from the cache.
|
|
843
|
+
*/
|
|
844
|
+
invalidateSession(sessionToken) {
|
|
845
|
+
this.sessionCache.delete(sessionToken);
|
|
703
846
|
}
|
|
704
847
|
/**
|
|
705
848
|
* Send a magic link email to a customer for passwordless authentication.
|
|
706
849
|
*
|
|
850
|
+
* If `devCallbackUrl` is configured and we're in a dev environment (NODE_ENV !== 'production'),
|
|
851
|
+
* the dev callback URL will be used automatically.
|
|
852
|
+
*
|
|
707
853
|
* @example
|
|
708
854
|
* ```typescript
|
|
709
855
|
* // Send magic link
|
|
@@ -714,19 +860,26 @@ var AuthClient = class {
|
|
|
714
860
|
* redirectUrl: 'https://myapp.com/dashboard',
|
|
715
861
|
* });
|
|
716
862
|
*
|
|
717
|
-
* // For localhost development
|
|
863
|
+
* // For localhost development (auto-detected if devCallbackUrl is configured)
|
|
718
864
|
* await stackbe.auth.sendMagicLink('user@example.com', {
|
|
719
865
|
* useDev: true,
|
|
720
866
|
* });
|
|
721
867
|
* ```
|
|
722
868
|
*/
|
|
723
869
|
async sendMagicLink(email, options = {}) {
|
|
870
|
+
let useDev = options.useDev;
|
|
871
|
+
let redirectUrl = options.redirectUrl;
|
|
872
|
+
if (!redirectUrl && this.devCallbackUrl) {
|
|
873
|
+
if (useDev || this.isDev()) {
|
|
874
|
+
redirectUrl = this.devCallbackUrl;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
724
877
|
return this.http.post(
|
|
725
878
|
`/v1/apps/${this.appId}/auth/magic-link`,
|
|
726
879
|
{
|
|
727
880
|
email,
|
|
728
|
-
redirectUrl
|
|
729
|
-
useDev
|
|
881
|
+
redirectUrl,
|
|
882
|
+
useDev
|
|
730
883
|
}
|
|
731
884
|
);
|
|
732
885
|
}
|
|
@@ -806,6 +959,9 @@ var AuthClient = class {
|
|
|
806
959
|
* Get the current session for an authenticated customer.
|
|
807
960
|
* Use this to validate session tokens and get customer data.
|
|
808
961
|
*
|
|
962
|
+
* If `sessionCacheTTL` is configured, results are cached to reduce API calls.
|
|
963
|
+
* Use `invalidateSession()` or `clearCache()` to manually clear cached sessions.
|
|
964
|
+
*
|
|
809
965
|
* Returns session info including tenant and organization context extracted from the JWT.
|
|
810
966
|
*
|
|
811
967
|
* @example
|
|
@@ -825,6 +981,15 @@ var AuthClient = class {
|
|
|
825
981
|
* ```
|
|
826
982
|
*/
|
|
827
983
|
async getSession(sessionToken) {
|
|
984
|
+
if (this.sessionCacheTTL > 0) {
|
|
985
|
+
const cached = this.sessionCache.get(sessionToken);
|
|
986
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
987
|
+
return cached.session;
|
|
988
|
+
}
|
|
989
|
+
if (cached) {
|
|
990
|
+
this.sessionCache.delete(sessionToken);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
828
993
|
try {
|
|
829
994
|
const response = await fetch(
|
|
830
995
|
`${this.http.baseUrl}/v1/apps/${this.appId}/auth/session`,
|
|
@@ -837,6 +1002,7 @@ var AuthClient = class {
|
|
|
837
1002
|
);
|
|
838
1003
|
if (!response.ok) {
|
|
839
1004
|
if (response.status === 401) {
|
|
1005
|
+
this.sessionCache.delete(sessionToken);
|
|
840
1006
|
return null;
|
|
841
1007
|
}
|
|
842
1008
|
throw new StackBEError("Failed to get session", response.status, "SESSION_INVALID");
|
|
@@ -851,7 +1017,7 @@ var AuthClient = class {
|
|
|
851
1017
|
organizationId = payload.organizationId;
|
|
852
1018
|
orgRole = payload.orgRole;
|
|
853
1019
|
}
|
|
854
|
-
|
|
1020
|
+
const session = {
|
|
855
1021
|
valid: data.valid ?? true,
|
|
856
1022
|
customerId: data.customerId,
|
|
857
1023
|
email: data.email,
|
|
@@ -863,6 +1029,13 @@ var AuthClient = class {
|
|
|
863
1029
|
subscription: data.subscription,
|
|
864
1030
|
entitlements: data.entitlements
|
|
865
1031
|
};
|
|
1032
|
+
if (this.sessionCacheTTL > 0) {
|
|
1033
|
+
this.sessionCache.set(sessionToken, {
|
|
1034
|
+
session,
|
|
1035
|
+
expiresAt: Date.now() + this.sessionCacheTTL
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
return session;
|
|
866
1039
|
} catch (error) {
|
|
867
1040
|
if (error instanceof StackBEError) {
|
|
868
1041
|
throw error;
|
|
@@ -978,7 +1151,10 @@ var StackBE = class {
|
|
|
978
1151
|
this.customers = new CustomersClient(this.http);
|
|
979
1152
|
this.checkout = new CheckoutClient(this.http, config.appId);
|
|
980
1153
|
this.subscriptions = new SubscriptionsClient(this.http);
|
|
981
|
-
this.auth = new AuthClient(this.http, config.appId
|
|
1154
|
+
this.auth = new AuthClient(this.http, config.appId, {
|
|
1155
|
+
sessionCacheTTL: config.sessionCacheTTL,
|
|
1156
|
+
devCallbackUrl: config.devCallbackUrl
|
|
1157
|
+
});
|
|
982
1158
|
}
|
|
983
1159
|
/**
|
|
984
1160
|
* Create a middleware for Express that tracks usage automatically.
|
package/dist/index.mjs
CHANGED
|
@@ -124,6 +124,9 @@ var HttpClient = class {
|
|
|
124
124
|
async post(path, body, params) {
|
|
125
125
|
return this.request("POST", path, { body, params });
|
|
126
126
|
}
|
|
127
|
+
async put(path, body, params) {
|
|
128
|
+
return this.request("PUT", path, { body, params });
|
|
129
|
+
}
|
|
127
130
|
async patch(path, body) {
|
|
128
131
|
return this.request("PATCH", path, { body });
|
|
129
132
|
}
|
|
@@ -223,6 +226,122 @@ var UsageClient = class {
|
|
|
223
226
|
allowed
|
|
224
227
|
};
|
|
225
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Sync usage to an absolute value (instead of incrementing).
|
|
231
|
+
* Use this when your app tracks usage internally and reports the final count periodically.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* // Your app tracks 847 billable tickets internally
|
|
236
|
+
* await stackbe.usage.sync('cust_123', 'tickets', 847);
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
async sync(customerId, metric, value, options = {}) {
|
|
240
|
+
const headers = {};
|
|
241
|
+
if (options.idempotencyKey) {
|
|
242
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
243
|
+
}
|
|
244
|
+
return this.http.put("/v1/usage/sync", {
|
|
245
|
+
customerId,
|
|
246
|
+
metric,
|
|
247
|
+
value
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Check usage limits including purchased add-on credits.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* const result = await stackbe.usage.checkWithAddons('cust_123', 'tickets');
|
|
256
|
+
*
|
|
257
|
+
* console.log(`Plan limit: ${result.planLimit}`);
|
|
258
|
+
* console.log(`Addon credits: ${result.addonCredits}`);
|
|
259
|
+
* console.log(`Effective limit: ${result.effectiveLimit}`);
|
|
260
|
+
*
|
|
261
|
+
* if (!result.allowed) {
|
|
262
|
+
* // Prompt to purchase more credits
|
|
263
|
+
* }
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
async checkWithAddons(customerId, metric) {
|
|
267
|
+
return this.http.get(
|
|
268
|
+
`/v1/customers/${customerId}/usage/check-with-addons`,
|
|
269
|
+
{ metric }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Track usage with automatic add-on credit deduction.
|
|
274
|
+
* When plan limit is exceeded, usage is deducted from purchased add-on credits.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* const result = await stackbe.usage.trackWithAddons('cust_123', 'tickets');
|
|
279
|
+
*
|
|
280
|
+
* if (result.addonUsage > 0) {
|
|
281
|
+
* console.log(`Used ${result.addonUsage} from addon credits`);
|
|
282
|
+
* }
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
async trackWithAddons(customerId, metric, options = {}) {
|
|
286
|
+
return this.http.post("/v1/usage/addons/track", {
|
|
287
|
+
customerId,
|
|
288
|
+
metric,
|
|
289
|
+
quantity: options.quantity ?? 1
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* List available add-on packs for purchase.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```typescript
|
|
297
|
+
* const addons = await stackbe.usage.listAddons();
|
|
298
|
+
*
|
|
299
|
+
* for (const addon of addons) {
|
|
300
|
+
* console.log(`${addon.name}: ${addon.quantity} ${addon.metricName} for $${addon.priceCents / 100}`);
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
async listAddons() {
|
|
305
|
+
return this.http.get("/v1/usage/addons");
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Purchase an add-on pack for a customer.
|
|
309
|
+
* Credits are immediately available after purchase.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* // Purchase a 10k ticket pack
|
|
314
|
+
* const purchase = await stackbe.usage.purchaseAddon('cust_123', 'addon_xyz');
|
|
315
|
+
* console.log(`Purchased ${purchase.quantity} credits`);
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
async purchaseAddon(customerId, addonId) {
|
|
319
|
+
return this.http.post("/v1/usage/addons/purchase", {
|
|
320
|
+
customerId,
|
|
321
|
+
addonId
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get customer's purchased add-on credits for a metric.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```typescript
|
|
329
|
+
* const credits = await stackbe.usage.getCredits('cust_123', 'tickets');
|
|
330
|
+
*
|
|
331
|
+
* console.log(`Total purchased: ${credits.totalPurchased}`);
|
|
332
|
+
* console.log(`Remaining: ${credits.remaining}`);
|
|
333
|
+
*
|
|
334
|
+
* for (const purchase of credits.purchases) {
|
|
335
|
+
* console.log(`${purchase.addonName}: ${purchase.remainingQuantity} left`);
|
|
336
|
+
* }
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
async getCredits(customerId, metric) {
|
|
340
|
+
return this.http.get(
|
|
341
|
+
`/v1/customers/${customerId}/usage/credits`,
|
|
342
|
+
{ metric }
|
|
343
|
+
);
|
|
344
|
+
}
|
|
226
345
|
};
|
|
227
346
|
|
|
228
347
|
// src/entitlements.ts
|
|
@@ -664,13 +783,40 @@ function decodeJwt(token) {
|
|
|
664
783
|
}
|
|
665
784
|
}
|
|
666
785
|
var AuthClient = class {
|
|
667
|
-
constructor(http, appId) {
|
|
786
|
+
constructor(http, appId, options = {}) {
|
|
668
787
|
this.http = http;
|
|
669
788
|
this.appId = appId;
|
|
789
|
+
this.sessionCache = /* @__PURE__ */ new Map();
|
|
790
|
+
this.sessionCacheTTL = (options.sessionCacheTTL ?? 0) * 1e3;
|
|
791
|
+
this.devCallbackUrl = options.devCallbackUrl;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Check if we're in a development environment.
|
|
795
|
+
*/
|
|
796
|
+
isDev() {
|
|
797
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Clear the session cache (useful for testing or logout).
|
|
804
|
+
*/
|
|
805
|
+
clearCache() {
|
|
806
|
+
this.sessionCache.clear();
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Remove a specific session from the cache.
|
|
810
|
+
*/
|
|
811
|
+
invalidateSession(sessionToken) {
|
|
812
|
+
this.sessionCache.delete(sessionToken);
|
|
670
813
|
}
|
|
671
814
|
/**
|
|
672
815
|
* Send a magic link email to a customer for passwordless authentication.
|
|
673
816
|
*
|
|
817
|
+
* If `devCallbackUrl` is configured and we're in a dev environment (NODE_ENV !== 'production'),
|
|
818
|
+
* the dev callback URL will be used automatically.
|
|
819
|
+
*
|
|
674
820
|
* @example
|
|
675
821
|
* ```typescript
|
|
676
822
|
* // Send magic link
|
|
@@ -681,19 +827,26 @@ var AuthClient = class {
|
|
|
681
827
|
* redirectUrl: 'https://myapp.com/dashboard',
|
|
682
828
|
* });
|
|
683
829
|
*
|
|
684
|
-
* // For localhost development
|
|
830
|
+
* // For localhost development (auto-detected if devCallbackUrl is configured)
|
|
685
831
|
* await stackbe.auth.sendMagicLink('user@example.com', {
|
|
686
832
|
* useDev: true,
|
|
687
833
|
* });
|
|
688
834
|
* ```
|
|
689
835
|
*/
|
|
690
836
|
async sendMagicLink(email, options = {}) {
|
|
837
|
+
let useDev = options.useDev;
|
|
838
|
+
let redirectUrl = options.redirectUrl;
|
|
839
|
+
if (!redirectUrl && this.devCallbackUrl) {
|
|
840
|
+
if (useDev || this.isDev()) {
|
|
841
|
+
redirectUrl = this.devCallbackUrl;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
691
844
|
return this.http.post(
|
|
692
845
|
`/v1/apps/${this.appId}/auth/magic-link`,
|
|
693
846
|
{
|
|
694
847
|
email,
|
|
695
|
-
redirectUrl
|
|
696
|
-
useDev
|
|
848
|
+
redirectUrl,
|
|
849
|
+
useDev
|
|
697
850
|
}
|
|
698
851
|
);
|
|
699
852
|
}
|
|
@@ -773,6 +926,9 @@ var AuthClient = class {
|
|
|
773
926
|
* Get the current session for an authenticated customer.
|
|
774
927
|
* Use this to validate session tokens and get customer data.
|
|
775
928
|
*
|
|
929
|
+
* If `sessionCacheTTL` is configured, results are cached to reduce API calls.
|
|
930
|
+
* Use `invalidateSession()` or `clearCache()` to manually clear cached sessions.
|
|
931
|
+
*
|
|
776
932
|
* Returns session info including tenant and organization context extracted from the JWT.
|
|
777
933
|
*
|
|
778
934
|
* @example
|
|
@@ -792,6 +948,15 @@ var AuthClient = class {
|
|
|
792
948
|
* ```
|
|
793
949
|
*/
|
|
794
950
|
async getSession(sessionToken) {
|
|
951
|
+
if (this.sessionCacheTTL > 0) {
|
|
952
|
+
const cached = this.sessionCache.get(sessionToken);
|
|
953
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
954
|
+
return cached.session;
|
|
955
|
+
}
|
|
956
|
+
if (cached) {
|
|
957
|
+
this.sessionCache.delete(sessionToken);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
795
960
|
try {
|
|
796
961
|
const response = await fetch(
|
|
797
962
|
`${this.http.baseUrl}/v1/apps/${this.appId}/auth/session`,
|
|
@@ -804,6 +969,7 @@ var AuthClient = class {
|
|
|
804
969
|
);
|
|
805
970
|
if (!response.ok) {
|
|
806
971
|
if (response.status === 401) {
|
|
972
|
+
this.sessionCache.delete(sessionToken);
|
|
807
973
|
return null;
|
|
808
974
|
}
|
|
809
975
|
throw new StackBEError("Failed to get session", response.status, "SESSION_INVALID");
|
|
@@ -818,7 +984,7 @@ var AuthClient = class {
|
|
|
818
984
|
organizationId = payload.organizationId;
|
|
819
985
|
orgRole = payload.orgRole;
|
|
820
986
|
}
|
|
821
|
-
|
|
987
|
+
const session = {
|
|
822
988
|
valid: data.valid ?? true,
|
|
823
989
|
customerId: data.customerId,
|
|
824
990
|
email: data.email,
|
|
@@ -830,6 +996,13 @@ var AuthClient = class {
|
|
|
830
996
|
subscription: data.subscription,
|
|
831
997
|
entitlements: data.entitlements
|
|
832
998
|
};
|
|
999
|
+
if (this.sessionCacheTTL > 0) {
|
|
1000
|
+
this.sessionCache.set(sessionToken, {
|
|
1001
|
+
session,
|
|
1002
|
+
expiresAt: Date.now() + this.sessionCacheTTL
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
return session;
|
|
833
1006
|
} catch (error) {
|
|
834
1007
|
if (error instanceof StackBEError) {
|
|
835
1008
|
throw error;
|
|
@@ -945,7 +1118,10 @@ var StackBE = class {
|
|
|
945
1118
|
this.customers = new CustomersClient(this.http);
|
|
946
1119
|
this.checkout = new CheckoutClient(this.http, config.appId);
|
|
947
1120
|
this.subscriptions = new SubscriptionsClient(this.http);
|
|
948
|
-
this.auth = new AuthClient(this.http, config.appId
|
|
1121
|
+
this.auth = new AuthClient(this.http, config.appId, {
|
|
1122
|
+
sessionCacheTTL: config.sessionCacheTTL,
|
|
1123
|
+
devCallbackUrl: config.devCallbackUrl
|
|
1124
|
+
});
|
|
949
1125
|
}
|
|
950
1126
|
/**
|
|
951
1127
|
* Create a middleware for Express that tracks usage automatically.
|
package/package.json
CHANGED