@elusion-sdk/briq
Version:
A modern TypeScript SDK for Briq SMS API integration
1,398 lines (1,369 loc) • 43.7 kB
JavaScript
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
};