@beinfi/pulse-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,679 @@
1
+ // src/errors.ts
2
+ var PulseError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "PulseError";
6
+ }
7
+ };
8
+ var PulseApiError = class extends PulseError {
9
+ /** HTTP status code (e.g. 400, 404, 500). */
10
+ status;
11
+ /** Machine-readable error code (e.g. `"unauthorized"`, `"not_found"`). */
12
+ errorCode;
13
+ /** Rate limit info from response headers, if available. */
14
+ rateLimit;
15
+ constructor(status, errorCode, message, rateLimit) {
16
+ super(message);
17
+ this.name = "PulseApiError";
18
+ this.status = status;
19
+ this.errorCode = errorCode;
20
+ this.rateLimit = rateLimit;
21
+ }
22
+ };
23
+ var PulseAuthenticationError = class extends PulseApiError {
24
+ constructor(message = "Invalid API key") {
25
+ super(401, "unauthorized", message);
26
+ this.name = "PulseAuthenticationError";
27
+ }
28
+ };
29
+ var PulseRateLimitError = class extends PulseApiError {
30
+ /** Number of seconds to wait before retrying. */
31
+ retryAfter;
32
+ constructor(retryAfter, rateLimit) {
33
+ super(429, "rate_limit_exceeded", "Rate limit exceeded", rateLimit);
34
+ this.name = "PulseRateLimitError";
35
+ this.retryAfter = retryAfter;
36
+ }
37
+ };
38
+
39
+ // src/client.ts
40
+ var DEFAULT_BASE_URL = "https://api.beinfi.com";
41
+ var HttpClient = class {
42
+ apiKey;
43
+ baseUrl;
44
+ constructor(apiKey, baseUrl) {
45
+ if (!apiKey.startsWith("sk_live_")) {
46
+ throw new PulseError(
47
+ 'Invalid API key format. Keys must start with "sk_live_"'
48
+ );
49
+ }
50
+ this.apiKey = apiKey;
51
+ this.baseUrl = (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
52
+ }
53
+ async request(method, path, options) {
54
+ let url = `${this.baseUrl}/api/v1${path}`;
55
+ if (options?.query) {
56
+ const params = new URLSearchParams();
57
+ for (const [key, value] of Object.entries(options.query)) {
58
+ if (value !== void 0) {
59
+ params.set(key, String(value));
60
+ }
61
+ }
62
+ const qs = params.toString();
63
+ if (qs) url += `?${qs}`;
64
+ }
65
+ const headers = {
66
+ Authorization: `Bearer ${this.apiKey}`,
67
+ "Content-Type": "application/json"
68
+ };
69
+ const response = await fetch(url, {
70
+ method,
71
+ headers,
72
+ body: options?.body ? JSON.stringify(options.body) : void 0
73
+ });
74
+ const rateLimit = this.parseRateLimit(response.headers);
75
+ if (!response.ok) {
76
+ this.handleError(response.status, await this.safeJson(response), rateLimit);
77
+ }
78
+ if (response.status === 204) {
79
+ return void 0;
80
+ }
81
+ const json = await response.json();
82
+ if (json && typeof json === "object" && "data" in json) {
83
+ return json.data;
84
+ }
85
+ return json;
86
+ }
87
+ parseRateLimit(headers) {
88
+ const limit = headers.get("X-RateLimit-Limit");
89
+ const remaining = headers.get("X-RateLimit-Remaining");
90
+ const reset = headers.get("X-RateLimit-Reset");
91
+ if (limit && remaining && reset) {
92
+ return {
93
+ limit: Number(limit),
94
+ remaining: Number(remaining),
95
+ reset: Number(reset)
96
+ };
97
+ }
98
+ return void 0;
99
+ }
100
+ async safeJson(response) {
101
+ try {
102
+ return await response.json();
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ handleError(status, body, rateLimit) {
108
+ const errorCode = body?.error ?? "unknown_error";
109
+ const message = body?.message ?? `Request failed with status ${status}`;
110
+ if (status === 401) {
111
+ throw new PulseAuthenticationError(message);
112
+ }
113
+ if (status === 429) {
114
+ const retryAfter = rateLimit?.reset ? Math.max(0, rateLimit.reset - Math.floor(Date.now() / 1e3)) : 60;
115
+ throw new PulseRateLimitError(retryAfter, rateLimit);
116
+ }
117
+ throw new PulseApiError(status, errorCode, message, rateLimit);
118
+ }
119
+ };
120
+
121
+ // src/resources/payment-links.ts
122
+ var PaymentLinksResource = class {
123
+ constructor(client) {
124
+ this.client = client;
125
+ }
126
+ /**
127
+ * Create a new payment link.
128
+ *
129
+ * @param params - Payment link parameters (title, amount, optional currency and description).
130
+ * @returns The created payment link object.
131
+ * @throws {PulseAuthenticationError} If the API key is invalid.
132
+ * @throws {PulseApiError} If validation fails (e.g. invalid currency).
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const link = await pulse.paymentLinks.create({
137
+ * title: 'Web Development',
138
+ * amount: '150.00',
139
+ * currency: 'USD',
140
+ * description: 'Landing page development',
141
+ * })
142
+ * console.log(link.id, link.slug)
143
+ * ```
144
+ */
145
+ async create(params) {
146
+ return this.client.request("POST", "/payment-links", {
147
+ body: params
148
+ });
149
+ }
150
+ /**
151
+ * List payment links for the authenticated user.
152
+ *
153
+ * @param params - Optional pagination parameters (limit, offset).
154
+ * @returns Array of payment link objects.
155
+ * @throws {PulseAuthenticationError} If the API key is invalid.
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * const links = await pulse.paymentLinks.list({ limit: 10, offset: 0 })
160
+ * for (const link of links) {
161
+ * console.log(link.title, link.amount, link.status)
162
+ * }
163
+ * ```
164
+ */
165
+ async list(params) {
166
+ return this.client.request("GET", "/payment-links", {
167
+ query: {
168
+ limit: params?.limit,
169
+ offset: params?.offset
170
+ }
171
+ });
172
+ }
173
+ /**
174
+ * Get a single payment link by ID.
175
+ *
176
+ * @param linkId - The payment link UUID.
177
+ * @returns The payment link object.
178
+ * @throws {PulseAuthenticationError} If the API key is invalid.
179
+ * @throws {PulseApiError} With status 404 if the link doesn't exist.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * const link = await pulse.paymentLinks.get('abc-123-def')
184
+ * console.log(link.title, link.amount)
185
+ * ```
186
+ */
187
+ async get(linkId) {
188
+ return this.client.request("GET", `/payment-links/${linkId}`);
189
+ }
190
+ /**
191
+ * List payment intents (payment attempts) for a specific payment link.
192
+ *
193
+ * @param linkId - The payment link UUID.
194
+ * @returns Array of payment intent objects.
195
+ * @throws {PulseAuthenticationError} If the API key is invalid.
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const intents = await pulse.paymentLinks.listIntents('abc-123-def')
200
+ * const confirmed = intents.filter(i => i.status === 'confirmed')
201
+ * console.log(`${confirmed.length} confirmed payments`)
202
+ * ```
203
+ */
204
+ async listIntents(linkId) {
205
+ return this.client.request(
206
+ "GET",
207
+ `/payment-links/${linkId}/intents`
208
+ );
209
+ }
210
+ };
211
+
212
+ // src/resources/webhooks.ts
213
+ var WebhooksResource = class {
214
+ constructor(client) {
215
+ this.client = client;
216
+ }
217
+ /**
218
+ * Create a new webhook subscription.
219
+ * The response includes a `secret` field (64-char hex) used to verify signatures.
220
+ * **Store this secret securely** — it is only returned once at creation time.
221
+ *
222
+ * @param params - Webhook URL and event types to subscribe to.
223
+ * @returns The created webhook with its signing secret.
224
+ * @throws {PulseAuthenticationError} If the API key is invalid.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * const wh = await pulse.webhooks.create({
229
+ * url: 'https://example.com/webhook',
230
+ * events: ['payment.confirmed'],
231
+ * })
232
+ * // Save wh.secret to your environment/secrets manager
233
+ * console.log('Webhook secret:', wh.secret)
234
+ * ```
235
+ */
236
+ async create(params) {
237
+ return this.client.request("POST", "/webhooks", {
238
+ body: params
239
+ });
240
+ }
241
+ /**
242
+ * List all webhook subscriptions for the authenticated user.
243
+ *
244
+ * @returns Array of webhook objects (without secrets).
245
+ * @throws {PulseAuthenticationError} If the API key is invalid.
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const webhooks = await pulse.webhooks.list()
250
+ * for (const wh of webhooks) {
251
+ * console.log(wh.url, wh.events, wh.isActive)
252
+ * }
253
+ * ```
254
+ */
255
+ async list() {
256
+ return this.client.request("GET", "/webhooks");
257
+ }
258
+ /**
259
+ * Delete a webhook subscription.
260
+ *
261
+ * @param id - The webhook UUID to delete.
262
+ * @throws {PulseAuthenticationError} If the API key is invalid.
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * await pulse.webhooks.delete('webhook-id')
267
+ * ```
268
+ */
269
+ async delete(id) {
270
+ return this.client.request("DELETE", `/webhooks/${id}`);
271
+ }
272
+ };
273
+
274
+ // src/resources/metering.ts
275
+ function generateId() {
276
+ const hex = "0123456789abcdef";
277
+ let id = "";
278
+ for (let i = 0; i < 32; i++) {
279
+ id += hex[Math.floor(Math.random() * 16)];
280
+ if (i === 7 || i === 11 || i === 15 || i === 19) id += "-";
281
+ }
282
+ return id;
283
+ }
284
+ var MeteringResource = class {
285
+ constructor(client) {
286
+ this.client = client;
287
+ }
288
+ /**
289
+ * Track a single usage event.
290
+ *
291
+ * @param params - Event parameters (meterId, customerId, value).
292
+ * @returns The created event object.
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * await pulse.metering.track({
297
+ * meterId: 'tokens',
298
+ * customerId: 'user_123',
299
+ * value: 1500,
300
+ * metadata: { model: 'gpt-4' }
301
+ * })
302
+ * ```
303
+ */
304
+ async track(params) {
305
+ return this.client.request(
306
+ "POST",
307
+ "/metering/events",
308
+ {
309
+ body: {
310
+ eventId: params.eventId || generateId(),
311
+ meterId: params.meterId,
312
+ customerId: params.customerId,
313
+ value: typeof params.value === "number" ? String(params.value) : params.value,
314
+ timestamp: params.timestamp?.toISOString(),
315
+ metadata: params.metadata
316
+ }
317
+ }
318
+ );
319
+ }
320
+ /**
321
+ * Track multiple usage events in a single request.
322
+ *
323
+ * @param events - Array of event parameters.
324
+ * @returns Batch result with accepted/failed counts.
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * await pulse.metering.trackBatch([
329
+ * { meterId: 'tokens', customerId: 'user_1', value: 500 },
330
+ * { meterId: 'tokens', customerId: 'user_2', value: 1200 },
331
+ * ])
332
+ * ```
333
+ */
334
+ async trackBatch(events) {
335
+ return this.client.request(
336
+ "POST",
337
+ "/metering/events/batch",
338
+ {
339
+ body: {
340
+ events: events.map((e) => ({
341
+ eventId: e.eventId || generateId(),
342
+ meterId: e.meterId,
343
+ customerId: e.customerId,
344
+ value: typeof e.value === "number" ? String(e.value) : e.value,
345
+ timestamp: e.timestamp?.toISOString(),
346
+ metadata: e.metadata
347
+ }))
348
+ }
349
+ }
350
+ );
351
+ }
352
+ /**
353
+ * Query aggregated usage data.
354
+ *
355
+ * @param query - Optional filters (customerId, date range).
356
+ * @returns Aggregated usage by meter.
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * const usage = await pulse.metering.getUsage({ customerId: 'user_123' })
361
+ * for (const item of usage.data) {
362
+ * console.log(item.meterName, item.totalValue, item.totalAmount)
363
+ * }
364
+ * ```
365
+ */
366
+ async getUsage(query) {
367
+ return this.client.request("GET", "/metering/usage", {
368
+ query: {
369
+ customerId: query?.customerId,
370
+ startDate: query?.startDate,
371
+ endDate: query?.endDate
372
+ }
373
+ });
374
+ }
375
+ /**
376
+ * List all products.
377
+ *
378
+ * @returns Array of product objects with meters.
379
+ */
380
+ async listProducts() {
381
+ return this.client.request("GET", "/metering/products");
382
+ }
383
+ /**
384
+ * Create a new product.
385
+ *
386
+ * @param data - Product data (name, optional description).
387
+ * @returns The created product object.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * const product = await pulse.metering.createProduct({
392
+ * name: 'AI Agent',
393
+ * description: 'Usage-based AI agent billing',
394
+ * })
395
+ * ```
396
+ */
397
+ async createProduct(data) {
398
+ return this.client.request(
399
+ "POST",
400
+ "/metering/products",
401
+ { body: data }
402
+ );
403
+ }
404
+ /**
405
+ * Create a new meter on a product.
406
+ *
407
+ * @param productId - The product UUID.
408
+ * @param data - Meter data (name, displayName, unit, unitPrice).
409
+ * @returns The created meter object.
410
+ *
411
+ * @example
412
+ * ```typescript
413
+ * const meter = await pulse.metering.createMeter('product-id', {
414
+ * name: 'tokens',
415
+ * displayName: 'AI Tokens',
416
+ * unit: 'token',
417
+ * unitPrice: '0.0001',
418
+ * })
419
+ * ```
420
+ */
421
+ async createMeter(productId, data) {
422
+ return this.client.request(
423
+ "POST",
424
+ `/metering/products/${productId}/meters`,
425
+ { body: data }
426
+ );
427
+ }
428
+ /**
429
+ * Create a customer for a product.
430
+ *
431
+ * @param productId - The product UUID.
432
+ * @param data - Customer data (externalId, name, email, metadata).
433
+ * @returns The created customer object.
434
+ *
435
+ * @example
436
+ * ```typescript
437
+ * const customer = await pulse.metering.createCustomer('product-id', {
438
+ * externalId: 'user_123',
439
+ * name: 'John Doe',
440
+ * email: 'john@example.com',
441
+ * })
442
+ * ```
443
+ */
444
+ async createCustomer(productId, data) {
445
+ return this.client.request(
446
+ "POST",
447
+ `/metering/products/${productId}/customers`,
448
+ { body: data }
449
+ );
450
+ }
451
+ /**
452
+ * Get customer usage for a specific product.
453
+ *
454
+ * @param productId - The product UUID.
455
+ * @param customerId - The customer external ID.
456
+ * @param query - Optional date range filters.
457
+ * @returns Aggregated usage by meter for the customer.
458
+ */
459
+ async getCustomerUsage(productId, customerId, query) {
460
+ return this.client.request(
461
+ "GET",
462
+ `/metering/products/${productId}/customers/${customerId}/usage`,
463
+ {
464
+ query: {
465
+ startDate: query?.startDate,
466
+ endDate: query?.endDate
467
+ }
468
+ }
469
+ );
470
+ }
471
+ /**
472
+ * Create a session for tracking multiple events for a customer.
473
+ * Events are accumulated and sent as a batch when `.end()` is called.
474
+ *
475
+ * @param customerId - The customer external ID.
476
+ * @returns A session instance.
477
+ *
478
+ * @example
479
+ * ```typescript
480
+ * const session = pulse.metering.session('user_123')
481
+ * session.track('tokens', 500)
482
+ * session.track('tokens', 300)
483
+ * session.track('requests', 1)
484
+ * await session.end() // sends batch: tokens=800, requests=1
485
+ * ```
486
+ */
487
+ session(customerId) {
488
+ return new MeteringSession(this, customerId);
489
+ }
490
+ };
491
+ var MeteringSession = class {
492
+ constructor(metering, customerId) {
493
+ this.metering = metering;
494
+ this.customerId = customerId;
495
+ }
496
+ events = [];
497
+ /**
498
+ * Queue a tracking event in this session.
499
+ */
500
+ track(meterId, value, metadata) {
501
+ this.events.push({
502
+ meterId,
503
+ customerId: this.customerId,
504
+ value,
505
+ metadata
506
+ });
507
+ return this;
508
+ }
509
+ /**
510
+ * Send all accumulated events as a batch and clear the session.
511
+ */
512
+ async end() {
513
+ if (this.events.length === 0) {
514
+ return { accepted: 0, failed: 0, results: [] };
515
+ }
516
+ const result = await this.metering.trackBatch(this.events);
517
+ this.events = [];
518
+ return result;
519
+ }
520
+ };
521
+
522
+ // src/webhooks/verify.ts
523
+ import { createHmac, timingSafeEqual } from "crypto";
524
+ function verifyWebhookSignature(rawBody, signatureHeader, secret) {
525
+ const expectedSig = createHmac("sha256", secret).update(rawBody).digest("hex");
526
+ const receivedSig = signatureHeader.startsWith("sha256=") ? signatureHeader.slice(7) : signatureHeader;
527
+ if (expectedSig.length !== receivedSig.length) {
528
+ return false;
529
+ }
530
+ return timingSafeEqual(
531
+ Buffer.from(expectedSig, "hex"),
532
+ Buffer.from(receivedSig, "hex")
533
+ );
534
+ }
535
+
536
+ // src/checkout/checkout.ts
537
+ var DEFAULT_BASE_URL2 = "https://pulse.beinfi.com";
538
+ function mountCheckout(selector, options) {
539
+ const container = typeof selector === "string" ? document.querySelector(selector) : selector;
540
+ if (!container) {
541
+ throw new Error(`[Pulse Checkout] Container not found: ${selector}`);
542
+ }
543
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
544
+ const allowedOrigin = new URL(baseUrl).origin;
545
+ const handlers = {};
546
+ function emit(event, ...args) {
547
+ for (const handler of handlers[event] ?? []) {
548
+ handler(...args);
549
+ }
550
+ }
551
+ const iframe = document.createElement("iframe");
552
+ iframe.src = `${baseUrl}/embed/pay/${options.linkId}`;
553
+ iframe.style.width = "100%";
554
+ iframe.style.border = "none";
555
+ iframe.style.colorScheme = "normal";
556
+ iframe.setAttribute("allowtransparency", "true");
557
+ iframe.allow = "clipboard-write";
558
+ function handleMessage(event) {
559
+ if (event.origin !== allowedOrigin) return;
560
+ const { type, ...data } = event.data ?? {};
561
+ switch (type) {
562
+ case "pulse:ready":
563
+ if (options.theme) {
564
+ iframe.contentWindow?.postMessage(
565
+ { type: "pulse:config", theme: options.theme },
566
+ allowedOrigin
567
+ );
568
+ }
569
+ options.onReady?.();
570
+ emit("ready");
571
+ break;
572
+ case "pulse:resize":
573
+ if (typeof data.height === "number") {
574
+ iframe.style.height = `${data.height}px`;
575
+ }
576
+ break;
577
+ case "pulse:success": {
578
+ const payment = data.payment;
579
+ options.onSuccess?.(payment);
580
+ emit("success", payment);
581
+ break;
582
+ }
583
+ case "pulse:error": {
584
+ const error = data.error;
585
+ options.onError?.(error);
586
+ emit("error", error);
587
+ break;
588
+ }
589
+ }
590
+ }
591
+ window.addEventListener("message", handleMessage);
592
+ container.appendChild(iframe);
593
+ return {
594
+ unmount() {
595
+ window.removeEventListener("message", handleMessage);
596
+ iframe.remove();
597
+ options.onClose?.();
598
+ emit("close");
599
+ },
600
+ on(event, handler) {
601
+ if (!handlers[event]) handlers[event] = [];
602
+ handlers[event].push(handler);
603
+ }
604
+ };
605
+ }
606
+
607
+ // src/index.ts
608
+ var Pulse = class {
609
+ /** Resource for creating, listing, and fetching payment links. */
610
+ paymentLinks;
611
+ /** Resource for creating, listing, and deleting webhook subscriptions. */
612
+ webhooks;
613
+ /** Resource for usage-based metering, tracking events, and querying usage. */
614
+ metering;
615
+ client;
616
+ /**
617
+ * Static utilities for verifying webhook signatures.
618
+ * Does not require a Pulse instance — useful in webhook handler endpoints.
619
+ *
620
+ * @example
621
+ * ```typescript
622
+ * const isValid = Pulse.webhooks.verifySignature(
623
+ * rawBody,
624
+ * req.headers['x-pulse-signature'],
625
+ * process.env.PULSE_WEBHOOK_SECRET
626
+ * )
627
+ * ```
628
+ */
629
+ static webhooks = {
630
+ verifySignature: verifyWebhookSignature
631
+ };
632
+ /**
633
+ * Static utilities for mounting the checkout widget.
634
+ * Browser-only — embeds an iframe-based checkout for a payment link.
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * const instance = Pulse.checkout.mount('#checkout', {
639
+ * linkId: 'abc-123',
640
+ * onSuccess: (payment) => console.log('Paid!', payment),
641
+ * })
642
+ * ```
643
+ */
644
+ static checkout = {
645
+ mount: mountCheckout
646
+ };
647
+ /**
648
+ * Create a new Pulse SDK client.
649
+ *
650
+ * @param config - Either an API key string (`"sk_live_..."`) or a {@link PulseConfig} object.
651
+ * @throws {PulseError} If the API key doesn't start with `"sk_live_"`.
652
+ *
653
+ * @example
654
+ * ```typescript
655
+ * // String shorthand
656
+ * const pulse = new Pulse('sk_live_...')
657
+ *
658
+ * // Config object
659
+ * const pulse = new Pulse({ apiKey: 'sk_live_...', baseUrl: 'https://api.beinfi.com' })
660
+ * ```
661
+ */
662
+ constructor(config) {
663
+ const apiKey = typeof config === "string" ? config : config.apiKey;
664
+ const baseUrl = typeof config === "string" ? void 0 : config.baseUrl;
665
+ this.client = new HttpClient(apiKey, baseUrl);
666
+ this.paymentLinks = new PaymentLinksResource(this.client);
667
+ this.webhooks = new WebhooksResource(this.client);
668
+ this.metering = new MeteringResource(this.client);
669
+ }
670
+ };
671
+ export {
672
+ Pulse,
673
+ PulseApiError,
674
+ PulseAuthenticationError,
675
+ PulseError,
676
+ PulseRateLimitError,
677
+ mountCheckout,
678
+ verifyWebhookSignature
679
+ };