@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/README.md +286 -0
- package/dist/index.d.mts +448 -0
- package/dist/index.d.ts +448 -0
- package/dist/index.js +592 -0
- package/dist/index.mjs +561 -0
- package/package.json +52 -0
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
|
+
});
|