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