@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 CHANGED
@@ -6,10 +6,6 @@ Official JavaScript/TypeScript SDK for [StackBE](https://stackbe.io) - the billi
6
6
 
7
7
  ```bash
8
8
  npm install @stackbe/sdk
9
- # or
10
- yarn add @stackbe/sdk
11
- # or
12
- pnpm add @stackbe/sdk
13
9
  ```
14
10
 
15
11
  ## Quick Start
@@ -25,14 +21,26 @@ const stackbe = new StackBE({
25
21
  // Track usage
26
22
  await stackbe.usage.track('customer_123', 'api_calls');
27
23
 
28
- // Check if within limits
29
- const { allowed, remaining } = await stackbe.usage.check('customer_123', 'api_calls');
30
-
31
- // Check feature access
24
+ // Check entitlements
32
25
  const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium_export');
26
+
27
+ // Create checkout session
28
+ const { url } = await stackbe.checkout.createSession({
29
+ customer: 'cust_123',
30
+ planId: 'plan_pro',
31
+ successUrl: 'https://myapp.com/success',
32
+ });
33
+
34
+ // Get subscription
35
+ const subscription = await stackbe.subscriptions.get('cust_123');
36
+
37
+ // Send magic link
38
+ await stackbe.auth.sendMagicLink('user@example.com');
33
39
  ```
34
40
 
35
- ## Usage Tracking
41
+ ## Modules
42
+
43
+ ### Usage Tracking
36
44
 
37
45
  Track billable usage events for your customers:
38
46
 
@@ -43,17 +51,16 @@ await stackbe.usage.track('customer_123', 'api_calls');
43
51
  // Track multiple units
44
52
  await stackbe.usage.track('customer_123', 'tokens', { quantity: 1500 });
45
53
 
46
- // Check current usage
47
- const usage = await stackbe.usage.get('customer_123');
48
- console.log(usage.metrics);
49
- // [{ metric: 'api_calls', currentUsage: 150, limit: 1000, remaining: 850 }]
50
-
51
- // Check specific metric
54
+ // Check if within limits
52
55
  const { allowed, remaining } = await stackbe.usage.check('customer_123', 'api_calls');
53
56
  if (!allowed) {
54
57
  throw new Error('Usage limit exceeded');
55
58
  }
56
59
 
60
+ // Get full usage summary
61
+ const usage = await stackbe.usage.get('customer_123');
62
+ console.log(usage.metrics);
63
+
57
64
  // Track and check in one call
58
65
  const result = await stackbe.usage.trackAndCheck('customer_123', 'api_calls');
59
66
  if (!result.allowed) {
@@ -61,7 +68,7 @@ if (!result.allowed) {
61
68
  }
62
69
  ```
63
70
 
64
- ## Entitlements
71
+ ### Entitlements
65
72
 
66
73
  Check feature access based on customer's plan:
67
74
 
@@ -69,33 +76,135 @@ Check feature access based on customer's plan:
69
76
  // Check single feature
70
77
  const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium_export');
71
78
 
72
- if (!hasAccess) {
73
- return res.status(403).json({ error: 'Upgrade to access this feature' });
74
- }
75
-
76
79
  // Get all entitlements
77
80
  const { entitlements, planName } = await stackbe.entitlements.getAll('customer_123');
78
- console.log(`Customer is on ${planName}`);
79
- console.log(entitlements);
80
81
  // { premium_export: true, api_access: true, max_projects: 10 }
81
82
 
82
83
  // Check multiple features at once
83
84
  const features = await stackbe.entitlements.checkMany('customer_123', [
84
85
  'premium_export',
85
86
  'advanced_analytics',
86
- 'api_access'
87
87
  ]);
88
- // { premium_export: true, advanced_analytics: false, api_access: true }
89
88
 
90
89
  // Require a feature (throws if not available)
91
90
  await stackbe.entitlements.require('customer_123', 'premium_export');
92
91
  ```
93
92
 
94
- ## Customer Management
93
+ ### Checkout
94
+
95
+ Create Stripe checkout sessions:
96
+
97
+ ```typescript
98
+ // With existing customer ID
99
+ const { url } = await stackbe.checkout.createSession({
100
+ customer: 'cust_123',
101
+ planId: 'plan_pro_monthly',
102
+ successUrl: 'https://myapp.com/success',
103
+ cancelUrl: 'https://myapp.com/pricing',
104
+ });
105
+
106
+ // Redirect to checkout
107
+ res.redirect(url);
108
+
109
+ // With new customer (will be created)
110
+ const { url } = await stackbe.checkout.createSession({
111
+ customer: { email: 'user@example.com', name: 'John' },
112
+ planId: 'plan_pro_monthly',
113
+ successUrl: 'https://myapp.com/success',
114
+ trialDays: 14,
115
+ });
116
+
117
+ // Get checkout URL directly
118
+ const checkoutUrl = await stackbe.checkout.getCheckoutUrl({
119
+ customer: 'cust_123',
120
+ planId: 'plan_pro',
121
+ successUrl: 'https://myapp.com/success',
122
+ });
123
+ ```
124
+
125
+ ### Subscriptions
126
+
127
+ Manage customer subscriptions:
128
+
129
+ ```typescript
130
+ // Get current subscription
131
+ const subscription = await stackbe.subscriptions.get('cust_123');
132
+ if (subscription) {
133
+ console.log(`Plan: ${subscription.plan.name}`);
134
+ console.log(`Status: ${subscription.status}`);
135
+ }
136
+
137
+ // Check if customer has active subscription
138
+ const isActive = await stackbe.subscriptions.isActive('cust_123');
139
+
140
+ // Cancel subscription (at end of billing period)
141
+ await stackbe.subscriptions.cancel('sub_123');
142
+
143
+ // Cancel immediately
144
+ await stackbe.subscriptions.cancel('sub_123', { immediate: true });
145
+
146
+ // Update subscription (change plan)
147
+ await stackbe.subscriptions.update('sub_123', {
148
+ planId: 'plan_enterprise',
149
+ prorate: true,
150
+ });
151
+
152
+ // Reactivate canceled subscription
153
+ await stackbe.subscriptions.reactivate('sub_123');
154
+
155
+ // List all subscriptions
156
+ const subscriptions = await stackbe.subscriptions.list('cust_123');
157
+ ```
158
+
159
+ ### Authentication
160
+
161
+ Passwordless authentication with magic links:
162
+
163
+ ```typescript
164
+ // Send magic link
165
+ await stackbe.auth.sendMagicLink('user@example.com');
166
+
167
+ // With redirect URL
168
+ await stackbe.auth.sendMagicLink('user@example.com', {
169
+ redirectUrl: 'https://myapp.com/dashboard',
170
+ });
171
+
172
+ // For localhost development
173
+ await stackbe.auth.sendMagicLink('user@example.com', {
174
+ useDev: true,
175
+ });
176
+
177
+ // Verify magic link token (in your /verify route)
178
+ const { token } = req.query;
179
+ const result = await stackbe.auth.verifyToken(token);
180
+
181
+ // result includes tenant and org context:
182
+ // - customerId, email, sessionToken
183
+ // - tenantId (your StackBE tenant)
184
+ // - organizationId, orgRole (if in org context)
185
+ res.cookie('session', result.sessionToken, { httpOnly: true });
186
+ res.redirect('/dashboard');
187
+
188
+ // Get session from token (includes tenant context)
189
+ const session = await stackbe.auth.getSession(sessionToken);
190
+ if (session) {
191
+ console.log(session.customerId);
192
+ console.log(session.email);
193
+ console.log(session.tenantId); // Tenant context
194
+ console.log(session.organizationId); // Org context (if applicable)
195
+ console.log(session.subscription);
196
+ console.log(session.entitlements);
197
+ }
198
+
199
+ // Check if authenticated
200
+ const isAuthenticated = await stackbe.auth.isAuthenticated(sessionToken);
201
+ ```
202
+
203
+ ### Customer Management
95
204
 
96
205
  ```typescript
97
206
  // Get customer
98
- const customer = await stackbe.customers.get('customer_123');
207
+ const customer = await stackbe.customers.get('cust_123');
99
208
 
100
209
  // Get by email
101
210
  const customer = await stackbe.customers.getByEmail('user@example.com');
@@ -104,53 +213,41 @@ const customer = await stackbe.customers.getByEmail('user@example.com');
104
213
  const newCustomer = await stackbe.customers.create({
105
214
  email: 'user@example.com',
106
215
  name: 'John Doe',
107
- metadata: { source: 'api' }
216
+ metadata: { source: 'api' },
108
217
  });
109
218
 
110
219
  // Get or create (idempotent)
111
220
  const customer = await stackbe.customers.getOrCreate({
112
221
  email: 'user@example.com',
113
- name: 'John Doe'
222
+ name: 'John Doe',
114
223
  });
115
224
 
116
225
  // Update customer
117
- await stackbe.customers.update('customer_123', {
118
- name: 'Jane Doe'
119
- });
226
+ await stackbe.customers.update('cust_123', { name: 'Jane Doe' });
120
227
  ```
121
228
 
122
229
  ## Express Middleware
123
230
 
124
- The SDK includes ready-to-use middleware for Express:
125
-
126
231
  ### Track Usage Automatically
127
232
 
128
233
  ```typescript
129
- import express from 'express';
130
- import { StackBE } from '@stackbe/sdk';
131
-
132
- const app = express();
133
- const stackbe = new StackBE({ apiKey: '...', appId: '...' });
134
-
135
- // Track all API calls
136
234
  app.use(stackbe.middleware({
137
235
  getCustomerId: (req) => req.user?.customerId,
138
236
  metric: 'api_calls',
139
- skip: (req) => req.path === '/health', // Skip health checks
237
+ skip: (req) => req.path === '/health',
140
238
  }));
141
239
  ```
142
240
 
143
241
  ### Require Feature Entitlements
144
242
 
145
243
  ```typescript
146
- // Protect premium endpoints
147
244
  app.get('/api/export',
148
245
  stackbe.requireFeature({
149
246
  getCustomerId: (req) => req.user?.customerId,
150
247
  feature: 'premium_export',
151
248
  onDenied: (req, res) => {
152
- res.status(403).json({ error: 'Upgrade to Pro to access exports' });
153
- }
249
+ res.status(403).json({ error: 'Upgrade to Pro' });
250
+ },
154
251
  }),
155
252
  exportHandler
156
253
  );
@@ -159,23 +256,33 @@ app.get('/api/export',
159
256
  ### Enforce Usage Limits
160
257
 
161
258
  ```typescript
162
- // Block requests when limit exceeded
163
259
  app.use('/api',
164
260
  stackbe.enforceLimit({
165
261
  getCustomerId: (req) => req.user?.customerId,
166
262
  metric: 'api_calls',
167
263
  onLimitExceeded: (req, res, { current, limit }) => {
168
- res.status(429).json({
169
- error: 'Rate limit exceeded',
170
- current,
171
- limit,
172
- upgradeUrl: 'https://myapp.com/upgrade'
173
- });
174
- }
264
+ res.status(429).json({ error: 'Rate limit exceeded', current, limit });
265
+ },
175
266
  })
176
267
  );
177
268
  ```
178
269
 
270
+ ### Authenticate Requests
271
+
272
+ ```typescript
273
+ app.use('/dashboard',
274
+ stackbe.auth.middleware({
275
+ getToken: (req) => req.cookies.session,
276
+ onUnauthenticated: (req, res) => res.redirect('/login'),
277
+ })
278
+ );
279
+
280
+ app.get('/dashboard', (req, res) => {
281
+ // req.customer, req.subscription, req.entitlements are available
282
+ res.json({ email: req.customer.email });
283
+ });
284
+ ```
285
+
179
286
  ## Next.js Integration
180
287
 
181
288
  ### API Routes (App Router)
@@ -193,30 +300,23 @@ const stackbe = new StackBE({
193
300
  export async function POST(request: Request) {
194
301
  const { customerId } = await request.json();
195
302
 
196
- // Check limits before processing
303
+ // Check limits
197
304
  const { allowed, remaining } = await stackbe.usage.check(customerId, 'generations');
198
-
199
305
  if (!allowed) {
200
- return NextResponse.json(
201
- { error: 'Generation limit reached', remaining: 0 },
202
- { status: 429 }
203
- );
306
+ return NextResponse.json({ error: 'Limit reached' }, { status: 429 });
204
307
  }
205
308
 
206
309
  // Track usage
207
310
  await stackbe.usage.track(customerId, 'generations');
208
311
 
209
- // Do the work...
210
- const result = await generateSomething();
211
-
212
- return NextResponse.json({ result, remaining: remaining! - 1 });
312
+ // Do work...
313
+ return NextResponse.json({ success: true, remaining: remaining! - 1 });
213
314
  }
214
315
  ```
215
316
 
216
317
  ### Server Actions
217
318
 
218
319
  ```typescript
219
- // app/actions.ts
220
320
  'use server';
221
321
 
222
322
  import { StackBE } from '@stackbe/sdk';
@@ -227,57 +327,143 @@ const stackbe = new StackBE({
227
327
  });
228
328
 
229
329
  export async function exportData(customerId: string) {
230
- // Check feature access
231
330
  const { hasAccess } = await stackbe.entitlements.check(customerId, 'data_export');
232
-
233
331
  if (!hasAccess) {
234
332
  throw new Error('Upgrade to Pro to export data');
235
333
  }
236
-
237
334
  // Perform export...
238
335
  }
239
336
  ```
240
337
 
241
338
  ## Error Handling
242
339
 
340
+ The SDK provides typed error codes for specific error handling:
341
+
243
342
  ```typescript
244
343
  import { StackBE, StackBEError } from '@stackbe/sdk';
245
344
 
246
345
  try {
247
- await stackbe.usage.track('customer_123', 'api_calls');
346
+ await stackbe.auth.verifyToken(token);
248
347
  } catch (error) {
249
348
  if (error instanceof StackBEError) {
250
- console.error(`StackBE Error: ${error.message}`);
251
- console.error(`Status: ${error.statusCode}`);
252
- console.error(`Code: ${error.code}`);
349
+ // Handle specific error types
350
+ switch (error.code) {
351
+ case 'TOKEN_EXPIRED':
352
+ return res.redirect('/login?error=expired');
353
+ case 'TOKEN_ALREADY_USED':
354
+ return res.redirect('/login?error=used');
355
+ case 'SESSION_EXPIRED':
356
+ return res.redirect('/login');
357
+ case 'CUSTOMER_NOT_FOUND':
358
+ return res.status(404).json({ error: 'Customer not found' });
359
+ case 'USAGE_LIMIT_EXCEEDED':
360
+ return res.status(429).json({ error: 'Limit exceeded' });
361
+ default:
362
+ return res.status(500).json({ error: 'Something went wrong' });
363
+ }
364
+
365
+ // Or use helper methods
366
+ if (error.isAuthError()) {
367
+ return res.redirect('/login');
368
+ }
369
+ if (error.isNotFoundError()) {
370
+ return res.status(404).json({ error: error.message });
371
+ }
253
372
  }
254
373
  }
255
374
  ```
256
375
 
376
+ ### Error Codes
377
+
378
+ | Category | Codes |
379
+ |----------|-------|
380
+ | Auth | `TOKEN_EXPIRED`, `TOKEN_ALREADY_USED`, `TOKEN_INVALID`, `SESSION_EXPIRED`, `SESSION_INVALID`, `UNAUTHORIZED` |
381
+ | Resources | `NOT_FOUND`, `CUSTOMER_NOT_FOUND`, `SUBSCRIPTION_NOT_FOUND`, `PLAN_NOT_FOUND`, `APP_NOT_FOUND` |
382
+ | Usage | `USAGE_LIMIT_EXCEEDED`, `METRIC_NOT_FOUND` |
383
+ | Entitlements | `FEATURE_NOT_AVAILABLE`, `NO_ACTIVE_SUBSCRIPTION` |
384
+ | Validation | `VALIDATION_ERROR`, `MISSING_REQUIRED_FIELD`, `INVALID_EMAIL` |
385
+ | Network | `TIMEOUT`, `NETWORK_ERROR`, `UNKNOWN_ERROR` |
386
+
257
387
  ## Configuration
258
388
 
259
389
  ```typescript
260
390
  const stackbe = new StackBE({
261
- // Required
262
- apiKey: 'sk_live_...', // Your API key
263
- appId: 'app_...', // Your App ID
391
+ apiKey: 'sk_live_...', // Required: Your API key
392
+ appId: 'app_...', // Required: Your App ID
393
+ baseUrl: 'https://api.stackbe.io', // Optional: API base URL
394
+ timeout: 30000, // Optional: Request timeout in ms
395
+ });
396
+ ```
397
+
398
+ ## Webhooks
264
399
 
265
- // Optional
266
- baseUrl: 'https://api.stackbe.io', // API base URL
267
- timeout: 30000, // Request timeout in ms
400
+ Typed webhook payloads for handling StackBE events:
401
+
402
+ ```typescript
403
+ import type {
404
+ AnyWebhookEvent,
405
+ SubscriptionCreatedEvent,
406
+ SubscriptionCancelledEvent,
407
+ PaymentFailedEvent,
408
+ } from '@stackbe/sdk';
409
+
410
+ // In your webhook handler
411
+ app.post('/webhooks/stackbe', (req, res) => {
412
+ const event = req.body as AnyWebhookEvent;
413
+
414
+ switch (event.type) {
415
+ case 'subscription_created':
416
+ const sub = event as SubscriptionCreatedEvent;
417
+ console.log(`New subscription: ${sub.data.planName}`);
418
+ break;
419
+
420
+ case 'subscription_cancelled':
421
+ const cancelled = event as SubscriptionCancelledEvent;
422
+ console.log(`Cancelled: ${cancelled.data.id}`);
423
+ break;
424
+
425
+ case 'payment_failed':
426
+ const payment = event as PaymentFailedEvent;
427
+ console.log(`Payment failed: ${payment.data.failureReason}`);
428
+ // Send dunning email
429
+ break;
430
+ }
431
+
432
+ res.json({ received: true });
268
433
  });
269
434
  ```
270
435
 
436
+ ### Webhook Event Types
437
+
438
+ | Event | Payload |
439
+ |-------|---------|
440
+ | `subscription_created` | `SubscriptionWebhookPayload` |
441
+ | `subscription_updated` | `SubscriptionWebhookPayload` |
442
+ | `subscription_cancelled` | `SubscriptionWebhookPayload` |
443
+ | `subscription_renewed` | `SubscriptionWebhookPayload` |
444
+ | `trial_started` | `SubscriptionWebhookPayload` |
445
+ | `trial_ended` | `SubscriptionWebhookPayload` |
446
+ | `payment_succeeded` | `PaymentWebhookPayload` |
447
+ | `payment_failed` | `PaymentWebhookPayload` |
448
+ | `customer_created` | `CustomerWebhookPayload` |
449
+ | `customer_updated` | `CustomerWebhookPayload` |
450
+
271
451
  ## TypeScript
272
452
 
273
- The SDK is written in TypeScript and includes full type definitions:
453
+ Full type definitions included:
274
454
 
275
455
  ```typescript
276
456
  import type {
277
457
  Customer,
278
458
  Subscription,
459
+ SubscriptionWithPlan,
460
+ CheckoutSessionResponse,
461
+ SessionResponse,
279
462
  TrackUsageResponse,
280
463
  CheckEntitlementResponse,
464
+ StackBEErrorCode,
465
+ WebhookEventType,
466
+ AnyWebhookEvent,
281
467
  } from '@stackbe/sdk';
282
468
  ```
283
469