@semboja/connect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,635 @@
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
+ AuthenticationError: () => AuthenticationError,
24
+ NetworkError: () => NetworkError,
25
+ NotFoundError: () => NotFoundError,
26
+ RateLimitError: () => RateLimitError,
27
+ SembojaClient: () => SembojaClient,
28
+ SembojaError: () => SembojaError,
29
+ ServerError: () => ServerError,
30
+ ValidationError: () => ValidationError,
31
+ parseWebhookPayload: () => parseWebhookPayload,
32
+ verifyWebhookSignature: () => verifyWebhookSignature
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/errors.ts
37
+ var SembojaError = class _SembojaError extends Error {
38
+ /** Error code from the API */
39
+ code;
40
+ /** HTTP status code */
41
+ statusCode;
42
+ /** Request ID for debugging */
43
+ requestId;
44
+ constructor(message, code, statusCode, requestId) {
45
+ super(message);
46
+ this.name = "SembojaError";
47
+ this.code = code;
48
+ this.statusCode = statusCode;
49
+ this.requestId = requestId;
50
+ Object.setPrototypeOf(this, _SembojaError.prototype);
51
+ }
52
+ };
53
+ var AuthenticationError = class _AuthenticationError extends SembojaError {
54
+ constructor(message, requestId) {
55
+ super(message, "INVALID_API_KEY", 401, requestId);
56
+ this.name = "AuthenticationError";
57
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
58
+ }
59
+ };
60
+ var RateLimitError = class _RateLimitError extends SembojaError {
61
+ /** When the rate limit resets (Unix timestamp) */
62
+ resetAt;
63
+ constructor(message, requestId, resetAt) {
64
+ super(message, "RATE_LIMITED", 429, requestId);
65
+ this.name = "RateLimitError";
66
+ this.resetAt = resetAt;
67
+ Object.setPrototypeOf(this, _RateLimitError.prototype);
68
+ }
69
+ };
70
+ var ValidationError = class _ValidationError extends SembojaError {
71
+ constructor(message, requestId) {
72
+ super(message, "VALIDATION_ERROR", 400, requestId);
73
+ this.name = "ValidationError";
74
+ Object.setPrototypeOf(this, _ValidationError.prototype);
75
+ }
76
+ };
77
+ var NotFoundError = class _NotFoundError extends SembojaError {
78
+ constructor(message, code, requestId) {
79
+ super(message, code, 404, requestId);
80
+ this.name = "NotFoundError";
81
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
82
+ }
83
+ };
84
+ var ServerError = class _ServerError extends SembojaError {
85
+ constructor(message, requestId) {
86
+ super(message, "INTERNAL_ERROR", 500, requestId);
87
+ this.name = "ServerError";
88
+ Object.setPrototypeOf(this, _ServerError.prototype);
89
+ }
90
+ };
91
+ var NetworkError = class _NetworkError extends SembojaError {
92
+ constructor(message) {
93
+ super(message, "NETWORK_ERROR", 0);
94
+ this.name = "NetworkError";
95
+ Object.setPrototypeOf(this, _NetworkError.prototype);
96
+ }
97
+ };
98
+
99
+ // src/lib/http.ts
100
+ function sleep(ms) {
101
+ return new Promise((resolve) => setTimeout(resolve, ms));
102
+ }
103
+ function getBackoffDelay(attempt, baseDelay = 1e3) {
104
+ return Math.min(baseDelay * Math.pow(2, attempt), 3e4);
105
+ }
106
+ var HttpClient = class {
107
+ baseUrl;
108
+ apiKey;
109
+ timeout;
110
+ retries;
111
+ constructor(options) {
112
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
113
+ this.apiKey = options.apiKey;
114
+ this.timeout = options.timeout;
115
+ this.retries = options.retries;
116
+ }
117
+ /**
118
+ * Make an HTTP request with retry logic
119
+ */
120
+ async request(options) {
121
+ const url = `${this.baseUrl}${options.path}`;
122
+ const headers = {
123
+ "Content-Type": "application/json",
124
+ "X-API-Key": this.apiKey,
125
+ ...options.headers
126
+ };
127
+ let lastError = null;
128
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
129
+ try {
130
+ const controller = new AbortController();
131
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
132
+ const response = await fetch(url, {
133
+ method: options.method,
134
+ headers,
135
+ body: options.body ? JSON.stringify(options.body) : void 0,
136
+ signal: controller.signal
137
+ });
138
+ clearTimeout(timeoutId);
139
+ const data = await response.json();
140
+ if (!response.ok) {
141
+ throw this.parseError(response.status, data);
142
+ }
143
+ return data;
144
+ } catch (error) {
145
+ lastError = error;
146
+ if (error instanceof SembojaError) {
147
+ if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
148
+ throw error;
149
+ }
150
+ }
151
+ if (error instanceof Error && error.name === "AbortError") {
152
+ throw new NetworkError(`Request timeout after ${this.timeout}ms`);
153
+ }
154
+ if (attempt < this.retries) {
155
+ const delay = getBackoffDelay(attempt);
156
+ await sleep(delay);
157
+ continue;
158
+ }
159
+ }
160
+ }
161
+ if (lastError instanceof SembojaError) {
162
+ throw lastError;
163
+ }
164
+ throw new NetworkError(lastError?.message || "Request failed");
165
+ }
166
+ /**
167
+ * Parse error response into appropriate error class
168
+ */
169
+ parseError(statusCode, data) {
170
+ const message = data.error?.message || "Unknown error";
171
+ const code = data.error?.code || "UNKNOWN_ERROR";
172
+ const requestId = data.meta?.request_id;
173
+ switch (statusCode) {
174
+ case 401:
175
+ return new AuthenticationError(message, requestId);
176
+ case 429:
177
+ return new RateLimitError(message, requestId);
178
+ case 400:
179
+ return new ValidationError(message, requestId);
180
+ case 404:
181
+ return new NotFoundError(message, code, requestId);
182
+ case 500:
183
+ case 502:
184
+ case 503:
185
+ return new ServerError(message, requestId);
186
+ default:
187
+ return new SembojaError(message, code, statusCode, requestId);
188
+ }
189
+ }
190
+ /**
191
+ * GET request
192
+ */
193
+ async get(path) {
194
+ return this.request({ method: "GET", path });
195
+ }
196
+ /**
197
+ * POST request
198
+ */
199
+ async post(path, body) {
200
+ return this.request({ method: "POST", path, body });
201
+ }
202
+ };
203
+
204
+ // src/resources/messages.ts
205
+ var Messages = class {
206
+ constructor(http) {
207
+ this.http = http;
208
+ }
209
+ /**
210
+ * Send a text message
211
+ *
212
+ * @param options - Text message options
213
+ * @returns Message response with message ID
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const result = await client.messages.sendText({
218
+ * phoneNumberId: '123456789',
219
+ * to: '+6281234567890',
220
+ * text: 'Hello, World!',
221
+ * previewUrl: true,
222
+ * });
223
+ * console.log('Message ID:', result.data.messages[0].id);
224
+ * ```
225
+ */
226
+ async sendText(options) {
227
+ return this.http.post("/api/v1/messages/text", {
228
+ phone_number_id: options.phoneNumberId,
229
+ to: options.to,
230
+ text: options.text,
231
+ preview_url: options.previewUrl,
232
+ reply_to: options.replyTo
233
+ });
234
+ }
235
+ /**
236
+ * Send a template message
237
+ *
238
+ * @param options - Template message options
239
+ * @returns Message response with message ID
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const result = await client.messages.sendTemplate({
244
+ * phoneNumberId: '123456789',
245
+ * to: '+6281234567890',
246
+ * template: {
247
+ * name: 'hello_world',
248
+ * language: { code: 'en' },
249
+ * },
250
+ * });
251
+ * ```
252
+ */
253
+ async sendTemplate(options) {
254
+ return this.http.post("/api/v1/messages/template", {
255
+ phone_number_id: options.phoneNumberId,
256
+ to: options.to,
257
+ template: options.template
258
+ });
259
+ }
260
+ /**
261
+ * Send an image message
262
+ *
263
+ * @param options - Image message options
264
+ * @returns Message response with message ID
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * const result = await client.messages.sendImage({
269
+ * phoneNumberId: '123456789',
270
+ * to: '+6281234567890',
271
+ * image: {
272
+ * link: 'https://example.com/image.jpg',
273
+ * caption: 'Check this out!',
274
+ * },
275
+ * });
276
+ * ```
277
+ */
278
+ async sendImage(options) {
279
+ return this.http.post("/api/v1/messages", {
280
+ phone_number_id: options.phoneNumberId,
281
+ to: options.to,
282
+ type: "image",
283
+ image: options.image,
284
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
285
+ });
286
+ }
287
+ /**
288
+ * Send a video message
289
+ *
290
+ * @param options - Video message options
291
+ * @returns Message response with message ID
292
+ */
293
+ async sendVideo(options) {
294
+ return this.http.post("/api/v1/messages", {
295
+ phone_number_id: options.phoneNumberId,
296
+ to: options.to,
297
+ type: "video",
298
+ video: options.video,
299
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
300
+ });
301
+ }
302
+ /**
303
+ * Send an audio message
304
+ *
305
+ * @param options - Audio message options
306
+ * @returns Message response with message ID
307
+ */
308
+ async sendAudio(options) {
309
+ return this.http.post("/api/v1/messages", {
310
+ phone_number_id: options.phoneNumberId,
311
+ to: options.to,
312
+ type: "audio",
313
+ audio: options.audio,
314
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
315
+ });
316
+ }
317
+ /**
318
+ * Send a document message
319
+ *
320
+ * @param options - Document message options
321
+ * @returns Message response with message ID
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * const result = await client.messages.sendDocument({
326
+ * phoneNumberId: '123456789',
327
+ * to: '+6281234567890',
328
+ * document: {
329
+ * link: 'https://example.com/invoice.pdf',
330
+ * filename: 'invoice.pdf',
331
+ * caption: 'Your invoice',
332
+ * },
333
+ * });
334
+ * ```
335
+ */
336
+ async sendDocument(options) {
337
+ return this.http.post("/api/v1/messages", {
338
+ phone_number_id: options.phoneNumberId,
339
+ to: options.to,
340
+ type: "document",
341
+ document: options.document,
342
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
343
+ });
344
+ }
345
+ /**
346
+ * Send a reaction to a message
347
+ *
348
+ * @param options - Reaction options
349
+ * @returns Message response
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * // Add reaction
354
+ * await client.messages.sendReaction({
355
+ * phoneNumberId: '123456789',
356
+ * to: '+6281234567890',
357
+ * reaction: {
358
+ * messageId: 'wamid.xxx',
359
+ * emoji: '👍',
360
+ * },
361
+ * });
362
+ *
363
+ * // Remove reaction
364
+ * await client.messages.sendReaction({
365
+ * phoneNumberId: '123456789',
366
+ * to: '+6281234567890',
367
+ * reaction: {
368
+ * messageId: 'wamid.xxx',
369
+ * emoji: '',
370
+ * },
371
+ * });
372
+ * ```
373
+ */
374
+ async sendReaction(options) {
375
+ return this.http.post("/api/v1/messages", {
376
+ phone_number_id: options.phoneNumberId,
377
+ to: options.to,
378
+ type: "reaction",
379
+ reaction: {
380
+ message_id: options.reaction.messageId,
381
+ emoji: options.reaction.emoji
382
+ }
383
+ });
384
+ }
385
+ /**
386
+ * Send an interactive message (buttons, lists, etc.)
387
+ *
388
+ * @param options - Interactive message options
389
+ * @returns Message response with message ID
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * // Button message
394
+ * await client.messages.sendInteractive({
395
+ * phoneNumberId: '123456789',
396
+ * to: '+6281234567890',
397
+ * interactive: {
398
+ * type: 'button',
399
+ * body: { text: 'Choose an option:' },
400
+ * action: {
401
+ * buttons: [
402
+ * { type: 'reply', reply: { id: 'yes', title: 'Yes' } },
403
+ * { type: 'reply', reply: { id: 'no', title: 'No' } },
404
+ * ],
405
+ * },
406
+ * },
407
+ * });
408
+ * ```
409
+ */
410
+ async sendInteractive(options) {
411
+ return this.http.post("/api/v1/messages", {
412
+ phone_number_id: options.phoneNumberId,
413
+ to: options.to,
414
+ type: "interactive",
415
+ interactive: options.interactive,
416
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
417
+ });
418
+ }
419
+ };
420
+
421
+ // src/resources/templates.ts
422
+ var Templates = class {
423
+ constructor(http) {
424
+ this.http = http;
425
+ }
426
+ /**
427
+ * List all message templates
428
+ *
429
+ * @param options - Optional filters
430
+ * @returns List of templates
431
+ *
432
+ * @example
433
+ * ```typescript
434
+ * const templates = await client.templates.list();
435
+ * console.log('Templates:', templates.data);
436
+ *
437
+ * // Filter by status
438
+ * const approved = await client.templates.list({ status: 'APPROVED' });
439
+ * ```
440
+ */
441
+ async list(options) {
442
+ let path = "/api/v1/templates";
443
+ if (options?.status) {
444
+ path += `?status=${encodeURIComponent(options.status)}`;
445
+ }
446
+ return this.http.get(path);
447
+ }
448
+ };
449
+
450
+ // src/resources/phone-numbers.ts
451
+ var PhoneNumbers = class {
452
+ constructor(http) {
453
+ this.http = http;
454
+ }
455
+ /**
456
+ * List all phone numbers configured for your account
457
+ *
458
+ * @returns List of phone numbers
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * const phoneNumbers = await client.phoneNumbers.list();
463
+ * for (const phone of phoneNumbers.data) {
464
+ * console.log(`${phone.verified_name}: ${phone.display_phone_number}`);
465
+ * }
466
+ * ```
467
+ */
468
+ async list() {
469
+ return this.http.get("/api/v1/phone-numbers");
470
+ }
471
+ };
472
+
473
+ // src/resources/usage.ts
474
+ var Usage = class {
475
+ constructor(http) {
476
+ this.http = http;
477
+ }
478
+ /**
479
+ * Get current usage statistics for the billing period
480
+ *
481
+ * @returns Usage statistics
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * const usage = await client.usage.get();
486
+ * console.log(`Period: ${usage.data.period.start} - ${usage.data.period.end}`);
487
+ * console.log(`Messages sent: ${usage.data.messages.sent}`);
488
+ * console.log(`Messages received: ${usage.data.messages.received}`);
489
+ * console.log(`API calls: ${usage.data.api_calls}`);
490
+ * ```
491
+ */
492
+ async get() {
493
+ return this.http.get("/api/v1/usage");
494
+ }
495
+ };
496
+
497
+ // src/resources/test.ts
498
+ var Test = class {
499
+ constructor(http) {
500
+ this.http = http;
501
+ }
502
+ /**
503
+ * Trigger a test webhook to simulate an incoming message
504
+ *
505
+ * **Note:** This only works with test API keys (sk_test_*)
506
+ *
507
+ * @param options - Webhook trigger options
508
+ * @returns Success response
509
+ *
510
+ * @example
511
+ * ```typescript
512
+ * await client.test.triggerWebhook({
513
+ * phoneNumberId: '123456789',
514
+ * type: 'text',
515
+ * from: '+6281234567890',
516
+ * text: 'Hello, this is a test incoming message!',
517
+ * });
518
+ * ```
519
+ */
520
+ async triggerWebhook(options) {
521
+ return this.http.post("/api/v1/test/webhooks/trigger", {
522
+ phone_number_id: options.phoneNumberId,
523
+ type: options.type || "text",
524
+ from: options.from,
525
+ text: options.text
526
+ });
527
+ }
528
+ };
529
+
530
+ // src/client.ts
531
+ var DEFAULT_BASE_URL = "https://connect.semboja.tech";
532
+ var DEFAULT_TIMEOUT = 3e4;
533
+ var DEFAULT_RETRIES = 3;
534
+ var SembojaClient = class {
535
+ /** Messages API */
536
+ messages;
537
+ /** Templates API */
538
+ templates;
539
+ /** Phone Numbers API */
540
+ phoneNumbers;
541
+ /** Usage API */
542
+ usage;
543
+ /** Test API (only works with sk_test_* keys) */
544
+ test;
545
+ /** Whether the client is in test mode */
546
+ isTestMode;
547
+ http;
548
+ /**
549
+ * Create a new Semboja client
550
+ *
551
+ * @param optionsOrApiKey - API key string or client options object
552
+ *
553
+ * @example
554
+ * ```typescript
555
+ * // Using API key directly
556
+ * const client = new SembojaClient('sk_live_xxx');
557
+ *
558
+ * // Using options object
559
+ * const client = new SembojaClient({
560
+ * apiKey: 'sk_live_xxx',
561
+ * timeout: 60000,
562
+ * retries: 5,
563
+ * });
564
+ * ```
565
+ */
566
+ constructor(optionsOrApiKey) {
567
+ const options = typeof optionsOrApiKey === "string" ? { apiKey: optionsOrApiKey } : optionsOrApiKey;
568
+ if (!options.apiKey) {
569
+ throw new Error("API key is required");
570
+ }
571
+ if (!options.apiKey.startsWith("sk_live_") && !options.apiKey.startsWith("sk_test_")) {
572
+ throw new Error("Invalid API key format. Must start with sk_live_ or sk_test_");
573
+ }
574
+ this.isTestMode = options.apiKey.startsWith("sk_test_");
575
+ this.http = new HttpClient({
576
+ baseUrl: options.baseUrl || DEFAULT_BASE_URL,
577
+ apiKey: options.apiKey,
578
+ timeout: options.timeout || DEFAULT_TIMEOUT,
579
+ retries: options.retries ?? DEFAULT_RETRIES
580
+ });
581
+ this.messages = new Messages(this.http);
582
+ this.templates = new Templates(this.http);
583
+ this.phoneNumbers = new PhoneNumbers(this.http);
584
+ this.usage = new Usage(this.http);
585
+ this.test = new Test(this.http);
586
+ }
587
+ };
588
+
589
+ // src/webhooks/verify.ts
590
+ var import_crypto = require("crypto");
591
+ function verifyWebhookSignature(options) {
592
+ const { payload, signature, timestamp, secret } = options;
593
+ if (!payload || !signature || !timestamp || !secret) {
594
+ return false;
595
+ }
596
+ const timestampNum = parseInt(timestamp, 10);
597
+ const now = Math.floor(Date.now() / 1e3);
598
+ const tolerance = 5 * 60;
599
+ if (isNaN(timestampNum) || Math.abs(now - timestampNum) > tolerance) {
600
+ return false;
601
+ }
602
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
603
+ const signatureData = timestamp + payloadString;
604
+ const expectedSignature = "sha256=" + (0, import_crypto.createHmac)("sha256", secret).update(signatureData).digest("hex");
605
+ try {
606
+ const sigBuffer = Buffer.from(signature);
607
+ const expectedBuffer = Buffer.from(expectedSignature);
608
+ if (sigBuffer.length !== expectedBuffer.length) {
609
+ return false;
610
+ }
611
+ return (0, import_crypto.timingSafeEqual)(sigBuffer, expectedBuffer);
612
+ } catch {
613
+ return false;
614
+ }
615
+ }
616
+ function parseWebhookPayload(payload) {
617
+ if (typeof payload === "string") {
618
+ return JSON.parse(payload);
619
+ }
620
+ return payload;
621
+ }
622
+ // Annotate the CommonJS export names for ESM import in node:
623
+ 0 && (module.exports = {
624
+ AuthenticationError,
625
+ NetworkError,
626
+ NotFoundError,
627
+ RateLimitError,
628
+ SembojaClient,
629
+ SembojaError,
630
+ ServerError,
631
+ ValidationError,
632
+ parseWebhookPayload,
633
+ verifyWebhookSignature
634
+ });
635
+ //# sourceMappingURL=index.js.map