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