@stackbe/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,561 @@
1
+ // src/types.ts
2
+ var StackBEError = class extends Error {
3
+ constructor(message, statusCode, code) {
4
+ super(message);
5
+ this.name = "StackBEError";
6
+ this.statusCode = statusCode;
7
+ this.code = code;
8
+ }
9
+ };
10
+
11
+ // src/http.ts
12
+ var HttpClient = class {
13
+ constructor(config) {
14
+ this.config = config;
15
+ }
16
+ async request(method, path, options = {}) {
17
+ const url = new URL(path, this.config.baseUrl);
18
+ if (options.params) {
19
+ Object.entries(options.params).forEach(([key, value]) => {
20
+ if (value !== void 0) {
21
+ url.searchParams.set(key, String(value));
22
+ }
23
+ });
24
+ }
25
+ if (!url.searchParams.has("appId")) {
26
+ url.searchParams.set("appId", this.config.appId);
27
+ }
28
+ const headers = {
29
+ "Authorization": `Bearer ${this.config.apiKey}`,
30
+ "Content-Type": "application/json",
31
+ ...options.headers
32
+ };
33
+ const controller = new AbortController();
34
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
35
+ try {
36
+ const response = await fetch(url.toString(), {
37
+ method,
38
+ headers,
39
+ body: options.body ? JSON.stringify(options.body) : void 0,
40
+ signal: controller.signal
41
+ });
42
+ clearTimeout(timeoutId);
43
+ const data = await response.json();
44
+ if (!response.ok) {
45
+ const errorData = data;
46
+ throw new StackBEError(
47
+ errorData.message || "Unknown error",
48
+ errorData.statusCode || response.status,
49
+ errorData.error || "UNKNOWN_ERROR"
50
+ );
51
+ }
52
+ return data;
53
+ } catch (error) {
54
+ clearTimeout(timeoutId);
55
+ if (error instanceof StackBEError) {
56
+ throw error;
57
+ }
58
+ if (error instanceof Error) {
59
+ if (error.name === "AbortError") {
60
+ throw new StackBEError("Request timeout", 408, "TIMEOUT");
61
+ }
62
+ throw new StackBEError(error.message, 0, "NETWORK_ERROR");
63
+ }
64
+ throw new StackBEError("Unknown error", 0, "UNKNOWN_ERROR");
65
+ }
66
+ }
67
+ async get(path, params) {
68
+ return this.request("GET", path, { params });
69
+ }
70
+ async post(path, body, params) {
71
+ return this.request("POST", path, { body, params });
72
+ }
73
+ async patch(path, body) {
74
+ return this.request("PATCH", path, { body });
75
+ }
76
+ async delete(path) {
77
+ return this.request("DELETE", path);
78
+ }
79
+ };
80
+
81
+ // src/usage.ts
82
+ var UsageClient = class {
83
+ constructor(http) {
84
+ this.http = http;
85
+ }
86
+ /**
87
+ * Track a usage event for a customer.
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * // Track 1 API call
92
+ * await stackbe.usage.track('cust_123', 'api_calls');
93
+ *
94
+ * // Track 5 API calls
95
+ * await stackbe.usage.track('cust_123', 'api_calls', { quantity: 5 });
96
+ *
97
+ * // Track with idempotency key
98
+ * await stackbe.usage.track('cust_123', 'api_calls', {
99
+ * quantity: 1,
100
+ * idempotencyKey: 'req_abc123'
101
+ * });
102
+ * ```
103
+ */
104
+ async track(customerId, metric, options = {}) {
105
+ const headers = {};
106
+ if (options.idempotencyKey) {
107
+ headers["Idempotency-Key"] = options.idempotencyKey;
108
+ }
109
+ return this.http.post("/v1/usage/track", {
110
+ customerId,
111
+ metric,
112
+ quantity: options.quantity ?? 1
113
+ });
114
+ }
115
+ /**
116
+ * Check if a customer is within their usage limits for a specific metric.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const { allowed, remaining } = await stackbe.usage.check('cust_123', 'api_calls');
121
+ *
122
+ * if (!allowed) {
123
+ * throw new Error('Usage limit exceeded');
124
+ * }
125
+ * ```
126
+ */
127
+ async check(customerId, metric) {
128
+ return this.http.get(
129
+ `/v1/customers/${customerId}/usage/check`,
130
+ { metric }
131
+ );
132
+ }
133
+ /**
134
+ * Get complete usage summary for a customer across all metrics.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const usage = await stackbe.usage.get('cust_123');
139
+ *
140
+ * for (const metric of usage.metrics) {
141
+ * console.log(`${metric.displayName}: ${metric.currentUsage}/${metric.limit}`);
142
+ * }
143
+ * ```
144
+ */
145
+ async get(customerId, billingPeriod) {
146
+ return this.http.get(
147
+ `/v1/customers/${customerId}/usage`,
148
+ { billingPeriod }
149
+ );
150
+ }
151
+ /**
152
+ * Track usage and check limits in one call.
153
+ * Returns whether the action is allowed after tracking.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const result = await stackbe.usage.trackAndCheck('cust_123', 'api_calls');
158
+ *
159
+ * if (!result.allowed) {
160
+ * // Rollback or handle limit exceeded
161
+ * }
162
+ * ```
163
+ */
164
+ async trackAndCheck(customerId, metric, options = {}) {
165
+ const trackResult = await this.track(customerId, metric, options);
166
+ const allowed = trackResult.limit === null || trackResult.currentUsage <= trackResult.limit;
167
+ return {
168
+ ...trackResult,
169
+ allowed
170
+ };
171
+ }
172
+ };
173
+
174
+ // src/entitlements.ts
175
+ var EntitlementsClient = class {
176
+ constructor(http) {
177
+ this.http = http;
178
+ }
179
+ /**
180
+ * Check if a customer has access to a specific feature.
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * const { hasAccess } = await stackbe.entitlements.check('cust_123', 'premium_export');
185
+ *
186
+ * if (!hasAccess) {
187
+ * return res.status(403).json({ error: 'Upgrade to access this feature' });
188
+ * }
189
+ * ```
190
+ */
191
+ async check(customerId, feature) {
192
+ return this.http.get(
193
+ `/v1/customers/${customerId}/entitlements/check`,
194
+ { feature }
195
+ );
196
+ }
197
+ /**
198
+ * Get all entitlements for a customer based on their current plan.
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * const { entitlements, planName } = await stackbe.entitlements.getAll('cust_123');
203
+ *
204
+ * console.log(`Customer is on ${planName} plan`);
205
+ * console.log('Features:', entitlements);
206
+ * // { premium_export: true, api_access: true, max_projects: 10 }
207
+ * ```
208
+ */
209
+ async getAll(customerId) {
210
+ return this.http.get(
211
+ `/v1/customers/${customerId}/entitlements`
212
+ );
213
+ }
214
+ /**
215
+ * Check multiple features at once.
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const results = await stackbe.entitlements.checkMany('cust_123', [
220
+ * 'premium_export',
221
+ * 'api_access',
222
+ * 'advanced_analytics'
223
+ * ]);
224
+ *
225
+ * // { premium_export: true, api_access: true, advanced_analytics: false }
226
+ * ```
227
+ */
228
+ async checkMany(customerId, features) {
229
+ const results = {};
230
+ const checks = await Promise.all(
231
+ features.map(async (feature) => {
232
+ try {
233
+ const result = await this.check(customerId, feature);
234
+ return { feature, hasAccess: result.hasAccess };
235
+ } catch {
236
+ return { feature, hasAccess: false };
237
+ }
238
+ })
239
+ );
240
+ for (const { feature, hasAccess } of checks) {
241
+ results[feature] = hasAccess;
242
+ }
243
+ return results;
244
+ }
245
+ /**
246
+ * Require a feature - throws if customer doesn't have access.
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * // Throws StackBEError if customer doesn't have access
251
+ * await stackbe.entitlements.require('cust_123', 'premium_export');
252
+ *
253
+ * // If we get here, customer has access
254
+ * performPremiumExport();
255
+ * ```
256
+ */
257
+ async require(customerId, feature) {
258
+ const { hasAccess } = await this.check(customerId, feature);
259
+ if (!hasAccess) {
260
+ const error = new Error(`Customer does not have access to feature: ${feature}`);
261
+ error.name = "EntitlementError";
262
+ throw error;
263
+ }
264
+ }
265
+ };
266
+
267
+ // src/customers.ts
268
+ var CustomersClient = class {
269
+ constructor(http) {
270
+ this.http = http;
271
+ }
272
+ /**
273
+ * Get a customer by ID.
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * const customer = await stackbe.customers.get('cust_123');
278
+ * console.log(customer.email);
279
+ * ```
280
+ */
281
+ async get(customerId) {
282
+ return this.http.get(`/v1/customers/${customerId}`);
283
+ }
284
+ /**
285
+ * Get a customer by email.
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * const customer = await stackbe.customers.getByEmail('user@example.com');
290
+ * ```
291
+ */
292
+ async getByEmail(email) {
293
+ try {
294
+ return await this.http.get("/v1/customers/by-email", { email });
295
+ } catch (error) {
296
+ if (error instanceof Error && "statusCode" in error && error.statusCode === 404) {
297
+ return null;
298
+ }
299
+ throw error;
300
+ }
301
+ }
302
+ /**
303
+ * Create a new customer.
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const customer = await stackbe.customers.create({
308
+ * email: 'user@example.com',
309
+ * name: 'John Doe',
310
+ * metadata: { source: 'website' }
311
+ * });
312
+ * ```
313
+ */
314
+ async create(options) {
315
+ return this.http.post("/v1/customers", options);
316
+ }
317
+ /**
318
+ * Update a customer.
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const customer = await stackbe.customers.update('cust_123', {
323
+ * name: 'Jane Doe',
324
+ * metadata: { plan: 'enterprise' }
325
+ * });
326
+ * ```
327
+ */
328
+ async update(customerId, options) {
329
+ return this.http.patch(`/v1/customers/${customerId}`, options);
330
+ }
331
+ /**
332
+ * Get or create a customer by email.
333
+ * Returns existing customer if found, creates new one if not.
334
+ *
335
+ * @example
336
+ * ```typescript
337
+ * const customer = await stackbe.customers.getOrCreate({
338
+ * email: 'user@example.com',
339
+ * name: 'John Doe'
340
+ * });
341
+ * ```
342
+ */
343
+ async getOrCreate(options) {
344
+ const existing = await this.getByEmail(options.email);
345
+ if (existing) {
346
+ return existing;
347
+ }
348
+ return this.create(options);
349
+ }
350
+ /**
351
+ * Send a magic link to a customer for passwordless authentication.
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * await stackbe.customers.sendMagicLink('user@example.com', {
356
+ * redirectUrl: 'https://myapp.com/dashboard'
357
+ * });
358
+ * ```
359
+ */
360
+ async sendMagicLink(email, options) {
361
+ return this.http.post("/v1/auth/magic-link", {
362
+ email,
363
+ redirectUrl: options?.redirectUrl
364
+ });
365
+ }
366
+ /**
367
+ * Get the current session for a customer token.
368
+ * Use this to validate tokens and get customer data.
369
+ *
370
+ * @example
371
+ * ```typescript
372
+ * const session = await stackbe.customers.getSession(token);
373
+ * console.log(session.customer.email);
374
+ * console.log(session.entitlements);
375
+ * ```
376
+ */
377
+ async getSession(token) {
378
+ return this.http.get("/v1/auth/session", void 0);
379
+ }
380
+ };
381
+
382
+ // src/client.ts
383
+ var DEFAULT_BASE_URL = "https://api.stackbe.io";
384
+ var DEFAULT_TIMEOUT = 3e4;
385
+ var StackBE = class {
386
+ /**
387
+ * Create a new StackBE client.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * import { StackBE } from '@stackbe/sdk';
392
+ *
393
+ * const stackbe = new StackBE({
394
+ * apiKey: process.env.STACKBE_API_KEY!,
395
+ * appId: process.env.STACKBE_APP_ID!,
396
+ * });
397
+ *
398
+ * // Track usage
399
+ * await stackbe.usage.track('customer_123', 'api_calls');
400
+ *
401
+ * // Check entitlements
402
+ * const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium');
403
+ *
404
+ * // Get customer
405
+ * const customer = await stackbe.customers.get('customer_123');
406
+ * ```
407
+ */
408
+ constructor(config) {
409
+ if (!config.apiKey) {
410
+ throw new StackBEError("apiKey is required", 400, "INVALID_CONFIG");
411
+ }
412
+ if (!config.appId) {
413
+ throw new StackBEError("appId is required", 400, "INVALID_CONFIG");
414
+ }
415
+ this.http = new HttpClient({
416
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
417
+ apiKey: config.apiKey,
418
+ appId: config.appId,
419
+ timeout: config.timeout ?? DEFAULT_TIMEOUT
420
+ });
421
+ this.usage = new UsageClient(this.http);
422
+ this.entitlements = new EntitlementsClient(this.http);
423
+ this.customers = new CustomersClient(this.http);
424
+ }
425
+ /**
426
+ * Create a middleware for Express that tracks usage automatically.
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * import express from 'express';
431
+ * import { StackBE } from '@stackbe/sdk';
432
+ *
433
+ * const app = express();
434
+ * const stackbe = new StackBE({ apiKey: '...', appId: '...' });
435
+ *
436
+ * // Track all API calls
437
+ * app.use(stackbe.middleware({
438
+ * getCustomerId: (req) => req.user?.customerId,
439
+ * metric: 'api_calls',
440
+ * }));
441
+ * ```
442
+ */
443
+ middleware(options) {
444
+ return async (req, res, next) => {
445
+ if (options.skip?.(req)) {
446
+ return next();
447
+ }
448
+ const customerId = options.getCustomerId(req);
449
+ if (!customerId) {
450
+ return next();
451
+ }
452
+ try {
453
+ this.usage.track(customerId, options.metric).catch((error) => {
454
+ console.error("[StackBE] Failed to track usage:", error.message);
455
+ });
456
+ } catch (error) {
457
+ console.error("[StackBE] Failed to track usage:", error);
458
+ }
459
+ next();
460
+ };
461
+ }
462
+ /**
463
+ * Create a middleware that requires a feature entitlement.
464
+ *
465
+ * @example
466
+ * ```typescript
467
+ * // Require premium feature
468
+ * app.get('/api/export',
469
+ * stackbe.requireFeature({
470
+ * getCustomerId: (req) => req.user?.customerId,
471
+ * feature: 'premium_export',
472
+ * }),
473
+ * exportHandler
474
+ * );
475
+ * ```
476
+ */
477
+ requireFeature(options) {
478
+ return async (req, res, next) => {
479
+ const customerId = options.getCustomerId(req);
480
+ if (!customerId) {
481
+ if (options.onDenied) {
482
+ return options.onDenied(req, res);
483
+ }
484
+ return res.status(401).json({ error: "Unauthorized" });
485
+ }
486
+ try {
487
+ const { hasAccess } = await this.entitlements.check(customerId, options.feature);
488
+ if (!hasAccess) {
489
+ if (options.onDenied) {
490
+ return options.onDenied(req, res);
491
+ }
492
+ return res.status(403).json({
493
+ error: "Feature not available",
494
+ feature: options.feature,
495
+ upgradeRequired: true
496
+ });
497
+ }
498
+ next();
499
+ } catch (error) {
500
+ console.error("[StackBE] Failed to check entitlement:", error);
501
+ if (options.onDenied) {
502
+ return options.onDenied(req, res);
503
+ }
504
+ return res.status(500).json({ error: "Failed to verify access" });
505
+ }
506
+ };
507
+ }
508
+ /**
509
+ * Create a middleware that enforces usage limits.
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * // Enforce API call limits
514
+ * app.use('/api',
515
+ * stackbe.enforceLimit({
516
+ * getCustomerId: (req) => req.user?.customerId,
517
+ * metric: 'api_calls',
518
+ * })
519
+ * );
520
+ * ```
521
+ */
522
+ enforceLimit(options) {
523
+ return async (req, res, next) => {
524
+ const customerId = options.getCustomerId(req);
525
+ if (!customerId) {
526
+ return next();
527
+ }
528
+ try {
529
+ const { allowed, currentUsage, limit } = await this.usage.check(customerId, options.metric);
530
+ if (!allowed) {
531
+ if (options.onLimitExceeded) {
532
+ return options.onLimitExceeded(req, res, {
533
+ current: currentUsage,
534
+ limit: limit ?? 0
535
+ });
536
+ }
537
+ return res.status(429).json({
538
+ error: "Usage limit exceeded",
539
+ metric: options.metric,
540
+ current: currentUsage,
541
+ limit
542
+ });
543
+ }
544
+ this.usage.track(customerId, options.metric).catch((error) => {
545
+ console.error("[StackBE] Failed to track usage:", error.message);
546
+ });
547
+ next();
548
+ } catch (error) {
549
+ console.error("[StackBE] Failed to check usage limit:", error);
550
+ next();
551
+ }
552
+ };
553
+ }
554
+ };
555
+ export {
556
+ CustomersClient,
557
+ EntitlementsClient,
558
+ StackBE,
559
+ StackBEError,
560
+ UsageClient
561
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@stackbe/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official JavaScript/TypeScript SDK for StackBE - the billing backend for your side project",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "lint": "eslint src/",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "stackbe",
27
+ "billing",
28
+ "subscriptions",
29
+ "saas",
30
+ "usage-based-billing",
31
+ "entitlements",
32
+ "stripe"
33
+ ],
34
+ "author": "StackBE",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Epic-Design-Labs/app-stackbe-sdk.git"
39
+ },
40
+ "homepage": "https://stackbe.io",
41
+ "bugs": {
42
+ "url": "https://github.com/Epic-Design-Labs/app-stackbe-sdk/issues"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.10.0",
46
+ "tsup": "^8.0.1",
47
+ "typescript": "^5.3.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ }