UNPKG

@elusion-sdk/briq

Version:

A modern TypeScript SDK for Briq SMS API integration

1,398 lines (1,369 loc) 43.7 kB
import { createRequire } from "node:module"; var __create = Object.create; var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __toESM = (mod, isNodeMode, target) => { target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { get: () => mod[key], enumerable: true }); return to; }; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); var __require = /* @__PURE__ */ createRequire(import.meta.url); // node_modules/dotenv/package.json var require_package = __commonJS((exports, module) => { module.exports = { name: "dotenv", version: "16.5.0", description: "Loads environment variables from .env file", main: "lib/main.js", types: "lib/main.d.ts", exports: { ".": { types: "./lib/main.d.ts", require: "./lib/main.js", default: "./lib/main.js" }, "./config": "./config.js", "./config.js": "./config.js", "./lib/env-options": "./lib/env-options.js", "./lib/env-options.js": "./lib/env-options.js", "./lib/cli-options": "./lib/cli-options.js", "./lib/cli-options.js": "./lib/cli-options.js", "./package.json": "./package.json" }, scripts: { "dts-check": "tsc --project tests/types/tsconfig.json", lint: "standard", pretest: "npm run lint && npm run dts-check", test: "tap run --allow-empty-coverage --disable-coverage --timeout=60000", "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=lcov", prerelease: "npm test", release: "standard-version" }, repository: { type: "git", url: "git://github.com/motdotla/dotenv.git" }, homepage: "https://github.com/motdotla/dotenv#readme", funding: "https://dotenvx.com", keywords: [ "dotenv", "env", ".env", "environment", "variables", "config", "settings" ], readmeFilename: "README.md", license: "BSD-2-Clause", devDependencies: { "@types/node": "^18.11.3", decache: "^4.6.2", sinon: "^14.0.1", standard: "^17.0.0", "standard-version": "^9.5.0", tap: "^19.2.0", typescript: "^4.8.4" }, engines: { node: ">=12" }, browser: { fs: false } }; }); // node_modules/dotenv/lib/main.js var require_main = __commonJS((exports, module) => { var fs = __require("fs"); var path = __require("path"); var os = __require("os"); var crypto = __require("crypto"); var packageJson = require_package(); var version = packageJson.version; var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg; function parse(src) { const obj = {}; let lines = src.toString(); lines = lines.replace(/\r\n?/mg, ` `); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; let value = match[2] || ""; value = value.trim(); const maybeQuote = value[0]; value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2"); if (maybeQuote === '"') { value = value.replace(/\\n/g, ` `); value = value.replace(/\\r/g, "\r"); } obj[key] = value; } return obj; } function _parseVault(options) { const vaultPath = _vaultPath(options); const result = DotenvModule.configDotenv({ path: vaultPath }); if (!result.parsed) { const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); err.code = "MISSING_DATA"; throw err; } const keys = _dotenvKey(options).split(","); const length = keys.length; let decrypted; for (let i = 0;i < length; i++) { try { const key = keys[i].trim(); const attrs = _instructions(result, key); decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); break; } catch (error) { if (i + 1 >= length) { throw error; } } } return DotenvModule.parse(decrypted); } function _warn(message) { console.log(`[dotenv@${version}][WARN] ${message}`); } function _debug(message) { console.log(`[dotenv@${version}][DEBUG] ${message}`); } function _dotenvKey(options) { if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { return options.DOTENV_KEY; } if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { return process.env.DOTENV_KEY; } return ""; } function _instructions(result, dotenvKey) { let uri; try { uri = new URL(dotenvKey); } catch (error) { if (error.code === "ERR_INVALID_URL") { const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); err.code = "INVALID_DOTENV_KEY"; throw err; } throw error; } const key = uri.password; if (!key) { const err = new Error("INVALID_DOTENV_KEY: Missing key part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environment = uri.searchParams.get("environment"); if (!environment) { const err = new Error("INVALID_DOTENV_KEY: Missing environment part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; const ciphertext = result.parsed[environmentKey]; if (!ciphertext) { const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; throw err; } return { ciphertext, key }; } function _vaultPath(options) { let possibleVaultPath = null; if (options && options.path && options.path.length > 0) { if (Array.isArray(options.path)) { for (const filepath of options.path) { if (fs.existsSync(filepath)) { possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; } } } else { possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; } } else { possibleVaultPath = path.resolve(process.cwd(), ".env.vault"); } if (fs.existsSync(possibleVaultPath)) { return possibleVaultPath; } return null; } function _resolveHome(envPath) { return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath; } function _configVault(options) { const debug = Boolean(options && options.debug); if (debug) { _debug("Loading env from encrypted .env.vault"); } const parsed = DotenvModule._parseVault(options); let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsed, options); return { parsed }; } function configDotenv(options) { const dotenvPath = path.resolve(process.cwd(), ".env"); let encoding = "utf8"; const debug = Boolean(options && options.debug); if (options && options.encoding) { encoding = options.encoding; } else { if (debug) { _debug("No encoding is specified. UTF-8 is used by default"); } } let optionPaths = [dotenvPath]; if (options && options.path) { if (!Array.isArray(options.path)) { optionPaths = [_resolveHome(options.path)]; } else { optionPaths = []; for (const filepath of options.path) { optionPaths.push(_resolveHome(filepath)); } } } let lastError; const parsedAll = {}; for (const path2 of optionPaths) { try { const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding })); DotenvModule.populate(parsedAll, parsed, options); } catch (e) { if (debug) { _debug(`Failed to load ${path2} ${e.message}`); } lastError = e; } } let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsedAll, options); if (lastError) { return { parsed: parsedAll, error: lastError }; } else { return { parsed: parsedAll }; } } function config(options) { if (_dotenvKey(options).length === 0) { return DotenvModule.configDotenv(options); } const vaultPath = _vaultPath(options); if (!vaultPath) { _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`); return DotenvModule.configDotenv(options); } return DotenvModule._configVault(options); } function decrypt(encrypted, keyStr) { const key = Buffer.from(keyStr.slice(-64), "hex"); let ciphertext = Buffer.from(encrypted, "base64"); const nonce = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(-16); ciphertext = ciphertext.subarray(12, -16); try { const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); aesgcm.setAuthTag(authTag); return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; } catch (error) { const isRange = error instanceof RangeError; const invalidKeyLength = error.message === "Invalid key length"; const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; if (isRange || invalidKeyLength) { const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); err.code = "INVALID_DOTENV_KEY"; throw err; } else if (decryptionFailed) { const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); err.code = "DECRYPTION_FAILED"; throw err; } else { throw error; } } } function populate(processEnv, parsed, options = {}) { const debug = Boolean(options && options.debug); const override = Boolean(options && options.override); if (typeof parsed !== "object") { const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); err.code = "OBJECT_REQUIRED"; throw err; } for (const key of Object.keys(parsed)) { if (Object.prototype.hasOwnProperty.call(processEnv, key)) { if (override === true) { processEnv[key] = parsed[key]; } if (debug) { if (override === true) { _debug(`"${key}" is already defined and WAS overwritten`); } else { _debug(`"${key}" is already defined and was NOT overwritten`); } } } else { processEnv[key] = parsed[key]; } } } var DotenvModule = { configDotenv, _configVault, _parseVault, config, decrypt, parse, populate }; exports.configDotenv = DotenvModule.configDotenv; exports._configVault = DotenvModule._configVault; exports._parseVault = DotenvModule._parseVault; exports.config = DotenvModule.config; exports.decrypt = DotenvModule.decrypt; exports.parse = DotenvModule.parse; exports.populate = DotenvModule.populate; module.exports = DotenvModule; }); // node_modules/dotenv/lib/env-options.js var require_env_options = __commonJS((exports, module) => { var options = {}; if (process.env.DOTENV_CONFIG_ENCODING != null) { options.encoding = process.env.DOTENV_CONFIG_ENCODING; } if (process.env.DOTENV_CONFIG_PATH != null) { options.path = process.env.DOTENV_CONFIG_PATH; } if (process.env.DOTENV_CONFIG_DEBUG != null) { options.debug = process.env.DOTENV_CONFIG_DEBUG; } if (process.env.DOTENV_CONFIG_OVERRIDE != null) { options.override = process.env.DOTENV_CONFIG_OVERRIDE; } if (process.env.DOTENV_CONFIG_DOTENV_KEY != null) { options.DOTENV_KEY = process.env.DOTENV_CONFIG_DOTENV_KEY; } module.exports = options; }); // node_modules/dotenv/lib/cli-options.js var require_cli_options = __commonJS((exports, module) => { var re = /^dotenv_config_(encoding|path|debug|override|DOTENV_KEY)=(.+)$/; module.exports = function optionMatcher(args) { return args.reduce(function(acc, cur) { const matches = cur.match(re); if (matches) { acc[matches[1]] = matches[2]; } return acc; }, {}); }; }); // node_modules/dotenv/config.js var require_config = __commonJS(() => { (function() { require_main().config(Object.assign({}, require_env_options(), require_cli_options()(process.argv))); })(); }); // src/client/BaseClient.ts class BaseClient { config; defaultHeaders; constructor(config) { this.config = { baseUrl: "https://karibu.briq.tz", timeout: 30000, retries: 3, version: "v1", ...config }; this.defaultHeaders = { "Content-Type": "application/json", "X-API-Key": this.config.apiKey, "User-Agent": "Briq-SDK-TS/1.0.0" }; } async request(config) { const url = this.buildUrl(config.url); const headers = { ...this.defaultHeaders, ...config.headers }; try { const response = await this.executeRequest({ ...config, url, headers }); return this.handleResponse(response); } catch (error) { throw this.handleError(error); } } buildUrl(endpoint) { const cleanEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint; return `${this.config.baseUrl}/${this.config.version}/${cleanEndpoint}`; } async get(url, config) { return this.request({ method: "GET", url, ...config }); } async post(url, data, config) { return this.request({ method: "POST", url, data, ...config }); } async patch(url, data, config) { return this.request({ method: "PATCH", url, data, ...config }); } async put(url, data, config) { return this.request({ method: "PUT", url, data, ...config }); } async delete(url, config) { return this.request({ method: "DELETE", url, ...config }); } } // src/utils/errors.ts class BriqError extends Error { code; statusCode; details; constructor(message, code, statusCode, details) { super(message); this.name = this.constructor.name; this.code = code; this.statusCode = statusCode; this.details = details; if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } } class AuthenticationError extends BriqError { constructor(message = "Authentication failed", details) { super(message, "AUTHENTICATION_ERROR", 401, details); } } class AuthorizationError extends BriqError { constructor(message = "Insufficient permissions", details) { super(message, "AUTHORIZATION_ERROR", 403, details); } } class ValidationError extends BriqError { constructor(message, details) { super(message, "VALIDATION_ERROR", 400, details); } } class NotFoundError extends BriqError { constructor(resource, id) { const message = id ? `${resource} with ID '${id}' not found` : `${resource} not found`; super(message, "NOT_FOUND_ERROR", 404, { resource, id }); } } class RateLimitError extends BriqError { constructor(message = "Rate limit exceeded", retryAfter) { super(message, "RATE_LIMIT_ERROR", 429, { retryAfter }); } } class NetworkError extends BriqError { constructor(message = "Network error occurred", details) { super(message, "NETWORK_ERROR", undefined, details); } } class ServerError extends BriqError { constructor(message = "Internal server error", statusCode = 500, details) { super(message, "SERVER_ERROR", statusCode, details); } } class ConfigurationError extends BriqError { constructor(message, details) { super(message, "CONFIGURATION_ERROR", undefined, details); } } class TimeoutError extends BriqError { constructor(timeout) { super(`Request timed out after ${timeout}ms`, "TIMEOUT_ERROR", 408, { timeout }); } } // src/config/defaults.ts var import_config = __toESM(require_config(), 1); function getDefaultsFromEnv() { return { apiKey: process.env["BRIQ_API_KEY"] || "", senderId: process.env["BRIQ_SENDER_ID"] || "", baseUrl: process.env["BRIQ_BASE_URL"] || "https://karibu.briq.tz", version: process.env["BRIQ_API_VERSION"] || "v1", timeout: parseInt(process.env["BRIQ_TIMEOUT"] || "5000", 10) }; } // src/utils/constants.ts var API_CONFIG = { API_KEY: getDefaultsFromEnv().apiKey, BASE_URL: getDefaultsFromEnv().baseUrl, VERSION: getDefaultsFromEnv().version, DEFAULT_TIMEOUT: getDefaultsFromEnv().timeout, DEFAULT_RETRIES: 3, USER_AGENT: "Briq-SDK-TS" }; var ENDPOINTS = { MESSAGES: { SEND_INSTANT: "message/send-instant", SEND_CAMPAIGN: "message/send-campaign", LOGS: "message/logs", HISTORY: "message/history" }, WORKSPACES: { CREATE: "workspace/create/", GET_ALL: "workspace/all/", GET_BY_ID: (id) => `workspace/${id}`, UPDATE: (id) => `workspace/update/${id}` }, CAMPAIGNS: { CREATE: "campaign/create/", GET_ALL: "campaign/all/", GET_BY_ID: (id) => `campaign/${id}/`, UPDATE: (id) => `campaign/update/${id}` } }; var HTTP_STATUS = { OK: 200, CREATED: 201, NO_CONTENT: 204, BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, METHOD_NOT_ALLOWED: 405, CONFLICT: 409, UNPROCESSABLE_ENTITY: 422, TOO_MANY_REQUESTS: 429, INTERNAL_SERVER_ERROR: 500, BAD_GATEWAY: 502, SERVICE_UNAVAILABLE: 503, GATEWAY_TIMEOUT: 504 }; var VALIDATION_PATTERNS = { PHONE_NUMBER: /^\+?[1-9]\d{1,14}$/, EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, API_KEY: /^[a-zA-Z0-9_-]{16,}$/ }; var PAGINATION_DEFAULTS = { PAGE: 1, LIMIT: 20, MAX_LIMIT: 100 }; var RETRY_CONFIG = { MAX_RETRIES: 3, INITIAL_DELAY: 1000, MAX_DELAY: 1e4, BACKOFF_FACTOR: 2 }; // src/utils/helpers.ts function formatPhoneNumber(phoneNumber) { let cleaned = phoneNumber.replace(/[^\d+]/g, ""); if (!cleaned.startsWith("+")) { cleaned = `+${cleaned}`; } return cleaned; } function formatPhoneNumbers(phoneNumbers) { return phoneNumbers.map(formatPhoneNumber); } function calculateSMSSegments(message) { const basicLength = 160; const unicodeLength = 70; const hasUnicode = /[^\x00-\x7F]/.test(message); const segmentLength = hasUnicode ? unicodeLength : basicLength; return Math.ceil(message.length / segmentLength); } function normalizePaginationParams(params) { const page = Math.max(1, params.page || PAGINATION_DEFAULTS.PAGE); const limit = Math.min(PAGINATION_DEFAULTS.MAX_LIMIT, Math.max(1, params.limit || PAGINATION_DEFAULTS.LIMIT)); const offset = params.offset || (page - 1) * limit; return { page, limit, offset }; } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function calculateBackoffDelay(attempt, baseDelay = 1000, maxDelay = 1e4, factor = 2) { const delay2 = baseDelay * Math.pow(factor, attempt - 1); return Math.min(delay2, maxDelay); } function isRetryableError(error) { if (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT_ERROR") { return true; } if (error.statusCode) { return error.statusCode >= 500 || error.statusCode === 429; } return false; } function deepClone(obj) { if (obj === null || typeof obj !== "object") { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (Array.isArray(obj)) { return obj.map((item) => deepClone(item)); } const cloned = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } // src/client/HttpClient.ts class ConsoleLogger { error(message, meta) { console.error(`[BRIQ-SDK-ERROR] ${message}`, meta || ""); } warn(message, meta) { console.warn(`[BRIQ-SDK-WARN] ${message}`, meta || ""); } info(message, meta) { console.info(`[BRIQ-SDK-INFO] ${message}`, meta || ""); } debug(message, meta) { console.debug(`[BRIQ-SDK-DEBUG] ${message}`, meta || ""); } } class HttpClient extends BaseClient { logger; constructor(config) { super(config); this.logger = config.logger || new ConsoleLogger; } async executeRequest(config) { const controller = new AbortController; const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); try { const fetchConfig = { method: config.method, signal: controller.signal }; if (config.headers) { fetchConfig.headers = config.headers; } if (config.data && config.method !== "GET") { fetchConfig.body = JSON.stringify(config.data); } let url = config.url; if (config.params && Object.keys(config.params).length > 0) { const searchParams = new URLSearchParams; Object.entries(config.params).forEach(([key, value]) => { if (value !== undefined && value !== null) { searchParams.append(key, String(value)); } }); url += `?${searchParams.toString()}`; } const response = await fetch(url, fetchConfig); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error.name === "AbortError") { const timeoutError = new TimeoutError(this.config.timeout); this.logger.error("Request timeout", { timeout: this.config.timeout, url: config.url, errorCode: timeoutError.code }); throw timeoutError; } const networkError = new NetworkError("Network request failed", { originalError: error.message }); throw networkError; } } async handleResponse(response) { let responseData; try { const text = await response.text(); responseData = text ? JSON.parse(text) : {}; } catch { throw new ServerError("Invalid JSON response from server", response.status); } if (response.ok) { if (responseData.success !== undefined) { return responseData; } else { return { success: true, data: responseData }; } } throw this.createErrorFromResponse(response, responseData); } handleError(error) { if (error.name?.includes("Error")) { return error; } return new NetworkError("Unexpected error occurred", { originalError: error }); } createErrorFromResponse(response, data) { const message = data?.message || data?.error || `HTTP ${response.status} error`; const details = { statusCode: response.status, statusText: response.statusText, response: data }; switch (response.status) { case HTTP_STATUS.BAD_REQUEST: return new ValidationError(message, details); case HTTP_STATUS.UNAUTHORIZED: return new AuthenticationError(message, details); case HTTP_STATUS.FORBIDDEN: return new AuthorizationError(message, details); case HTTP_STATUS.NOT_FOUND: return new NotFoundError("Resource", undefined); case HTTP_STATUS.TOO_MANY_REQUESTS: const retryAfter = response.headers.get("Retry-After"); return new RateLimitError(message, retryAfter ? parseInt(retryAfter) : undefined); case HTTP_STATUS.INTERNAL_SERVER_ERROR: case HTTP_STATUS.BAD_GATEWAY: case HTTP_STATUS.SERVICE_UNAVAILABLE: case HTTP_STATUS.GATEWAY_TIMEOUT: return new ServerError(message, response.status, details); default: return new ServerError(message, response.status, details); } } async request(config) { let lastError; for (let attempt = 1;attempt <= this.config.retries; attempt++) { try { const response = await super.request(config); return response; } catch (error) { lastError = error; if (attempt === this.config.retries || !isRetryableError(error)) { if (attempt === this.config.retries) { this.logger.error("All retry attempts exhausted", { method: config.method, url: config.url, totalAttempts: this.config.retries, finalErrorCode: error instanceof BriqError ? error.code : "UNKNOWN" }); } else { this.logger.info("Error is not retryable, failing immediately", { method: config.method, url: config.url, errorCode: error instanceof BriqError ? error.code : "UNKNOWN", details: error.details || "" }); } break; } const delayMs = calculateBackoffDelay(attempt, RETRY_CONFIG.INITIAL_DELAY); await delay(delayMs); } } throw lastError; } getLogger() { return this.logger; } setLogger(logger) { this.logger = logger; this.logger.info("Custom logger set for HTTP client"); } } // src/utils/validators.ts function validateApiKey(apiKey) { if (!apiKey || typeof apiKey !== "string") { throw new ValidationError("API key is required and must be a string"); } if (!VALIDATION_PATTERNS.API_KEY.test(apiKey)) { throw new ValidationError("Invalid API key format"); } } function validatePhoneNumber(phoneNumber) { if (!phoneNumber || typeof phoneNumber !== "string") { throw new ValidationError("Phone number is required and must be a string"); } const cleanNumber = phoneNumber.replace(/[\s-]/g, ""); if (!VALIDATION_PATTERNS.PHONE_NUMBER.test(cleanNumber)) { throw new ValidationError(`Invalid phone number format: ${phoneNumber}`); } } function validatePhoneNumbers(phoneNumbers) { if (!Array.isArray(phoneNumbers) || phoneNumbers.length === 0) { throw new ValidationError("Phone numbers must be a non-empty array"); } phoneNumbers.forEach((phoneNumber, index) => { try { validatePhoneNumber(phoneNumber); } catch { throw new ValidationError(`Invalid phone number at index ${index}: ${phoneNumber}`); } }); } function validateMessage(message, maxLength = 1600) { if (!message || typeof message !== "string") { throw new ValidationError("Message is required and must be a string"); } if (message.trim().length === 0) { throw new ValidationError("Message cannot be empty"); } if (message.length > maxLength) { throw new ValidationError(`Message too long. Maximum ${maxLength} characters allowed`); } } function validateWorkspaceName(name) { if (!name || typeof name !== "string") { throw new ValidationError("Workspace name is required and must be a string"); } if (name.trim().length === 0) { throw new ValidationError("Workspace name cannot be empty"); } if (name.length > 100) { throw new ValidationError("Workspace name too long. Maximum 100 characters allowed"); } } function validateCampaignName(name) { if (!name || typeof name !== "string") { throw new ValidationError("Campaign name is required and must be a string"); } if (name.trim().length === 0) { throw new ValidationError("Campaign name cannot be empty"); } if (name.length > 150) { throw new ValidationError("Campaign name too long. Maximum 150 characters allowed"); } } function validateUUID(id, fieldName = "ID") { if (!id || typeof id !== "string") { throw new ValidationError(`${fieldName} is required and must be a string`); } if (!VALIDATION_PATTERNS.UUID.test(id)) { throw new ValidationError(`Invalid ${fieldName} format`); } } function validatePaginationParams(params) { if (params.page !== undefined) { if (!Number.isInteger(params.page) || params.page < 1) { throw new ValidationError("Page must be a positive integer"); } } if (params.limit !== undefined) { if (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 100) { throw new ValidationError("Limit must be an integer between 1 and 100"); } } if (params.offset !== undefined) { if (!Number.isInteger(params.offset) || params.offset < 0) { throw new ValidationError("Offset must be a non-negative integer"); } } } function validateISODate(dateString, fieldName = "Date") { if (!dateString || typeof dateString !== "string") { throw new ValidationError(`${fieldName} is required and must be a string`); } const date = new Date(dateString); if (isNaN(date.getTime())) { throw new ValidationError(`Invalid ${fieldName} format. Must be a valid ISO date string`); } if (date <= new Date) { throw new ValidationError(`${fieldName} must be in the future`); } } // src/services/BaseService.ts class BaseService { client; constructor(client) { this.client = client; } validateRequired(params, requiredFields) { const missing = requiredFields.filter((field) => { const value = params[field]; return value === undefined || value === null || value === ""; }); if (missing.length > 0) { throw new Error(`Missing required parameters: ${missing.join(", ")}`); } } sanitizeInput(input) { if (typeof input === "object" && input !== null) { const sanitized = {}; for (const [key, value] of Object.entries(input)) { if (value !== undefined) { if (typeof value === "string") { sanitized[key] = value.trim(); } else { sanitized[key] = value; } } } return sanitized; } return input; } formatError(error) { if (error.response?.data?.message) { return error.response.data.message; } if (error.message) { return error.message; } return "An unexpected error occurred"; } } // src/services/WorkspaceService.ts class WorkspaceService extends BaseService { async create(request) { this.validateRequired(request, ["name"]); validateWorkspaceName(request.name); if (request.description && request.description.length > 500) { throw new ValidationError("Description too long. Maximum 500 characters allowed"); } const sanitizedRequest = this.sanitizeInput(request); try { return await this.client.post(ENDPOINTS.WORKSPACES.CREATE, sanitizedRequest); } catch (error) { throw new Error(`Failed to create workspace: ${this.formatError(error)}`); } } async list(params = {}) { if (Object.keys(params).length > 0) { validatePaginationParams(params); } if (params.search) { params.search = params.search.trim(); if (params.search.length > 100) { throw new ValidationError("Search term too long. Maximum 100 characters allowed"); } } try { const response = await this.client.get(ENDPOINTS.WORKSPACES.GET_ALL, { params }); if (response.data && Array.isArray(response.data)) { return { success: true, data: response.data, pagination: { page: params.page || 1, limit: params.limit || 20, total: response.data.length, totalPages: Math.ceil(response.data.length / (params.limit || 20)), hasNext: false, hasPrev: (params.page || 1) > 1 } }; } return response; } catch (error) { throw new Error(`Failed to list workspaces: ${this.formatError(error)}`); } } async getById(workspaceId) { validateUUID(workspaceId, "Workspace ID"); try { return await this.client.get(ENDPOINTS.WORKSPACES.GET_BY_ID(workspaceId)); } catch (error) { if (error.statusCode === 404) { throw new NotFoundError("Workspace", workspaceId); } throw new Error(`Failed to get workspace: ${this.formatError(error)}`); } } async update(workspaceId, request) { validateUUID(workspaceId, "Workspace ID"); if (Object.keys(request).length === 0) { throw new ValidationError("Update request cannot be empty"); } if (request.name !== undefined) { validateWorkspaceName(request.name); } if (request.description && request.description.length > 500) { throw new ValidationError("Description too long. Maximum 500 characters allowed"); } const sanitizedRequest = this.sanitizeInput(request); try { return await this.client.patch(ENDPOINTS.WORKSPACES.UPDATE(workspaceId), sanitizedRequest); } catch (error) { if (error.statusCode === 404) { throw new NotFoundError("Workspace", workspaceId); } throw new Error(`Failed to update workspace: ${this.formatError(error)}`); } } async exists(workspaceId) { try { await this.getById(workspaceId); return true; } catch (error) { if (error instanceof NotFoundError) { return false; } throw error; } } } // src/services/MessageService.ts class MessageService extends BaseService { senderId; async sendInstant(request) { this.validateRequired(request, ["recipients", "content"]); const recipients = Array.isArray(request.recipients) ? request.recipients : [request.recipients]; validatePhoneNumbers(recipients); validateMessage(request.content); const formattedRecipients = formatPhoneNumbers(recipients); if (!request.sender_id) { if (!this.senderId) { this.senderId = getDefaultsFromEnv().senderId; } if (!this.senderId) { throw new ValidationError("Sender ID is required, set BRIQ_SENDER_ID env variable or pass it in the request"); } } const senderId = request.sender_id !== undefined ? request.sender_id : this.senderId; const sanitizedRequest = { content: request.content, recipients: formattedRecipients, sender_id: senderId }; if (request.campaign_id) { sanitizedRequest.campaign_id = request.campaign_id; } try { return await this.client.post(ENDPOINTS.MESSAGES.SEND_INSTANT, sanitizedRequest); } catch (error) { throw new Error(`Failed to send instant message: ${this.formatError(error)}`); } } async sendCampaign(request) { this.validateRequired(request, ["campaign_id"]); validateUUID(request.campaign_id, "Campaign ID"); const sanitizedRequest = this.sanitizeInput(request); try { return await this.client.post(ENDPOINTS.MESSAGES.SEND_CAMPAIGN, sanitizedRequest); } catch (error) { throw new Error(`Failed to send campaign message: ${this.formatError(error)}`); } } async getLogs(params = {}) { if (Object.keys(params).length > 0) { validatePaginationParams(params); } if (params.campaign_id) { validateUUID(params.campaign_id, "Campaign ID"); } if (params.status && !["pending", "sent", "delivered", "failed", "cancelled"].includes(params.status)) { throw new ValidationError("Invalid status. Must be one of: pending, sent, delivered, failed, cancelled"); } if (params.phoneNumber !== undefined) { const formatted = formatPhoneNumbers([params.phoneNumber])[0]; if (formatted !== undefined) { params.phoneNumber = formatted; } } try { return await this.client.get(ENDPOINTS.MESSAGES.LOGS, { params }); } catch (error) { throw new Error(`Failed to get message logs: ${this.formatError(error)}`); } } async getHistory(params = {}) { if (Object.keys(params).length > 0) { validatePaginationParams(params); } if (params.workspace_id) { validateUUID(params.workspace_id, "Workspace ID"); } if (params.phoneNumber) { const formatted = formatPhoneNumbers([params.phoneNumber])[0]; if (formatted !== undefined) { params.phoneNumber = formatted; } else { delete params.phoneNumber; } } try { return await this.client.get(ENDPOINTS.MESSAGES.HISTORY, { params }); } catch (error) { throw new Error(`Failed to get message history: ${this.formatError(error)}`); } } } // src/services/CampaignService.ts class CampaignService extends BaseService { async create(request) { this.validateRequired(request, ["name", "workspace_id", "launch_date"]); validateCampaignName(request.name); validateUUID(request.workspace_id, "Workspace ID"); if (request.description && request.description.length > 500) { throw new ValidationError("Description too long. Maximum 500 characters allowed"); } const sanitizedRequest = { ...this.sanitizeInput(request), settings: { sendRate: 60, retryFailures: true, maxRetries: 3, stopOnFailure: false, trackClicks: false, trackReplies: false } }; try { return await this.client.post(ENDPOINTS.CAMPAIGNS.CREATE, sanitizedRequest); } catch (error) { throw new Error(`Failed to create campaign: ${this.formatError(error)}`); } } async list(params = {}) { if (Object.keys(params).length > 0) { validatePaginationParams(params); } if (params.workspace_id) { validateUUID(params.workspace_id, "Workspace ID"); } if (params.search) { params.search = params.search.trim(); if (params.search.length > 100) { throw new ValidationError("Search term too long. Maximum 100 characters allowed"); } } try { const response = await this.client.get(ENDPOINTS.CAMPAIGNS.GET_ALL, { params }); if (response.data && Array.isArray(response.data)) { return { success: true, data: response.data, pagination: { page: params.page || 1, limit: params.limit || 20, total: response.data.length, totalPages: Math.ceil(response.data.length / (params.limit || 20)), hasNext: false, hasPrev: (params.page || 1) > 1 } }; } return response; } catch (error) { throw new Error(`Failed to list campaigns: ${this.formatError(error)}`); } } async getById(campaignId) { validateUUID(campaignId, "Campaign ID"); try { return await this.client.get(ENDPOINTS.CAMPAIGNS.GET_BY_ID(campaignId)); } catch (error) { if (error.statusCode === 404) { throw new NotFoundError("Campaign", campaignId); } throw new Error(`Failed to get campaign: ${this.formatError(error)}`); } } async update(campaignId, request) { validateUUID(campaignId, "Campaign ID"); if (Object.keys(request).length === 0) { throw new ValidationError("Update request cannot be empty"); } if (request.name !== undefined) { validateCampaignName(request.name); } if (request.launch_date !== undefined) { validateISODate(request.launch_date, "Schedule date"); } if (request.description && request.description.length > 500) { throw new ValidationError("Description too long. Maximum 500 characters allowed"); } const sanitizedRequest = this.sanitizeInput(request); try { return await this.client.patch(ENDPOINTS.CAMPAIGNS.UPDATE(campaignId), sanitizedRequest); } catch (error) { if (error.statusCode === 404) { throw new NotFoundError("Campaign", campaignId); } throw new Error(`Failed to update campaign: ${this.formatError(error)}`); } } async delete(campaignId) { validateUUID(campaignId, "Campaign ID"); try { return await this.client.delete(`campaign/${campaignId}`); } catch (error) { if (error.statusCode === 404) { throw new NotFoundError("Campaign", campaignId); } throw new Error(`Failed to delete campaign: ${this.formatError(error)}`); } } async exists(campaignId) { try { await this.getById(campaignId); return true; } catch (error) { if (error instanceof NotFoundError) { return false; } throw error; } } } // src/client/BriqClient.ts class Briq { httpClient; workspaces; messages; campaigns; constructor(config) { this.validateConfig(config); this.httpClient = new HttpClient({ baseUrl: API_CONFIG.BASE_URL, timeout: API_CONFIG.DEFAULT_TIMEOUT, retries: API_CONFIG.DEFAULT_RETRIES, version: API_CONFIG.VERSION, ...config }); this.workspaces = new WorkspaceService(this.httpClient); this.messages = new MessageService(this.httpClient); this.campaigns = new CampaignService(this.httpClient); } validateConfig(config) { if (!config) { throw new ConfigurationError("Configuration object is required"); } try { validateApiKey(config.apiKey); } catch (error) { throw new ConfigurationError(`Invalid API key: ${error.message}`); } if (config.baseUrl && typeof config.baseUrl !== "string") { throw new ConfigurationError("Base URL must be a string"); } if (config.timeout && (!Number.isInteger(config.timeout) || config.timeout <= 0)) { throw new ConfigurationError("Timeout must be a positive integer"); } if (config.retries && (!Number.isInteger(config.retries) || config.retries < 0)) { throw new ConfigurationError("Retries must be a non-negative integer"); } } async testConnection() { try { await this.workspaces.list(); return true; } catch { return false; } } getConfig() { return { baseUrl: this.httpClient["config"].baseUrl, timeout: this.httpClient["config"].timeout, retries: this.httpClient["config"].retries, version: this.httpClient["config"].version }; } } // src/factory.ts function briq() { const apiKey = getDefaultsFromEnv().apiKey; validateApiKey(apiKey); return new Briq({ apiKey }); } // src/index.ts var src_default = Briq; function createClient(config) { return new Briq(config); } var VERSION = "0.1.0"; var SDK_INFO = { name: "briq-sdk", packageName: "@elusion-sdk/briq", version: VERSION, description: "TypeScript/JavaScript SDK for Karibu Briq SMS API", author: "Elusion Lab <elusion.lab@gmail.com>", maintainers: [ "Elusion Lab <elusion.lab@gmail.com>", "Eric Kweyunga <maverickweyunga@gmail.com>", "Briq Team <sms@briq.tz>" ], license: "MIT", repository: "https://github.com/elusionhub/briq-sdk.git", documentation: "https://briq.tz/documentation/home", apiVersion: "v1" }; export { validateWorkspaceName, validateUUID, validatePhoneNumbers, validatePhoneNumber, validatePaginationParams, validateMessage, validateISODate, validateCampaignName, validateApiKey, normalizePaginationParams, isRetryableError, formatPhoneNumbers, formatPhoneNumber, delay, src_default as default, deepClone, createClient, calculateSMSSegments, calculateBackoffDelay, briq, WorkspaceService, ValidationError, VERSION, VALIDATION_PATTERNS, TimeoutError, ServerError, SDK_INFO, RateLimitError, RETRY_CONFIG, PAGINATION_DEFAULTS, NotFoundError, NetworkError, MessageService, HttpClient, HTTP_STATUS, ENDPOINTS, ConfigurationError, CampaignService, BriqError, Briq, BaseService, BaseClient, AuthorizationError, AuthenticationError, API_CONFIG };