@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/lib/http.ts","../src/resources/messages.ts","../src/resources/templates.ts","../src/resources/phone-numbers.ts","../src/resources/usage.ts","../src/resources/test.ts","../src/client.ts","../src/webhooks/verify.ts"],"sourcesContent":["/**\n * @semboja/connect - Official Node.js SDK for Semboja WhatsApp API Bridge\n * \n * @example\n * ```typescript\n * import { SembojaClient, verifyWebhookSignature } from '@semboja/connect';\n * \n * const client = new SembojaClient('sk_live_your_api_key');\n * \n * // Send a text message\n * await client.messages.sendText({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * text: 'Hello from Semboja!',\n * });\n * \n * // Verify webhook signature\n * const isValid = verifyWebhookSignature({\n * payload: req.body,\n * signature: req.headers['x-semboja-signature'],\n * timestamp: req.headers['x-semboja-timestamp'],\n * secret: process.env.WEBHOOK_SECRET,\n * });\n * ```\n * \n * @packageDocumentation\n */\n\n// Main client\nexport { SembojaClient } from './client';\n\n// Webhook utilities\nexport { verifyWebhookSignature, parseWebhookPayload } from './webhooks/verify';\n\n// Error classes\nexport {\n SembojaError,\n AuthenticationError,\n RateLimitError,\n ValidationError,\n NotFoundError,\n ServerError,\n NetworkError,\n} from './errors';\n\n// Types\nexport type {\n // Client\n ClientOptions,\n ApiResponse,\n ApiErrorResponse,\n \n // Messages\n MessageType,\n SendMessageOptions,\n SendTextOptions,\n SendTemplateOptions,\n SendImageOptions,\n SendVideoOptions,\n SendAudioOptions,\n SendDocumentOptions,\n SendReactionOptions,\n SendInteractiveOptions,\n Template,\n TemplateLanguage,\n TemplateComponent,\n TemplateParameter,\n MediaObject,\n InteractiveMessage,\n InteractiveButton,\n InteractiveListSection,\n MessageResponseData,\n MessageContact,\n MessageResult,\n \n // Templates\n TemplateStatus,\n TemplateInfo,\n ListTemplatesOptions,\n \n // Phone Numbers\n PhoneNumber,\n \n // Usage\n UsageData,\n UsagePeriod,\n UsageMessages,\n \n // Test\n TriggerWebhookOptions,\n \n // Webhooks\n VerifyWebhookOptions,\n} from './types';\n","/**\n * Base error class for all Semboja API errors\n */\nexport class SembojaError extends Error {\n /** Error code from the API */\n readonly code: string;\n /** HTTP status code */\n readonly statusCode: number;\n /** Request ID for debugging */\n readonly requestId?: string;\n\n constructor(\n message: string,\n code: string,\n statusCode: number,\n requestId?: string\n ) {\n super(message);\n this.name = 'SembojaError';\n this.code = code;\n this.statusCode = statusCode;\n this.requestId = requestId;\n Object.setPrototypeOf(this, SembojaError.prototype);\n }\n}\n\n/**\n * Authentication error (invalid or missing API key)\n */\nexport class AuthenticationError extends SembojaError {\n constructor(message: string, requestId?: string) {\n super(message, 'INVALID_API_KEY', 401, requestId);\n this.name = 'AuthenticationError';\n Object.setPrototypeOf(this, AuthenticationError.prototype);\n }\n}\n\n/**\n * Rate limit exceeded error\n */\nexport class RateLimitError extends SembojaError {\n /** When the rate limit resets (Unix timestamp) */\n readonly resetAt?: number;\n\n constructor(message: string, requestId?: string, resetAt?: number) {\n super(message, 'RATE_LIMITED', 429, requestId);\n this.name = 'RateLimitError';\n this.resetAt = resetAt;\n Object.setPrototypeOf(this, RateLimitError.prototype);\n }\n}\n\n/**\n * Validation error (invalid request parameters)\n */\nexport class ValidationError extends SembojaError {\n constructor(message: string, requestId?: string) {\n super(message, 'VALIDATION_ERROR', 400, requestId);\n this.name = 'ValidationError';\n Object.setPrototypeOf(this, ValidationError.prototype);\n }\n}\n\n/**\n * Resource not found error\n */\nexport class NotFoundError extends SembojaError {\n constructor(message: string, code: string, requestId?: string) {\n super(message, code, 404, requestId);\n this.name = 'NotFoundError';\n Object.setPrototypeOf(this, NotFoundError.prototype);\n }\n}\n\n/**\n * Server error from Semboja API\n */\nexport class ServerError extends SembojaError {\n constructor(message: string, requestId?: string) {\n super(message, 'INTERNAL_ERROR', 500, requestId);\n this.name = 'ServerError';\n Object.setPrototypeOf(this, ServerError.prototype);\n }\n}\n\n/**\n * Network or connection error\n */\nexport class NetworkError extends SembojaError {\n constructor(message: string) {\n super(message, 'NETWORK_ERROR', 0);\n this.name = 'NetworkError';\n Object.setPrototypeOf(this, NetworkError.prototype);\n }\n}\n","import {\n SembojaError,\n AuthenticationError,\n RateLimitError,\n ValidationError,\n NotFoundError,\n ServerError,\n NetworkError,\n} from '../errors';\nimport type { ApiErrorResponse } from '../types';\n\nexport interface HttpClientOptions {\n baseUrl: string;\n apiKey: string;\n timeout: number;\n retries: number;\n}\n\nexport interface RequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n path: string;\n body?: unknown;\n headers?: Record<string, string>;\n}\n\n/**\n * Sleep for a given number of milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Calculate exponential backoff delay\n */\nfunction getBackoffDelay(attempt: number, baseDelay = 1000): number {\n return Math.min(baseDelay * Math.pow(2, attempt), 30000);\n}\n\n/**\n * HTTP client for making API requests\n */\nexport class HttpClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeout: number;\n private readonly retries: number;\n\n constructor(options: HttpClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.timeout = options.timeout;\n this.retries = options.retries;\n }\n\n /**\n * Make an HTTP request with retry logic\n */\n async request<T>(options: RequestOptions): Promise<T> {\n const url = `${this.baseUrl}${options.path}`;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n ...options.headers,\n };\n\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= this.retries; attempt++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Parse response\n const data = await response.json();\n\n // Handle errors\n if (!response.ok) {\n throw this.parseError(response.status, data as ApiErrorResponse);\n }\n\n return data as T;\n } catch (error) {\n lastError = error as Error;\n\n // Don't retry on client errors (4xx) except rate limits\n if (error instanceof SembojaError) {\n if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {\n throw error;\n }\n }\n\n // Don't retry on abort (timeout)\n if (error instanceof Error && error.name === 'AbortError') {\n throw new NetworkError(`Request timeout after ${this.timeout}ms`);\n }\n\n // Retry on network errors and rate limits\n if (attempt < this.retries) {\n const delay = getBackoffDelay(attempt);\n await sleep(delay);\n continue;\n }\n }\n }\n\n // If we get here, all retries failed\n if (lastError instanceof SembojaError) {\n throw lastError;\n }\n throw new NetworkError(lastError?.message || 'Request failed');\n }\n\n /**\n * Parse error response into appropriate error class\n */\n private parseError(statusCode: number, data: ApiErrorResponse): SembojaError {\n const message = data.error?.message || 'Unknown error';\n const code = data.error?.code || 'UNKNOWN_ERROR';\n const requestId = data.meta?.request_id;\n\n switch (statusCode) {\n case 401:\n return new AuthenticationError(message, requestId);\n case 429:\n return new RateLimitError(message, requestId);\n case 400:\n return new ValidationError(message, requestId);\n case 404:\n return new NotFoundError(message, code, requestId);\n case 500:\n case 502:\n case 503:\n return new ServerError(message, requestId);\n default:\n return new SembojaError(message, code, statusCode, requestId);\n }\n }\n\n /**\n * GET request\n */\n async get<T>(path: string): Promise<T> {\n return this.request<T>({ method: 'GET', path });\n }\n\n /**\n * POST request\n */\n async post<T>(path: string, body?: unknown): Promise<T> {\n return this.request<T>({ method: 'POST', path, body });\n }\n}\n","import type { HttpClient } from '../lib/http';\nimport type {\n ApiResponse,\n MessageResponseData,\n SendTextOptions,\n SendTemplateOptions,\n SendImageOptions,\n SendVideoOptions,\n SendAudioOptions,\n SendDocumentOptions,\n SendReactionOptions,\n SendInteractiveOptions,\n} from '../types';\n\n/**\n * Messages API resource\n * \n * @example\n * ```typescript\n * await client.messages.sendText({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * text: 'Hello from Semboja!',\n * });\n * ```\n */\nexport class Messages {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Send a text message\n * \n * @param options - Text message options\n * @returns Message response with message ID\n * \n * @example\n * ```typescript\n * const result = await client.messages.sendText({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * text: 'Hello, World!',\n * previewUrl: true,\n * });\n * console.log('Message ID:', result.data.messages[0].id);\n * ```\n */\n async sendText(options: SendTextOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages/text', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n text: options.text,\n preview_url: options.previewUrl,\n reply_to: options.replyTo,\n });\n }\n\n /**\n * Send a template message\n * \n * @param options - Template message options\n * @returns Message response with message ID\n * \n * @example\n * ```typescript\n * const result = await client.messages.sendTemplate({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * template: {\n * name: 'hello_world',\n * language: { code: 'en' },\n * },\n * });\n * ```\n */\n async sendTemplate(options: SendTemplateOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages/template', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n template: options.template,\n });\n }\n\n /**\n * Send an image message\n * \n * @param options - Image message options\n * @returns Message response with message ID\n * \n * @example\n * ```typescript\n * const result = await client.messages.sendImage({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * image: {\n * link: 'https://example.com/image.jpg',\n * caption: 'Check this out!',\n * },\n * });\n * ```\n */\n async sendImage(options: SendImageOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n type: 'image',\n image: options.image,\n context: options.replyTo ? { message_id: options.replyTo } : undefined,\n });\n }\n\n /**\n * Send a video message\n * \n * @param options - Video message options\n * @returns Message response with message ID\n */\n async sendVideo(options: SendVideoOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n type: 'video',\n video: options.video,\n context: options.replyTo ? { message_id: options.replyTo } : undefined,\n });\n }\n\n /**\n * Send an audio message\n * \n * @param options - Audio message options\n * @returns Message response with message ID\n */\n async sendAudio(options: SendAudioOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n type: 'audio',\n audio: options.audio,\n context: options.replyTo ? { message_id: options.replyTo } : undefined,\n });\n }\n\n /**\n * Send a document message\n * \n * @param options - Document message options\n * @returns Message response with message ID\n * \n * @example\n * ```typescript\n * const result = await client.messages.sendDocument({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * document: {\n * link: 'https://example.com/invoice.pdf',\n * filename: 'invoice.pdf',\n * caption: 'Your invoice',\n * },\n * });\n * ```\n */\n async sendDocument(options: SendDocumentOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n type: 'document',\n document: options.document,\n context: options.replyTo ? { message_id: options.replyTo } : undefined,\n });\n }\n\n /**\n * Send a reaction to a message\n * \n * @param options - Reaction options\n * @returns Message response\n * \n * @example\n * ```typescript\n * // Add reaction\n * await client.messages.sendReaction({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * reaction: {\n * messageId: 'wamid.xxx',\n * emoji: '👍',\n * },\n * });\n * \n * // Remove reaction\n * await client.messages.sendReaction({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * reaction: {\n * messageId: 'wamid.xxx',\n * emoji: '',\n * },\n * });\n * ```\n */\n async sendReaction(options: SendReactionOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n type: 'reaction',\n reaction: {\n message_id: options.reaction.messageId,\n emoji: options.reaction.emoji,\n },\n });\n }\n\n /**\n * Send an interactive message (buttons, lists, etc.)\n * \n * @param options - Interactive message options\n * @returns Message response with message ID\n * \n * @example\n * ```typescript\n * // Button message\n * await client.messages.sendInteractive({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * interactive: {\n * type: 'button',\n * body: { text: 'Choose an option:' },\n * action: {\n * buttons: [\n * { type: 'reply', reply: { id: 'yes', title: 'Yes' } },\n * { type: 'reply', reply: { id: 'no', title: 'No' } },\n * ],\n * },\n * },\n * });\n * ```\n */\n async sendInteractive(options: SendInteractiveOptions): Promise<ApiResponse<MessageResponseData>> {\n return this.http.post('/api/v1/messages', {\n phone_number_id: options.phoneNumberId,\n to: options.to,\n type: 'interactive',\n interactive: options.interactive,\n context: options.replyTo ? { message_id: options.replyTo } : undefined,\n });\n }\n}\n","import type { HttpClient } from '../lib/http';\nimport type { ApiResponse, TemplateInfo, ListTemplatesOptions } from '../types';\n\n/**\n * Templates API resource\n * \n * @example\n * ```typescript\n * const templates = await client.templates.list();\n * const approved = await client.templates.list({ status: 'APPROVED' });\n * ```\n */\nexport class Templates {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * List all message templates\n * \n * @param options - Optional filters\n * @returns List of templates\n * \n * @example\n * ```typescript\n * const templates = await client.templates.list();\n * console.log('Templates:', templates.data);\n * \n * // Filter by status\n * const approved = await client.templates.list({ status: 'APPROVED' });\n * ```\n */\n async list(options?: ListTemplatesOptions): Promise<ApiResponse<TemplateInfo[]>> {\n let path = '/api/v1/templates';\n \n if (options?.status) {\n path += `?status=${encodeURIComponent(options.status)}`;\n }\n \n return this.http.get(path);\n }\n}\n","import type { HttpClient } from '../lib/http';\nimport type { ApiResponse, PhoneNumber } from '../types';\n\n/**\n * Phone Numbers API resource\n * \n * @example\n * ```typescript\n * const phoneNumbers = await client.phoneNumbers.list();\n * ```\n */\nexport class PhoneNumbers {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * List all phone numbers configured for your account\n * \n * @returns List of phone numbers\n * \n * @example\n * ```typescript\n * const phoneNumbers = await client.phoneNumbers.list();\n * for (const phone of phoneNumbers.data) {\n * console.log(`${phone.verified_name}: ${phone.display_phone_number}`);\n * }\n * ```\n */\n async list(): Promise<ApiResponse<PhoneNumber[]>> {\n return this.http.get('/api/v1/phone-numbers');\n }\n}\n","import type { HttpClient } from '../lib/http';\nimport type { ApiResponse, UsageData } from '../types';\n\n/**\n * Usage API resource\n * \n * @example\n * ```typescript\n * const usage = await client.usage.get();\n * console.log(`Sent: ${usage.data.messages.sent}`);\n * ```\n */\nexport class Usage {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Get current usage statistics for the billing period\n * \n * @returns Usage statistics\n * \n * @example\n * ```typescript\n * const usage = await client.usage.get();\n * console.log(`Period: ${usage.data.period.start} - ${usage.data.period.end}`);\n * console.log(`Messages sent: ${usage.data.messages.sent}`);\n * console.log(`Messages received: ${usage.data.messages.received}`);\n * console.log(`API calls: ${usage.data.api_calls}`);\n * ```\n */\n async get(): Promise<ApiResponse<UsageData>> {\n return this.http.get('/api/v1/usage');\n }\n}\n","import type { HttpClient } from '../lib/http';\nimport type { ApiResponse, TriggerWebhookOptions } from '../types';\n\n/**\n * Test API resource (only works with sk_test_* keys)\n * \n * @example\n * ```typescript\n * // Trigger a test webhook\n * await client.test.triggerWebhook({\n * phoneNumberId: '123456789',\n * type: 'text',\n * from: '+6281234567890',\n * text: 'Test message',\n * });\n * ```\n */\nexport class Test {\n constructor(private readonly http: HttpClient) {}\n\n /**\n * Trigger a test webhook to simulate an incoming message\n * \n * **Note:** This only works with test API keys (sk_test_*)\n * \n * @param options - Webhook trigger options\n * @returns Success response\n * \n * @example\n * ```typescript\n * await client.test.triggerWebhook({\n * phoneNumberId: '123456789',\n * type: 'text',\n * from: '+6281234567890',\n * text: 'Hello, this is a test incoming message!',\n * });\n * ```\n */\n async triggerWebhook(options: TriggerWebhookOptions): Promise<ApiResponse<{ queued: boolean }>> {\n return this.http.post('/api/v1/test/webhooks/trigger', {\n phone_number_id: options.phoneNumberId,\n type: options.type || 'text',\n from: options.from,\n text: options.text,\n });\n }\n}\n","import { HttpClient } from './lib/http';\nimport { Messages } from './resources/messages';\nimport { Templates } from './resources/templates';\nimport { PhoneNumbers } from './resources/phone-numbers';\nimport { Usage } from './resources/usage';\nimport { Test } from './resources/test';\nimport type { ClientOptions } from './types';\n\nconst DEFAULT_BASE_URL = 'https://connect.semboja.tech';\nconst DEFAULT_TIMEOUT = 30000;\nconst DEFAULT_RETRIES = 3;\n\n/**\n * Semboja WhatsApp API Client\n * \n * @example\n * ```typescript\n * import { SembojaClient } from '@semboja/connect';\n * \n * // Simple initialization\n * const client = new SembojaClient('sk_live_your_api_key');\n * \n * // With options\n * const client = new SembojaClient({\n * apiKey: process.env.SEMBOJA_API_KEY!,\n * timeout: 60000,\n * retries: 5,\n * });\n * \n * // Send a message\n * await client.messages.sendText({\n * phoneNumberId: '123456789',\n * to: '+6281234567890',\n * text: 'Hello from Semboja!',\n * });\n * ```\n */\nexport class SembojaClient {\n /** Messages API */\n readonly messages: Messages;\n \n /** Templates API */\n readonly templates: Templates;\n \n /** Phone Numbers API */\n readonly phoneNumbers: PhoneNumbers;\n \n /** Usage API */\n readonly usage: Usage;\n \n /** Test API (only works with sk_test_* keys) */\n readonly test: Test;\n\n /** Whether the client is in test mode */\n readonly isTestMode: boolean;\n\n private readonly http: HttpClient;\n\n /**\n * Create a new Semboja client\n * \n * @param optionsOrApiKey - API key string or client options object\n * \n * @example\n * ```typescript\n * // Using API key directly\n * const client = new SembojaClient('sk_live_xxx');\n * \n * // Using options object\n * const client = new SembojaClient({\n * apiKey: 'sk_live_xxx',\n * timeout: 60000,\n * retries: 5,\n * });\n * ```\n */\n constructor(optionsOrApiKey: string | ClientOptions) {\n const options: ClientOptions = typeof optionsOrApiKey === 'string'\n ? { apiKey: optionsOrApiKey }\n : optionsOrApiKey;\n\n // Validate API key\n if (!options.apiKey) {\n throw new Error('API key is required');\n }\n\n if (!options.apiKey.startsWith('sk_live_') && !options.apiKey.startsWith('sk_test_')) {\n throw new Error('Invalid API key format. Must start with sk_live_ or sk_test_');\n }\n\n // Determine if test mode\n this.isTestMode = options.apiKey.startsWith('sk_test_');\n\n // Create HTTP client\n this.http = new HttpClient({\n baseUrl: options.baseUrl || DEFAULT_BASE_URL,\n apiKey: options.apiKey,\n timeout: options.timeout || DEFAULT_TIMEOUT,\n retries: options.retries ?? DEFAULT_RETRIES,\n });\n\n // Initialize resources\n this.messages = new Messages(this.http);\n this.templates = new Templates(this.http);\n this.phoneNumbers = new PhoneNumbers(this.http);\n this.usage = new Usage(this.http);\n this.test = new Test(this.http);\n }\n}\n","import { createHmac, timingSafeEqual } from 'crypto';\nimport type { VerifyWebhookOptions } from '../types';\n\n/**\n * Verify a webhook signature from Semboja\n * \n * @param options - Verification options\n * @returns true if the signature is valid, false otherwise\n * \n * @example\n * ```typescript\n * import { verifyWebhookSignature } from '@semboja/connect';\n * \n * // Express.js example\n * app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {\n * const isValid = verifyWebhookSignature({\n * payload: req.body,\n * signature: req.headers['x-semboja-signature'] as string,\n * timestamp: req.headers['x-semboja-timestamp'] as string,\n * secret: process.env.WEBHOOK_SECRET!,\n * });\n * \n * if (!isValid) {\n * return res.status(401).send('Invalid signature');\n * }\n * \n * // Process the webhook\n * const event = JSON.parse(req.body.toString());\n * console.log('Received event:', event);\n * \n * res.status(200).send('OK');\n * });\n * ```\n * \n * @example\n * ```typescript\n * // Hono example\n * app.post('/webhook', async (c) => {\n * const body = await c.req.text();\n * const signature = c.req.header('x-semboja-signature');\n * const timestamp = c.req.header('x-semboja-timestamp');\n * \n * const isValid = verifyWebhookSignature({\n * payload: body,\n * signature: signature!,\n * timestamp: timestamp!,\n * secret: process.env.WEBHOOK_SECRET!,\n * });\n * \n * if (!isValid) {\n * return c.text('Invalid signature', 401);\n * }\n * \n * const event = JSON.parse(body);\n * // Process the webhook...\n * \n * return c.text('OK');\n * });\n * ```\n */\nexport function verifyWebhookSignature(options: VerifyWebhookOptions): boolean {\n const { payload, signature, timestamp, secret } = options;\n\n // Validate inputs\n if (!payload || !signature || !timestamp || !secret) {\n return false;\n }\n\n // Check timestamp to prevent replay attacks (5 minute tolerance)\n const timestampNum = parseInt(timestamp, 10);\n const now = Math.floor(Date.now() / 1000);\n const tolerance = 5 * 60; // 5 minutes\n\n if (isNaN(timestampNum) || Math.abs(now - timestampNum) > tolerance) {\n return false;\n }\n\n // Stringify payload if it's an object\n const payloadString = typeof payload === 'string' \n ? payload \n : JSON.stringify(payload);\n\n // Compute expected signature\n const signatureData = timestamp + payloadString;\n const expectedSignature = 'sha256=' + createHmac('sha256', secret)\n .update(signatureData)\n .digest('hex');\n\n // Compare signatures using timing-safe comparison\n try {\n const sigBuffer = Buffer.from(signature);\n const expectedBuffer = Buffer.from(expectedSignature);\n\n if (sigBuffer.length !== expectedBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(sigBuffer, expectedBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Parse a webhook payload into a typed event object\n * \n * @param payload - Raw webhook payload (string or object)\n * @returns Parsed webhook event\n */\nexport function parseWebhookPayload<T = unknown>(payload: string | object): T {\n if (typeof payload === 'string') {\n return JSON.parse(payload) as T;\n }\n return payload as T;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGO,IAAM,eAAN,MAAM,sBAAqB,MAAM;AAAA;AAAA,EAE7B;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAET,YACE,SACA,MACA,YACA,WACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,YAAY;AACjB,WAAO,eAAe,MAAM,cAAa,SAAS;AAAA,EACpD;AACF;AAKO,IAAM,sBAAN,MAAM,6BAA4B,aAAa;AAAA,EACpD,YAAY,SAAiB,WAAoB;AAC/C,UAAM,SAAS,mBAAmB,KAAK,SAAS;AAChD,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,qBAAoB,SAAS;AAAA,EAC3D;AACF;AAKO,IAAM,iBAAN,MAAM,wBAAuB,aAAa;AAAA;AAAA,EAEtC;AAAA,EAET,YAAY,SAAiB,WAAoB,SAAkB;AACjE,UAAM,SAAS,gBAAgB,KAAK,SAAS;AAC7C,SAAK,OAAO;AACZ,SAAK,UAAU;AACf,WAAO,eAAe,MAAM,gBAAe,SAAS;AAAA,EACtD;AACF;AAKO,IAAM,kBAAN,MAAM,yBAAwB,aAAa;AAAA,EAChD,YAAY,SAAiB,WAAoB;AAC/C,UAAM,SAAS,oBAAoB,KAAK,SAAS;AACjD,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,iBAAgB,SAAS;AAAA,EACvD;AACF;AAKO,IAAM,gBAAN,MAAM,uBAAsB,aAAa;AAAA,EAC9C,YAAY,SAAiB,MAAc,WAAoB;AAC7D,UAAM,SAAS,MAAM,KAAK,SAAS;AACnC,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,eAAc,SAAS;AAAA,EACrD;AACF;AAKO,IAAM,cAAN,MAAM,qBAAoB,aAAa;AAAA,EAC5C,YAAY,SAAiB,WAAoB;AAC/C,UAAM,SAAS,kBAAkB,KAAK,SAAS;AAC/C,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,aAAY,SAAS;AAAA,EACnD;AACF;AAKO,IAAM,eAAN,MAAM,sBAAqB,aAAa;AAAA,EAC7C,YAAY,SAAiB;AAC3B,UAAM,SAAS,iBAAiB,CAAC;AACjC,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,cAAa,SAAS;AAAA,EACpD;AACF;;;AClEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAKA,SAAS,gBAAgB,SAAiB,YAAY,KAAc;AAClE,SAAO,KAAK,IAAI,YAAY,KAAK,IAAI,GAAG,OAAO,GAAG,GAAK;AACzD;AAKO,IAAM,aAAN,MAAiB;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,SAA4B;AACtC,SAAK,UAAU,QAAQ,QAAQ,QAAQ,OAAO,EAAE;AAChD,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AACvB,SAAK,UAAU,QAAQ;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAW,SAAqC;AACpD,UAAM,MAAM,GAAG,KAAK,OAAO,GAAG,QAAQ,IAAI;AAC1C,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,MAChB,aAAa,KAAK;AAAA,MAClB,GAAG,QAAQ;AAAA,IACb;AAEA,QAAI,YAA0B;AAE9B,aAAS,UAAU,GAAG,WAAW,KAAK,SAAS,WAAW;AACxD,UAAI;AACF,cAAM,aAAa,IAAI,gBAAgB;AACvC,cAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAEnE,cAAM,WAAW,MAAM,MAAM,KAAK;AAAA,UAChC,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,MAAM,QAAQ,OAAO,KAAK,UAAU,QAAQ,IAAI,IAAI;AAAA,UACpD,QAAQ,WAAW;AAAA,QACrB,CAAC;AAED,qBAAa,SAAS;AAGtB,cAAM,OAAO,MAAM,SAAS,KAAK;AAGjC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,KAAK,WAAW,SAAS,QAAQ,IAAwB;AAAA,QACjE;AAEA,eAAO;AAAA,MACT,SAAS,OAAO;AACd,oBAAY;AAGZ,YAAI,iBAAiB,cAAc;AACjC,cAAI,MAAM,cAAc,OAAO,MAAM,aAAa,OAAO,MAAM,eAAe,KAAK;AACjF,kBAAM;AAAA,UACR;AAAA,QACF;AAGA,YAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD,gBAAM,IAAI,aAAa,yBAAyB,KAAK,OAAO,IAAI;AAAA,QAClE;AAGA,YAAI,UAAU,KAAK,SAAS;AAC1B,gBAAM,QAAQ,gBAAgB,OAAO;AACrC,gBAAM,MAAM,KAAK;AACjB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,qBAAqB,cAAc;AACrC,YAAM;AAAA,IACR;AACA,UAAM,IAAI,aAAa,WAAW,WAAW,gBAAgB;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAW,YAAoB,MAAsC;AAC3E,UAAM,UAAU,KAAK,OAAO,WAAW;AACvC,UAAM,OAAO,KAAK,OAAO,QAAQ;AACjC,UAAM,YAAY,KAAK,MAAM;AAE7B,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,eAAO,IAAI,oBAAoB,SAAS,SAAS;AAAA,MACnD,KAAK;AACH,eAAO,IAAI,eAAe,SAAS,SAAS;AAAA,MAC9C,KAAK;AACH,eAAO,IAAI,gBAAgB,SAAS,SAAS;AAAA,MAC/C,KAAK;AACH,eAAO,IAAI,cAAc,SAAS,MAAM,SAAS;AAAA,MACnD,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO,IAAI,YAAY,SAAS,SAAS;AAAA,MAC3C;AACE,eAAO,IAAI,aAAa,SAAS,MAAM,YAAY,SAAS;AAAA,IAChE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,IAAO,MAA0B;AACrC,WAAO,KAAK,QAAW,EAAE,QAAQ,OAAO,KAAK,CAAC;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAQ,MAAc,MAA4B;AACtD,WAAO,KAAK,QAAW,EAAE,QAAQ,QAAQ,MAAM,KAAK,CAAC;AAAA,EACvD;AACF;;;ACvIO,IAAM,WAAN,MAAe;AAAA,EACpB,YAA6B,MAAkB;AAAlB;AAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBhD,MAAM,SAAS,SAAqE;AAClF,WAAO,KAAK,KAAK,KAAK,yBAAyB;AAAA,MAC7C,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,MAAM,QAAQ;AAAA,MACd,aAAa,QAAQ;AAAA,MACrB,UAAU,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAM,aAAa,SAAyE;AAC1F,WAAO,KAAK,KAAK,KAAK,6BAA6B;AAAA,MACjD,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,UAAU,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAM,UAAU,SAAsE;AACpF,WAAO,KAAK,KAAK,KAAK,oBAAoB;AAAA,MACxC,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,MACf,SAAS,QAAQ,UAAU,EAAE,YAAY,QAAQ,QAAQ,IAAI;AAAA,IAC/D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UAAU,SAAsE;AACpF,WAAO,KAAK,KAAK,KAAK,oBAAoB;AAAA,MACxC,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,MACf,SAAS,QAAQ,UAAU,EAAE,YAAY,QAAQ,QAAQ,IAAI;AAAA,IAC/D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UAAU,SAAsE;AACpF,WAAO,KAAK,KAAK,KAAK,oBAAoB;AAAA,MACxC,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,MACf,SAAS,QAAQ,UAAU,EAAE,YAAY,QAAQ,QAAQ,IAAI;AAAA,IAC/D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,MAAM,aAAa,SAAyE;AAC1F,WAAO,KAAK,KAAK,KAAK,oBAAoB;AAAA,MACxC,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,UAAU,QAAQ;AAAA,MAClB,SAAS,QAAQ,UAAU,EAAE,YAAY,QAAQ,QAAQ,IAAI;AAAA,IAC/D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BA,MAAM,aAAa,SAAyE;AAC1F,WAAO,KAAK,KAAK,KAAK,oBAAoB;AAAA,MACxC,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,UAAU;AAAA,QACR,YAAY,QAAQ,SAAS;AAAA,QAC7B,OAAO,QAAQ,SAAS;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2BA,MAAM,gBAAgB,SAA4E;AAChG,WAAO,KAAK,KAAK,KAAK,oBAAoB;AAAA,MACxC,iBAAiB,QAAQ;AAAA,MACzB,IAAI,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,aAAa,QAAQ;AAAA,MACrB,SAAS,QAAQ,UAAU,EAAE,YAAY,QAAQ,QAAQ,IAAI;AAAA,IAC/D,CAAC;AAAA,EACH;AACF;;;AC1OO,IAAM,YAAN,MAAgB;AAAA,EACrB,YAA6B,MAAkB;AAAlB;AAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBhD,MAAM,KAAK,SAAsE;AAC/E,QAAI,OAAO;AAEX,QAAI,SAAS,QAAQ;AACnB,cAAQ,WAAW,mBAAmB,QAAQ,MAAM,CAAC;AAAA,IACvD;AAEA,WAAO,KAAK,KAAK,IAAI,IAAI;AAAA,EAC3B;AACF;;;AC5BO,IAAM,eAAN,MAAmB;AAAA,EACxB,YAA6B,MAAkB;AAAlB;AAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAehD,MAAM,OAA4C;AAChD,WAAO,KAAK,KAAK,IAAI,uBAAuB;AAAA,EAC9C;AACF;;;AClBO,IAAM,QAAN,MAAY;AAAA,EACjB,YAA6B,MAAkB;AAAlB;AAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBhD,MAAM,MAAuC;AAC3C,WAAO,KAAK,KAAK,IAAI,eAAe;AAAA,EACtC;AACF;;;ACfO,IAAM,OAAN,MAAW;AAAA,EAChB,YAA6B,MAAkB;AAAlB;AAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBhD,MAAM,eAAe,SAA2E;AAC9F,WAAO,KAAK,KAAK,KAAK,iCAAiC;AAAA,MACrD,iBAAiB,QAAQ;AAAA,MACzB,MAAM,QAAQ,QAAQ;AAAA,MACtB,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ;AAAA,IAChB,CAAC;AAAA,EACH;AACF;;;ACtCA,IAAM,mBAAmB;AACzB,IAAM,kBAAkB;AACxB,IAAM,kBAAkB;AA2BjB,IAAM,gBAAN,MAAoB;AAAA;AAAA,EAEhB;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA,EAEQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBjB,YAAY,iBAAyC;AACnD,UAAM,UAAyB,OAAO,oBAAoB,WACtD,EAAE,QAAQ,gBAAgB,IAC1B;AAGJ,QAAI,CAAC,QAAQ,QAAQ;AACnB,YAAM,IAAI,MAAM,qBAAqB;AAAA,IACvC;AAEA,QAAI,CAAC,QAAQ,OAAO,WAAW,UAAU,KAAK,CAAC,QAAQ,OAAO,WAAW,UAAU,GAAG;AACpF,YAAM,IAAI,MAAM,8DAA8D;AAAA,IAChF;AAGA,SAAK,aAAa,QAAQ,OAAO,WAAW,UAAU;AAGtD,SAAK,OAAO,IAAI,WAAW;AAAA,MACzB,SAAS,QAAQ,WAAW;AAAA,MAC5B,QAAQ,QAAQ;AAAA,MAChB,SAAS,QAAQ,WAAW;AAAA,MAC5B,SAAS,QAAQ,WAAW;AAAA,IAC9B,CAAC;AAGD,SAAK,WAAW,IAAI,SAAS,KAAK,IAAI;AACtC,SAAK,YAAY,IAAI,UAAU,KAAK,IAAI;AACxC,SAAK,eAAe,IAAI,aAAa,KAAK,IAAI;AAC9C,SAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;AAChC,SAAK,OAAO,IAAI,KAAK,KAAK,IAAI;AAAA,EAChC;AACF;;;AC5GA,oBAA4C;AA4DrC,SAAS,uBAAuB,SAAwC;AAC7E,QAAM,EAAE,SAAS,WAAW,WAAW,OAAO,IAAI;AAGlD,MAAI,CAAC,WAAW,CAAC,aAAa,CAAC,aAAa,CAAC,QAAQ;AACnD,WAAO;AAAA,EACT;AAGA,QAAM,eAAe,SAAS,WAAW,EAAE;AAC3C,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,YAAY,IAAI;AAEtB,MAAI,MAAM,YAAY,KAAK,KAAK,IAAI,MAAM,YAAY,IAAI,WAAW;AACnE,WAAO;AAAA,EACT;AAGA,QAAM,gBAAgB,OAAO,YAAY,WACrC,UACA,KAAK,UAAU,OAAO;AAG1B,QAAM,gBAAgB,YAAY;AAClC,QAAM,oBAAoB,gBAAY,0BAAW,UAAU,MAAM,EAC9D,OAAO,aAAa,EACpB,OAAO,KAAK;AAGf,MAAI;AACF,UAAM,YAAY,OAAO,KAAK,SAAS;AACvC,UAAM,iBAAiB,OAAO,KAAK,iBAAiB;AAEpD,QAAI,UAAU,WAAW,eAAe,QAAQ;AAC9C,aAAO;AAAA,IACT;AAEA,eAAO,+BAAgB,WAAW,cAAc;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,oBAAiC,SAA6B;AAC5E,MAAI,OAAO,YAAY,UAAU;AAC/B,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B;AACA,SAAO;AACT;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,599 @@
1
+ // src/errors.ts
2
+ var SembojaError = class _SembojaError extends Error {
3
+ /** Error code from the API */
4
+ code;
5
+ /** HTTP status code */
6
+ statusCode;
7
+ /** Request ID for debugging */
8
+ requestId;
9
+ constructor(message, code, statusCode, requestId) {
10
+ super(message);
11
+ this.name = "SembojaError";
12
+ this.code = code;
13
+ this.statusCode = statusCode;
14
+ this.requestId = requestId;
15
+ Object.setPrototypeOf(this, _SembojaError.prototype);
16
+ }
17
+ };
18
+ var AuthenticationError = class _AuthenticationError extends SembojaError {
19
+ constructor(message, requestId) {
20
+ super(message, "INVALID_API_KEY", 401, requestId);
21
+ this.name = "AuthenticationError";
22
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
23
+ }
24
+ };
25
+ var RateLimitError = class _RateLimitError extends SembojaError {
26
+ /** When the rate limit resets (Unix timestamp) */
27
+ resetAt;
28
+ constructor(message, requestId, resetAt) {
29
+ super(message, "RATE_LIMITED", 429, requestId);
30
+ this.name = "RateLimitError";
31
+ this.resetAt = resetAt;
32
+ Object.setPrototypeOf(this, _RateLimitError.prototype);
33
+ }
34
+ };
35
+ var ValidationError = class _ValidationError extends SembojaError {
36
+ constructor(message, requestId) {
37
+ super(message, "VALIDATION_ERROR", 400, requestId);
38
+ this.name = "ValidationError";
39
+ Object.setPrototypeOf(this, _ValidationError.prototype);
40
+ }
41
+ };
42
+ var NotFoundError = class _NotFoundError extends SembojaError {
43
+ constructor(message, code, requestId) {
44
+ super(message, code, 404, requestId);
45
+ this.name = "NotFoundError";
46
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
47
+ }
48
+ };
49
+ var ServerError = class _ServerError extends SembojaError {
50
+ constructor(message, requestId) {
51
+ super(message, "INTERNAL_ERROR", 500, requestId);
52
+ this.name = "ServerError";
53
+ Object.setPrototypeOf(this, _ServerError.prototype);
54
+ }
55
+ };
56
+ var NetworkError = class _NetworkError extends SembojaError {
57
+ constructor(message) {
58
+ super(message, "NETWORK_ERROR", 0);
59
+ this.name = "NetworkError";
60
+ Object.setPrototypeOf(this, _NetworkError.prototype);
61
+ }
62
+ };
63
+
64
+ // src/lib/http.ts
65
+ function sleep(ms) {
66
+ return new Promise((resolve) => setTimeout(resolve, ms));
67
+ }
68
+ function getBackoffDelay(attempt, baseDelay = 1e3) {
69
+ return Math.min(baseDelay * Math.pow(2, attempt), 3e4);
70
+ }
71
+ var HttpClient = class {
72
+ baseUrl;
73
+ apiKey;
74
+ timeout;
75
+ retries;
76
+ constructor(options) {
77
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
78
+ this.apiKey = options.apiKey;
79
+ this.timeout = options.timeout;
80
+ this.retries = options.retries;
81
+ }
82
+ /**
83
+ * Make an HTTP request with retry logic
84
+ */
85
+ async request(options) {
86
+ const url = `${this.baseUrl}${options.path}`;
87
+ const headers = {
88
+ "Content-Type": "application/json",
89
+ "X-API-Key": this.apiKey,
90
+ ...options.headers
91
+ };
92
+ let lastError = null;
93
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
94
+ try {
95
+ const controller = new AbortController();
96
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
97
+ const response = await fetch(url, {
98
+ method: options.method,
99
+ headers,
100
+ body: options.body ? JSON.stringify(options.body) : void 0,
101
+ signal: controller.signal
102
+ });
103
+ clearTimeout(timeoutId);
104
+ const data = await response.json();
105
+ if (!response.ok) {
106
+ throw this.parseError(response.status, data);
107
+ }
108
+ return data;
109
+ } catch (error) {
110
+ lastError = error;
111
+ if (error instanceof SembojaError) {
112
+ if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
113
+ throw error;
114
+ }
115
+ }
116
+ if (error instanceof Error && error.name === "AbortError") {
117
+ throw new NetworkError(`Request timeout after ${this.timeout}ms`);
118
+ }
119
+ if (attempt < this.retries) {
120
+ const delay = getBackoffDelay(attempt);
121
+ await sleep(delay);
122
+ continue;
123
+ }
124
+ }
125
+ }
126
+ if (lastError instanceof SembojaError) {
127
+ throw lastError;
128
+ }
129
+ throw new NetworkError(lastError?.message || "Request failed");
130
+ }
131
+ /**
132
+ * Parse error response into appropriate error class
133
+ */
134
+ parseError(statusCode, data) {
135
+ const message = data.error?.message || "Unknown error";
136
+ const code = data.error?.code || "UNKNOWN_ERROR";
137
+ const requestId = data.meta?.request_id;
138
+ switch (statusCode) {
139
+ case 401:
140
+ return new AuthenticationError(message, requestId);
141
+ case 429:
142
+ return new RateLimitError(message, requestId);
143
+ case 400:
144
+ return new ValidationError(message, requestId);
145
+ case 404:
146
+ return new NotFoundError(message, code, requestId);
147
+ case 500:
148
+ case 502:
149
+ case 503:
150
+ return new ServerError(message, requestId);
151
+ default:
152
+ return new SembojaError(message, code, statusCode, requestId);
153
+ }
154
+ }
155
+ /**
156
+ * GET request
157
+ */
158
+ async get(path) {
159
+ return this.request({ method: "GET", path });
160
+ }
161
+ /**
162
+ * POST request
163
+ */
164
+ async post(path, body) {
165
+ return this.request({ method: "POST", path, body });
166
+ }
167
+ };
168
+
169
+ // src/resources/messages.ts
170
+ var Messages = class {
171
+ constructor(http) {
172
+ this.http = http;
173
+ }
174
+ /**
175
+ * Send a text message
176
+ *
177
+ * @param options - Text message options
178
+ * @returns Message response with message ID
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const result = await client.messages.sendText({
183
+ * phoneNumberId: '123456789',
184
+ * to: '+6281234567890',
185
+ * text: 'Hello, World!',
186
+ * previewUrl: true,
187
+ * });
188
+ * console.log('Message ID:', result.data.messages[0].id);
189
+ * ```
190
+ */
191
+ async sendText(options) {
192
+ return this.http.post("/api/v1/messages/text", {
193
+ phone_number_id: options.phoneNumberId,
194
+ to: options.to,
195
+ text: options.text,
196
+ preview_url: options.previewUrl,
197
+ reply_to: options.replyTo
198
+ });
199
+ }
200
+ /**
201
+ * Send a template message
202
+ *
203
+ * @param options - Template message options
204
+ * @returns Message response with message ID
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const result = await client.messages.sendTemplate({
209
+ * phoneNumberId: '123456789',
210
+ * to: '+6281234567890',
211
+ * template: {
212
+ * name: 'hello_world',
213
+ * language: { code: 'en' },
214
+ * },
215
+ * });
216
+ * ```
217
+ */
218
+ async sendTemplate(options) {
219
+ return this.http.post("/api/v1/messages/template", {
220
+ phone_number_id: options.phoneNumberId,
221
+ to: options.to,
222
+ template: options.template
223
+ });
224
+ }
225
+ /**
226
+ * Send an image message
227
+ *
228
+ * @param options - Image message options
229
+ * @returns Message response with message ID
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * const result = await client.messages.sendImage({
234
+ * phoneNumberId: '123456789',
235
+ * to: '+6281234567890',
236
+ * image: {
237
+ * link: 'https://example.com/image.jpg',
238
+ * caption: 'Check this out!',
239
+ * },
240
+ * });
241
+ * ```
242
+ */
243
+ async sendImage(options) {
244
+ return this.http.post("/api/v1/messages", {
245
+ phone_number_id: options.phoneNumberId,
246
+ to: options.to,
247
+ type: "image",
248
+ image: options.image,
249
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
250
+ });
251
+ }
252
+ /**
253
+ * Send a video message
254
+ *
255
+ * @param options - Video message options
256
+ * @returns Message response with message ID
257
+ */
258
+ async sendVideo(options) {
259
+ return this.http.post("/api/v1/messages", {
260
+ phone_number_id: options.phoneNumberId,
261
+ to: options.to,
262
+ type: "video",
263
+ video: options.video,
264
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
265
+ });
266
+ }
267
+ /**
268
+ * Send an audio message
269
+ *
270
+ * @param options - Audio message options
271
+ * @returns Message response with message ID
272
+ */
273
+ async sendAudio(options) {
274
+ return this.http.post("/api/v1/messages", {
275
+ phone_number_id: options.phoneNumberId,
276
+ to: options.to,
277
+ type: "audio",
278
+ audio: options.audio,
279
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
280
+ });
281
+ }
282
+ /**
283
+ * Send a document message
284
+ *
285
+ * @param options - Document message options
286
+ * @returns Message response with message ID
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * const result = await client.messages.sendDocument({
291
+ * phoneNumberId: '123456789',
292
+ * to: '+6281234567890',
293
+ * document: {
294
+ * link: 'https://example.com/invoice.pdf',
295
+ * filename: 'invoice.pdf',
296
+ * caption: 'Your invoice',
297
+ * },
298
+ * });
299
+ * ```
300
+ */
301
+ async sendDocument(options) {
302
+ return this.http.post("/api/v1/messages", {
303
+ phone_number_id: options.phoneNumberId,
304
+ to: options.to,
305
+ type: "document",
306
+ document: options.document,
307
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
308
+ });
309
+ }
310
+ /**
311
+ * Send a reaction to a message
312
+ *
313
+ * @param options - Reaction options
314
+ * @returns Message response
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * // Add reaction
319
+ * await client.messages.sendReaction({
320
+ * phoneNumberId: '123456789',
321
+ * to: '+6281234567890',
322
+ * reaction: {
323
+ * messageId: 'wamid.xxx',
324
+ * emoji: '👍',
325
+ * },
326
+ * });
327
+ *
328
+ * // Remove reaction
329
+ * await client.messages.sendReaction({
330
+ * phoneNumberId: '123456789',
331
+ * to: '+6281234567890',
332
+ * reaction: {
333
+ * messageId: 'wamid.xxx',
334
+ * emoji: '',
335
+ * },
336
+ * });
337
+ * ```
338
+ */
339
+ async sendReaction(options) {
340
+ return this.http.post("/api/v1/messages", {
341
+ phone_number_id: options.phoneNumberId,
342
+ to: options.to,
343
+ type: "reaction",
344
+ reaction: {
345
+ message_id: options.reaction.messageId,
346
+ emoji: options.reaction.emoji
347
+ }
348
+ });
349
+ }
350
+ /**
351
+ * Send an interactive message (buttons, lists, etc.)
352
+ *
353
+ * @param options - Interactive message options
354
+ * @returns Message response with message ID
355
+ *
356
+ * @example
357
+ * ```typescript
358
+ * // Button message
359
+ * await client.messages.sendInteractive({
360
+ * phoneNumberId: '123456789',
361
+ * to: '+6281234567890',
362
+ * interactive: {
363
+ * type: 'button',
364
+ * body: { text: 'Choose an option:' },
365
+ * action: {
366
+ * buttons: [
367
+ * { type: 'reply', reply: { id: 'yes', title: 'Yes' } },
368
+ * { type: 'reply', reply: { id: 'no', title: 'No' } },
369
+ * ],
370
+ * },
371
+ * },
372
+ * });
373
+ * ```
374
+ */
375
+ async sendInteractive(options) {
376
+ return this.http.post("/api/v1/messages", {
377
+ phone_number_id: options.phoneNumberId,
378
+ to: options.to,
379
+ type: "interactive",
380
+ interactive: options.interactive,
381
+ context: options.replyTo ? { message_id: options.replyTo } : void 0
382
+ });
383
+ }
384
+ };
385
+
386
+ // src/resources/templates.ts
387
+ var Templates = class {
388
+ constructor(http) {
389
+ this.http = http;
390
+ }
391
+ /**
392
+ * List all message templates
393
+ *
394
+ * @param options - Optional filters
395
+ * @returns List of templates
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * const templates = await client.templates.list();
400
+ * console.log('Templates:', templates.data);
401
+ *
402
+ * // Filter by status
403
+ * const approved = await client.templates.list({ status: 'APPROVED' });
404
+ * ```
405
+ */
406
+ async list(options) {
407
+ let path = "/api/v1/templates";
408
+ if (options?.status) {
409
+ path += `?status=${encodeURIComponent(options.status)}`;
410
+ }
411
+ return this.http.get(path);
412
+ }
413
+ };
414
+
415
+ // src/resources/phone-numbers.ts
416
+ var PhoneNumbers = class {
417
+ constructor(http) {
418
+ this.http = http;
419
+ }
420
+ /**
421
+ * List all phone numbers configured for your account
422
+ *
423
+ * @returns List of phone numbers
424
+ *
425
+ * @example
426
+ * ```typescript
427
+ * const phoneNumbers = await client.phoneNumbers.list();
428
+ * for (const phone of phoneNumbers.data) {
429
+ * console.log(`${phone.verified_name}: ${phone.display_phone_number}`);
430
+ * }
431
+ * ```
432
+ */
433
+ async list() {
434
+ return this.http.get("/api/v1/phone-numbers");
435
+ }
436
+ };
437
+
438
+ // src/resources/usage.ts
439
+ var Usage = class {
440
+ constructor(http) {
441
+ this.http = http;
442
+ }
443
+ /**
444
+ * Get current usage statistics for the billing period
445
+ *
446
+ * @returns Usage statistics
447
+ *
448
+ * @example
449
+ * ```typescript
450
+ * const usage = await client.usage.get();
451
+ * console.log(`Period: ${usage.data.period.start} - ${usage.data.period.end}`);
452
+ * console.log(`Messages sent: ${usage.data.messages.sent}`);
453
+ * console.log(`Messages received: ${usage.data.messages.received}`);
454
+ * console.log(`API calls: ${usage.data.api_calls}`);
455
+ * ```
456
+ */
457
+ async get() {
458
+ return this.http.get("/api/v1/usage");
459
+ }
460
+ };
461
+
462
+ // src/resources/test.ts
463
+ var Test = class {
464
+ constructor(http) {
465
+ this.http = http;
466
+ }
467
+ /**
468
+ * Trigger a test webhook to simulate an incoming message
469
+ *
470
+ * **Note:** This only works with test API keys (sk_test_*)
471
+ *
472
+ * @param options - Webhook trigger options
473
+ * @returns Success response
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * await client.test.triggerWebhook({
478
+ * phoneNumberId: '123456789',
479
+ * type: 'text',
480
+ * from: '+6281234567890',
481
+ * text: 'Hello, this is a test incoming message!',
482
+ * });
483
+ * ```
484
+ */
485
+ async triggerWebhook(options) {
486
+ return this.http.post("/api/v1/test/webhooks/trigger", {
487
+ phone_number_id: options.phoneNumberId,
488
+ type: options.type || "text",
489
+ from: options.from,
490
+ text: options.text
491
+ });
492
+ }
493
+ };
494
+
495
+ // src/client.ts
496
+ var DEFAULT_BASE_URL = "https://connect.semboja.tech";
497
+ var DEFAULT_TIMEOUT = 3e4;
498
+ var DEFAULT_RETRIES = 3;
499
+ var SembojaClient = class {
500
+ /** Messages API */
501
+ messages;
502
+ /** Templates API */
503
+ templates;
504
+ /** Phone Numbers API */
505
+ phoneNumbers;
506
+ /** Usage API */
507
+ usage;
508
+ /** Test API (only works with sk_test_* keys) */
509
+ test;
510
+ /** Whether the client is in test mode */
511
+ isTestMode;
512
+ http;
513
+ /**
514
+ * Create a new Semboja client
515
+ *
516
+ * @param optionsOrApiKey - API key string or client options object
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * // Using API key directly
521
+ * const client = new SembojaClient('sk_live_xxx');
522
+ *
523
+ * // Using options object
524
+ * const client = new SembojaClient({
525
+ * apiKey: 'sk_live_xxx',
526
+ * timeout: 60000,
527
+ * retries: 5,
528
+ * });
529
+ * ```
530
+ */
531
+ constructor(optionsOrApiKey) {
532
+ const options = typeof optionsOrApiKey === "string" ? { apiKey: optionsOrApiKey } : optionsOrApiKey;
533
+ if (!options.apiKey) {
534
+ throw new Error("API key is required");
535
+ }
536
+ if (!options.apiKey.startsWith("sk_live_") && !options.apiKey.startsWith("sk_test_")) {
537
+ throw new Error("Invalid API key format. Must start with sk_live_ or sk_test_");
538
+ }
539
+ this.isTestMode = options.apiKey.startsWith("sk_test_");
540
+ this.http = new HttpClient({
541
+ baseUrl: options.baseUrl || DEFAULT_BASE_URL,
542
+ apiKey: options.apiKey,
543
+ timeout: options.timeout || DEFAULT_TIMEOUT,
544
+ retries: options.retries ?? DEFAULT_RETRIES
545
+ });
546
+ this.messages = new Messages(this.http);
547
+ this.templates = new Templates(this.http);
548
+ this.phoneNumbers = new PhoneNumbers(this.http);
549
+ this.usage = new Usage(this.http);
550
+ this.test = new Test(this.http);
551
+ }
552
+ };
553
+
554
+ // src/webhooks/verify.ts
555
+ import { createHmac, timingSafeEqual } from "crypto";
556
+ function verifyWebhookSignature(options) {
557
+ const { payload, signature, timestamp, secret } = options;
558
+ if (!payload || !signature || !timestamp || !secret) {
559
+ return false;
560
+ }
561
+ const timestampNum = parseInt(timestamp, 10);
562
+ const now = Math.floor(Date.now() / 1e3);
563
+ const tolerance = 5 * 60;
564
+ if (isNaN(timestampNum) || Math.abs(now - timestampNum) > tolerance) {
565
+ return false;
566
+ }
567
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
568
+ const signatureData = timestamp + payloadString;
569
+ const expectedSignature = "sha256=" + createHmac("sha256", secret).update(signatureData).digest("hex");
570
+ try {
571
+ const sigBuffer = Buffer.from(signature);
572
+ const expectedBuffer = Buffer.from(expectedSignature);
573
+ if (sigBuffer.length !== expectedBuffer.length) {
574
+ return false;
575
+ }
576
+ return timingSafeEqual(sigBuffer, expectedBuffer);
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+ function parseWebhookPayload(payload) {
582
+ if (typeof payload === "string") {
583
+ return JSON.parse(payload);
584
+ }
585
+ return payload;
586
+ }
587
+ export {
588
+ AuthenticationError,
589
+ NetworkError,
590
+ NotFoundError,
591
+ RateLimitError,
592
+ SembojaClient,
593
+ SembojaError,
594
+ ServerError,
595
+ ValidationError,
596
+ parseWebhookPayload,
597
+ verifyWebhookSignature
598
+ };
599
+ //# sourceMappingURL=index.mjs.map