@raba7ni/raba7ni 1.0.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.
@@ -0,0 +1,704 @@
1
+ import axios from 'axios';
2
+ import { createHmac, timingSafeEqual } from 'crypto';
3
+
4
+ /**
5
+ * SDK initialization error
6
+ */
7
+ class Raba7niError extends Error {
8
+ constructor(message, code, statusCode, response) {
9
+ super(message);
10
+ this.code = code;
11
+ this.statusCode = statusCode;
12
+ this.response = response;
13
+ this.name = 'Raba7niError';
14
+ }
15
+ }
16
+ /**
17
+ * Authentication error
18
+ */
19
+ class AuthenticationError extends Raba7niError {
20
+ constructor(message = 'Authentication failed') {
21
+ super(message, 'AUTHENTICATION_ERROR', 401);
22
+ this.name = 'AuthenticationError';
23
+ }
24
+ }
25
+ /**
26
+ * Rate limit error
27
+ */
28
+ class RateLimitError extends Raba7niError {
29
+ constructor(message = 'Rate limit exceeded', retryAfter) {
30
+ super(message, 'RATE_LIMIT_ERROR', 429);
31
+ this.retryAfter = retryAfter;
32
+ this.name = 'RateLimitError';
33
+ }
34
+ }
35
+ /**
36
+ * Validation error
37
+ */
38
+ class ValidationError extends Raba7niError {
39
+ constructor(message, validationErrors) {
40
+ super(message, 'VALIDATION_ERROR', 422);
41
+ this.validationErrors = validationErrors;
42
+ this.name = 'ValidationError';
43
+ }
44
+ }
45
+ /**
46
+ * Network error
47
+ */
48
+ class NetworkError extends Raba7niError {
49
+ constructor(message, originalError) {
50
+ super(message, 'NETWORK_ERROR');
51
+ this.originalError = originalError;
52
+ this.name = 'NetworkError';
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Core API client for Raba7ni Developer API
58
+ */
59
+ class Raba7niClient {
60
+ constructor(config) {
61
+ this.rateLimitInfo = {
62
+ hourlyLimit: 0,
63
+ hourlyRemaining: 0,
64
+ dailyLimit: 0,
65
+ dailyRemaining: 0
66
+ };
67
+ // Set default values
68
+ this.config = {
69
+ baseUrl: config.baseUrl || 'https://api.raba7ni.com',
70
+ locale: config.locale || 'en',
71
+ timeout: config.timeout || 30000,
72
+ maxRetries: config.maxRetries || 3,
73
+ retryDelay: config.retryDelay || 1000,
74
+ ...config
75
+ };
76
+ // Validate required fields
77
+ if (!this.config.appId || !this.config.appId.startsWith('app_')) {
78
+ throw new Error('Invalid appId: must start with "app_"');
79
+ }
80
+ if (!this.config.apiKey || !this.config.apiKey.startsWith('dev_')) {
81
+ throw new Error('Invalid apiKey: must start with "dev_"');
82
+ }
83
+ // Create axios instance
84
+ this.axiosInstance = axios.create({
85
+ baseURL: this.config.baseUrl,
86
+ timeout: this.config.timeout,
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'Accept': 'application/json',
90
+ 'X-App-Id': this.config.appId,
91
+ 'X-Api-Key': this.config.apiKey
92
+ }
93
+ });
94
+ // Add request interceptor for logging and debugging
95
+ this.axiosInstance.interceptors.request.use((config) => {
96
+ // Add locale to URL if not already present
97
+ if (!config.url?.includes('/api/')) {
98
+ config.url = `/api/${this.config.locale}/v1/developer${config.url || ''}`;
99
+ }
100
+ return config;
101
+ }, (error) => {
102
+ return Promise.reject(error);
103
+ });
104
+ // Add response interceptor for error handling and rate limit tracking
105
+ this.axiosInstance.interceptors.response.use((response) => {
106
+ this.extractRateLimitInfo(response);
107
+ return response;
108
+ }, (error) => {
109
+ if (error.response) {
110
+ this.extractRateLimitInfo(error.response);
111
+ this.handleApiError(error);
112
+ }
113
+ else if (error.request) {
114
+ throw new NetworkError('Network error: No response received', error);
115
+ }
116
+ else {
117
+ throw new NetworkError('Network error: Request setup failed', error);
118
+ }
119
+ return Promise.reject(error);
120
+ });
121
+ }
122
+ /**
123
+ * Extract rate limit information from response headers
124
+ */
125
+ extractRateLimitInfo(response) {
126
+ const headers = response.headers;
127
+ this.rateLimitInfo = {
128
+ hourlyLimit: parseInt(headers['x-ratelimit-hourly-limit']) || 0,
129
+ hourlyRemaining: parseInt(headers['x-ratelimit-hourly-remaining']) || 0,
130
+ dailyLimit: parseInt(headers['x-ratelimit-daily-limit']) || 0,
131
+ dailyRemaining: parseInt(headers['x-ratelimit-daily-remaining']) || 0,
132
+ resetTime: headers['x-ratelimit-reset']
133
+ };
134
+ }
135
+ /**
136
+ * Handle API errors and convert to appropriate error classes
137
+ */
138
+ handleApiError(error) {
139
+ const status = error.response?.status;
140
+ const data = error.response?.data;
141
+ switch (status) {
142
+ case 401:
143
+ throw new AuthenticationError(data?.message || 'Authentication failed');
144
+ case 403:
145
+ throw new Raba7niError(data?.message || 'Access forbidden', 'FORBIDDEN', 403, data);
146
+ case 404:
147
+ throw new Raba7niError(data?.message || 'Resource not found', 'NOT_FOUND', 404, data);
148
+ case 422:
149
+ throw new ValidationError(data?.message || 'Validation failed', data?.errors);
150
+ case 429:
151
+ const retryAfter = error.response?.headers['retry-after'];
152
+ throw new RateLimitError(data?.message || 'Rate limit exceeded', retryAfter ? parseInt(retryAfter) : undefined);
153
+ case 500:
154
+ case 502:
155
+ case 503:
156
+ case 504:
157
+ throw new Raba7niError(data?.message || 'Server error', 'SERVER_ERROR', status, data);
158
+ default:
159
+ throw new Raba7niError(data?.message || error.message || 'Unknown error', 'UNKNOWN_ERROR', status, data);
160
+ }
161
+ }
162
+ /**
163
+ * Make HTTP request with retry logic
164
+ */
165
+ async request(config) {
166
+ let lastError;
167
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
168
+ try {
169
+ const response = await this.axiosInstance.request(config);
170
+ return response.data;
171
+ }
172
+ catch (error) {
173
+ lastError = error;
174
+ // Don't retry on authentication or validation errors
175
+ if (error instanceof AuthenticationError ||
176
+ error instanceof ValidationError ||
177
+ error instanceof Raba7niError &&
178
+ (error.statusCode === 403 || error.statusCode === 404)) {
179
+ throw error;
180
+ }
181
+ // Retry on rate limit errors with exponential backoff
182
+ if (error instanceof RateLimitError) {
183
+ if (attempt === this.config.maxRetries) {
184
+ throw error;
185
+ }
186
+ const delay = error.retryAfter
187
+ ? error.retryAfter * 1000
188
+ : this.config.retryDelay * Math.pow(2, attempt);
189
+ await this.sleep(delay);
190
+ continue;
191
+ }
192
+ // Retry on network errors
193
+ if (error instanceof NetworkError && attempt < this.config.maxRetries) {
194
+ const delay = this.config.retryDelay * Math.pow(2, attempt);
195
+ await this.sleep(delay);
196
+ continue;
197
+ }
198
+ throw error;
199
+ }
200
+ }
201
+ throw lastError;
202
+ }
203
+ /**
204
+ * Sleep helper for retry delays
205
+ */
206
+ sleep(ms) {
207
+ return new Promise(resolve => {
208
+ if (typeof setTimeout !== 'undefined') {
209
+ setTimeout(resolve, ms);
210
+ }
211
+ else {
212
+ resolve();
213
+ }
214
+ });
215
+ }
216
+ /**
217
+ * Get current rate limit information
218
+ */
219
+ getRateLimitInfo() {
220
+ return { ...this.rateLimitInfo };
221
+ }
222
+ /**
223
+ * Make GET request
224
+ */
225
+ async get(url, config) {
226
+ return this.request({ ...config, method: 'GET', url });
227
+ }
228
+ /**
229
+ * Make POST request
230
+ */
231
+ async post(url, data, config) {
232
+ return this.request({ ...config, method: 'POST', url, data });
233
+ }
234
+ /**
235
+ * Make PUT request
236
+ */
237
+ async put(url, data, config) {
238
+ return this.request({ ...config, method: 'PUT', url, data });
239
+ }
240
+ /**
241
+ * Make PATCH request
242
+ */
243
+ async patch(url, data, config) {
244
+ return this.request({ ...config, method: 'PATCH', url, data });
245
+ }
246
+ /**
247
+ * Make DELETE request
248
+ */
249
+ async delete(url, config) {
250
+ return this.request({ ...config, method: 'DELETE', url });
251
+ }
252
+ /**
253
+ * Update configuration
254
+ */
255
+ updateConfig(config) {
256
+ this.config = { ...this.config, ...config };
257
+ if (config.appId) {
258
+ this.axiosInstance.defaults.headers['X-App-Id'] = config.appId;
259
+ }
260
+ if (config.apiKey) {
261
+ this.axiosInstance.defaults.headers['X-Api-Key'] = config.apiKey;
262
+ }
263
+ if (config.baseUrl) {
264
+ this.axiosInstance.defaults.baseURL = config.baseUrl;
265
+ }
266
+ if (config.timeout) {
267
+ this.axiosInstance.defaults.timeout = config.timeout;
268
+ }
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Utility functions for the Raba7ni SDK
274
+ */
275
+ /**
276
+ * Normalize phone number to E.164 format
277
+ */
278
+ function normalizePhoneNumber(phoneNumber) {
279
+ if (!phoneNumber) {
280
+ throw new Error('Phone number is required');
281
+ }
282
+ // Remove all non-numeric characters except +
283
+ let normalized = phoneNumber.replace(/[^\d+]/g, '');
284
+ // If starts with +, keep it, otherwise assume international format
285
+ if (!normalized.startsWith('+')) {
286
+ // Remove leading zeros
287
+ normalized = normalized.replace(/^0+/, '');
288
+ // If no country code, you might want to add a default one
289
+ // For now, we'll assume the input is already in international format
290
+ // In a real implementation, you might want to use a library like
291
+ // google-libphonenumber to handle country codes properly
292
+ normalized = '+' + normalized;
293
+ }
294
+ // Basic validation
295
+ if (normalized.length < 8 || normalized.length > 15) {
296
+ throw new Error('Invalid phone number format');
297
+ }
298
+ return normalized;
299
+ }
300
+ /**
301
+ * Validate email format
302
+ */
303
+ function validateEmail(email) {
304
+ if (!email) {
305
+ return false;
306
+ }
307
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
308
+ return emailRegex.test(email);
309
+ }
310
+ /**
311
+ * Validate card ID
312
+ */
313
+ function validateCardId(cardId) {
314
+ const id = typeof cardId === 'string' ? parseInt(cardId, 10) : cardId;
315
+ if (isNaN(id) || id <= 0) {
316
+ throw new Error('Card ID must be a positive integer');
317
+ }
318
+ return id;
319
+ }
320
+ /**
321
+ * Validate monetary amount
322
+ */
323
+ function validateAmount(amount) {
324
+ const num = typeof amount === 'string' ? parseFloat(amount) : amount;
325
+ if (isNaN(num) || num < 0) {
326
+ throw new Error('Amount must be a non-negative number');
327
+ }
328
+ // Round to 2 decimal places
329
+ return Math.round(num * 100) / 100;
330
+ }
331
+ /**
332
+ * Sanitize string input
333
+ */
334
+ function sanitizeString(input, maxLength = 255) {
335
+ if (!input) {
336
+ return '';
337
+ }
338
+ // Remove HTML tags and extra whitespace
339
+ let sanitized = input.replace(/<[^>]*>/g, '').trim();
340
+ // Truncate if too long
341
+ if (sanitized.length > maxLength) {
342
+ sanitized = sanitized.substring(0, maxLength);
343
+ }
344
+ return sanitized;
345
+ }
346
+ /**
347
+ * Validate name field
348
+ */
349
+ function validateName(name) {
350
+ if (!name || name.trim().length === 0) {
351
+ throw new Error('Name is required');
352
+ }
353
+ const sanitized = sanitizeString(name.trim(), 100);
354
+ if (sanitized.length < 2) {
355
+ throw new Error('Name must be at least 2 characters long');
356
+ }
357
+ return sanitized;
358
+ }
359
+ /**
360
+ * Deep clone object
361
+ */
362
+ function deepClone(obj) {
363
+ if (obj === null || typeof obj !== 'object') {
364
+ return obj;
365
+ }
366
+ if (obj instanceof Date) {
367
+ return new Date(obj.getTime());
368
+ }
369
+ if (Array.isArray(obj)) {
370
+ return obj.map(item => deepClone(item));
371
+ }
372
+ const cloned = {};
373
+ for (const key in obj) {
374
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
375
+ cloned[key] = deepClone(obj[key]);
376
+ }
377
+ }
378
+ return cloned;
379
+ }
380
+ /**
381
+ * Retry delay calculation with exponential backoff
382
+ */
383
+ function calculateRetryDelay(attempt, baseDelay = 1000) {
384
+ return Math.min(baseDelay * Math.pow(2, attempt), 30000); // Max 30 seconds
385
+ }
386
+ /**
387
+ * Generate random string for request tracking
388
+ */
389
+ function generateRequestId() {
390
+ return Math.random().toString(36).substring(2, 15) +
391
+ Math.random().toString(36).substring(2, 15);
392
+ }
393
+ /**
394
+ * Check if a value is a valid ISO date string
395
+ */
396
+ function isValidISODate(dateString) {
397
+ if (!dateString) {
398
+ return false;
399
+ }
400
+ const date = new Date(dateString);
401
+ return !isNaN(date.getTime()) && dateString.includes('T');
402
+ }
403
+ /**
404
+ * Format date to ISO string
405
+ */
406
+ function formatDate(date) {
407
+ if (typeof date === 'string') {
408
+ return date;
409
+ }
410
+ return date.toISOString();
411
+ }
412
+
413
+ /**
414
+ * Main Raba7ni SDK class
415
+ */
416
+ class Raba7niSDK {
417
+ constructor(config) {
418
+ this.client = new Raba7niClient(config);
419
+ }
420
+ /**
421
+ * Validate if a phone number is a member of a specific card
422
+ */
423
+ async validateMember(cardId, phoneNumber, options = {}) {
424
+ const validatedCardId = validateCardId(cardId);
425
+ const normalizedPhone = normalizePhoneNumber(phoneNumber);
426
+ const request = {
427
+ card_id: validatedCardId,
428
+ phone_number: normalizedPhone,
429
+ include_member_data: options.include_member_data || false
430
+ };
431
+ const response = await this.client.post('validate-member', request);
432
+ return response.data;
433
+ }
434
+ /**
435
+ * Get detailed information about a member for a specific card
436
+ */
437
+ async getMemberDetails(cardId, phoneNumber) {
438
+ const validatedCardId = validateCardId(cardId);
439
+ const normalizedPhone = normalizePhoneNumber(phoneNumber);
440
+ const request = {
441
+ card_id: validatedCardId,
442
+ phone_number: normalizedPhone
443
+ };
444
+ const response = await this.client.post('member-details', request);
445
+ return response.data;
446
+ }
447
+ /**
448
+ * Find an existing member by email/phone, or create a new account.
449
+ * Optionally create an online order and/or award points.
450
+ */
451
+ async findOrCreateMember(request) {
452
+ const validatedRequest = {
453
+ card_id: validateCardId(request.card_id),
454
+ name: validateName(request.name),
455
+ email: sanitizeString(request.email.toLowerCase()),
456
+ phone_number: normalizePhoneNumber(request.phone_number),
457
+ create_order: request.create_order,
458
+ order: request.order ? {
459
+ award_points: request.order.award_points || false,
460
+ total_amount: validateAmount(request.order.total_amount),
461
+ delivery_cost: request.order.delivery_cost
462
+ ? validateAmount(request.order.delivery_cost)
463
+ : undefined,
464
+ items: request.order.items?.map(item => ({
465
+ name: sanitizeString(item.name, 100),
466
+ amount: validateAmount(item.amount),
467
+ metadata: item.metadata || {}
468
+ }))
469
+ } : undefined
470
+ };
471
+ // Validate email format if provided
472
+ if (validatedRequest.email && !validateEmail(validatedRequest.email)) {
473
+ throw new Error('Invalid email format');
474
+ }
475
+ const response = await this.client.post('find-or-create-member', validatedRequest);
476
+ return response.data;
477
+ }
478
+ /**
479
+ * Get card information
480
+ */
481
+ async getCardInfo(cardId) {
482
+ const validatedCardId = validateCardId(cardId);
483
+ const response = await this.client.get(`cards/${validatedCardId}`);
484
+ return response.data;
485
+ }
486
+ /**
487
+ * Get available API scopes
488
+ */
489
+ async getScopes() {
490
+ const response = await this.client.get('scopes');
491
+ return response.data;
492
+ }
493
+ /**
494
+ * Get available webhook events
495
+ */
496
+ async getWebhookEvents() {
497
+ const response = await this.client.get('webhook-events');
498
+ return response.data;
499
+ }
500
+ /**
501
+ * Get current rate limit information
502
+ */
503
+ getRateLimitInfo() {
504
+ return this.client.getRateLimitInfo();
505
+ }
506
+ /**
507
+ * Update SDK configuration
508
+ */
509
+ updateConfig(config) {
510
+ this.client.updateConfig(config);
511
+ }
512
+ /**
513
+ * Helper method to make custom API requests
514
+ */
515
+ async request(method, endpoint, data, options) {
516
+ let response;
517
+ switch (method.toUpperCase()) {
518
+ case 'GET':
519
+ response = await this.client.get(endpoint, options);
520
+ break;
521
+ case 'POST':
522
+ response = await this.client.post(endpoint, data, options);
523
+ break;
524
+ case 'PUT':
525
+ response = await this.client.put(endpoint, data, options);
526
+ break;
527
+ case 'PATCH':
528
+ response = await this.client.patch(endpoint, data, options);
529
+ break;
530
+ case 'DELETE':
531
+ response = await this.client.delete(endpoint, options);
532
+ break;
533
+ default:
534
+ throw new Error(`Unsupported HTTP method: ${method}`);
535
+ }
536
+ return response.data;
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Webhook signature verification utilities
542
+ */
543
+ /**
544
+ * Verify webhook signature
545
+ */
546
+ function verifyWebhookSignature(payload, signature, secret) {
547
+ if (!payload || !signature || !secret) {
548
+ return false;
549
+ }
550
+ try {
551
+ const expectedSignature = createHmac('sha256', secret)
552
+ .update(payload, 'utf8')
553
+ .digest('hex');
554
+ // Use constant-time comparison to prevent timing attacks
555
+ return timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
556
+ }
557
+ catch (error) {
558
+ return false;
559
+ }
560
+ }
561
+ /**
562
+ * Parse webhook payload with type safety
563
+ */
564
+ function parseWebhookPayload(payload) {
565
+ try {
566
+ const parsed = JSON.parse(payload);
567
+ // Validate required fields
568
+ if (!parsed.event || !parsed.timestamp || !parsed.data) {
569
+ throw new Error('Invalid webhook payload: missing required fields');
570
+ }
571
+ // Validate timestamp format
572
+ const timestamp = new Date(parsed.timestamp);
573
+ if (isNaN(timestamp.getTime())) {
574
+ throw new Error('Invalid webhook payload: invalid timestamp format');
575
+ }
576
+ return parsed;
577
+ }
578
+ catch (error) {
579
+ throw new Error(`Failed to parse webhook payload: ${error}`);
580
+ }
581
+ }
582
+ /**
583
+ * Type guard for member joined webhook
584
+ */
585
+ function isMemberJoinedWebhook(payload) {
586
+ return payload.event === 'member_joined';
587
+ }
588
+ /**
589
+ * Type guard for member left webhook
590
+ */
591
+ function isMemberLeftWebhook(payload) {
592
+ return payload.event === 'member_left';
593
+ }
594
+ /**
595
+ * Type guard for points earned webhook
596
+ */
597
+ function isPointsEarnedWebhook(payload) {
598
+ return payload.event === 'points_earned';
599
+ }
600
+ /**
601
+ * Type guard for points redeemed webhook
602
+ */
603
+ function isPointsRedeemedWebhook(payload) {
604
+ return payload.event === 'points_redeemed';
605
+ }
606
+ /**
607
+ * Type guard for transaction created webhook
608
+ */
609
+ function isTransactionCreatedWebhook(payload) {
610
+ return payload.event === 'transaction_created';
611
+ }
612
+ /**
613
+ * Webhook handler class
614
+ */
615
+ class WebhookHandler {
616
+ constructor(secret) {
617
+ if (!secret) {
618
+ throw new Error('Webhook secret is required');
619
+ }
620
+ this.secret = secret;
621
+ }
622
+ /**
623
+ * Verify and parse incoming webhook
624
+ */
625
+ async verifyAndParse(payload, signature) {
626
+ if (!this.verifyWebhookSignature(payload, signature)) {
627
+ throw new Error('Invalid webhook signature');
628
+ }
629
+ return this.parseWebhookPayload(payload);
630
+ }
631
+ /**
632
+ * Verify webhook signature
633
+ */
634
+ verifyWebhookSignature(payload, signature) {
635
+ return verifyWebhookSignature(payload, signature, this.secret);
636
+ }
637
+ /**
638
+ * Parse webhook payload
639
+ */
640
+ parseWebhookPayload(payload) {
641
+ return parseWebhookPayload(payload);
642
+ }
643
+ /**
644
+ * Handle webhook with event-specific callbacks
645
+ */
646
+ async handleWebhook(payload, signature, handlers) {
647
+ const webhook = await this.verifyAndParse(payload, signature);
648
+ try {
649
+ switch (webhook.event) {
650
+ case 'member_joined':
651
+ if (handlers.onMemberJoined && isMemberJoinedWebhook(webhook)) {
652
+ await handlers.onMemberJoined(webhook.data);
653
+ }
654
+ break;
655
+ case 'member_left':
656
+ if (handlers.onMemberLeft && isMemberLeftWebhook(webhook)) {
657
+ await handlers.onMemberLeft(webhook.data);
658
+ }
659
+ break;
660
+ case 'points_earned':
661
+ if (handlers.onPointsEarned && isPointsEarnedWebhook(webhook)) {
662
+ await handlers.onPointsEarned(webhook.data);
663
+ }
664
+ break;
665
+ case 'points_redeemed':
666
+ if (handlers.onPointsRedeemed && isPointsRedeemedWebhook(webhook)) {
667
+ await handlers.onPointsRedeemed(webhook.data);
668
+ }
669
+ break;
670
+ case 'transaction_created':
671
+ if (handlers.onTransactionCreated && isTransactionCreatedWebhook(webhook)) {
672
+ await handlers.onTransactionCreated(webhook.data);
673
+ }
674
+ break;
675
+ default:
676
+ if (handlers.onUnknownEvent) {
677
+ await handlers.onUnknownEvent(webhook.event, webhook.data);
678
+ }
679
+ break;
680
+ }
681
+ }
682
+ catch (error) {
683
+ throw new Error(`Error handling webhook event ${webhook.event}: ${error}`);
684
+ }
685
+ }
686
+ /**
687
+ * Update webhook secret
688
+ */
689
+ updateSecret(secret) {
690
+ if (!secret) {
691
+ throw new Error('Webhook secret is required');
692
+ }
693
+ this.secret = secret;
694
+ }
695
+ /**
696
+ * Get current secret
697
+ */
698
+ getSecret() {
699
+ return this.secret;
700
+ }
701
+ }
702
+
703
+ export { AuthenticationError, NetworkError, Raba7niClient, Raba7niError, Raba7niSDK, RateLimitError, ValidationError, WebhookHandler, calculateRetryDelay, deepClone, Raba7niSDK as default, formatDate, generateRequestId, isMemberJoinedWebhook, isMemberLeftWebhook, isPointsEarnedWebhook, isPointsRedeemedWebhook, isTransactionCreatedWebhook, isValidISODate, normalizePhoneNumber, parseWebhookPayload, sanitizeString, validateAmount, validateCardId, validateEmail, validateName, verifyWebhookSignature };
704
+ //# sourceMappingURL=index.esm.js.map