UNPKG

@sonatel-os/juf

Version:

The community SDK for Orange Money, SMS, Email & Sonatel APIs on the Orange Developer Platform.

508 lines (468 loc) 16.7 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/core/logger.js var LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3 }; var Logger = class _Logger { /** @type {string} */ #prefix; /** @type {number} */ #level; /** * @param {object} options - Logger configuration. * @param {string} [options.prefix='juf'] - Log line prefix / service identifier. * @param {string} [options.level='info'] - Minimum log level to output. */ constructor({ prefix = "juf", level = "info" } = {}) { this.#prefix = prefix; this.#level = _nullishCoalesce(LOG_LEVELS[level], () => ( LOG_LEVELS.info)); } /** * Creates a child logger with a scoped prefix. * @param {string} scope - The scope name (e.g. 'payment', 'auth'). * @returns {Logger} A new Logger instance with the scoped prefix. */ child(scope) { return new _Logger({ prefix: `${this.#prefix}:${scope}`, level: Object.keys(LOG_LEVELS).find((k) => LOG_LEVELS[k] === this.#level) }); } /** * @param {string} message - Log message. * @param {object} [context] - Structured context metadata. */ error(message, context) { this.#log("error", message, context); } /** * @param {string} message - Log message. * @param {object} [context] - Structured context metadata. */ warn(message, context) { this.#log("warn", message, context); } /** * @param {string} message - Log message. * @param {object} [context] - Structured context metadata. */ info(message, context) { this.#log("info", message, context); } /** * @param {string} message - Log message. * @param {object} [context] - Structured context metadata. */ debug(message, context) { this.#log("debug", message, context); } /** * @private * @param {string} level - Log level. * @param {string} message - Log message. * @param {object} [context] - Structured context metadata. */ #log(level, message, context) { if (LOG_LEVELS[level] > this.#level) return; const entry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level, prefix: this.#prefix, message, ...context && { context: this.#sanitize(context) } }; const output = level === "error" || level === "warn" ? console.error : console.log; output(JSON.stringify(entry)); } /** * Strips sensitive fields from context before logging. * @private * @param {object} context - The context object to sanitize. * @returns {object} Sanitized copy of the context. */ #sanitize(context) { const SENSITIVE_KEYS = /* @__PURE__ */ new Set([ "authorization", "password", "client_secret", "access_token", "token", "secret", "cookie" ]); const sanitized = {}; for (const [key, value] of Object.entries(context)) { if (SENSITIVE_KEYS.has(key.toLowerCase())) { sanitized[key] = "[REDACTED]"; } else if (typeof value === "object" && value !== null && !Array.isArray(value)) { sanitized[key] = this.#sanitize(value); } else { sanitized[key] = value; } } return sanitized; } }; var logger = new Logger({ level: process.env.JUF_LOG_LEVEL || "info" }); // src/core/constants.js var AUTH_CACHE_TTL_SECONDS = 240; var MAX_RETRY_ATTEMPTS = 2; var RETRY_BASE_DELAY_MS = 2e3; var RETRYABLE_STATUS_CODE = 503; var CURRENCY_UNIT = "XOF"; var SMS_CHANNEL = "SMS"; var OAUTH_CONTENT_TYPE = "application/x-www-form-urlencoded"; var OAUTH_GRANT_TYPE = "client_credentials"; var OAUTH_TOKEN_PATH = "/oauth/v1/token"; var HTTP_BAD_REQUEST = 400; var HTTP_UNAUTHORIZED = 401; var HTTP_INTERNAL_SERVER_ERROR = 500; var HTTP_BAD_GATEWAY = 502; // src/core/cache.js var _nodecache = require('node-cache'); var _nodecache2 = _interopRequireDefault(_nodecache); var logger2 = logger.child("cache"); var Cache = class { /** @private @type {NodeCache} */ #cache; /** * Creates an instance of Cache. * @param {number} ttlSeconds - Time-to-live (TTL) in seconds for cached items. */ constructor(ttlSeconds) { this.#cache = new (0, _nodecache2.default)({ stdTTL: ttlSeconds }); } /** * Stores a value in the cache with a specified key. * * @param {string} key - The key under which the value is stored. * @param {*} value - The value to be stored. Must be serializable to JSON. * @returns {boolean} Returns true if stored successfully, otherwise false. */ store(key, value) { try { return this.#cache.set(key, JSON.stringify(value)); } catch (error) { logger2.error("Failed to store cache entry", { key, error: error.message }); return false; } } /** * Retrieves a value from the cache by its key. * * @param {string} key - The key to retrieve the value. * @returns {object|null} Returns the parsed cached value if found, otherwise null. */ retrieve(key) { try { if (this.#cache.has(key)) { return JSON.parse(this.#cache.get(key)); } } catch (error) { logger2.error("Failed to retrieve cache entry", { key, error: error.message }); } return null; } }; var CachingSystem = new Cache(AUTH_CACHE_TTL_SECONDS); var cache_default = Cache; // config/index.js var _dotenv = require('dotenv'); var _dotenv2 = _interopRequireDefault(_dotenv); // config/src/apigee.js var apigeeConfig = { onProd: { doc: "Flag to indicate production environment for Apigee", format: "Boolean", default: false, env: "JUF_APIGEE_ON_PROD" }, onPProd: { doc: "Flag to indicate pre-production environment for Apigee", format: "Boolean", default: false, env: "JUF_APIGEE_SP_PPROD" }, url: { production: { doc: "Production URL for Apigee", format: String, default: "https://api.orange-sonatel.com", env: "JUF_APIGEE_PROD_URL" }, sandbox: { doc: "Sandbox URL for Apigee", format: String, default: "https://api.sandbox.orange-sonatel.com", env: "JUF_APIGEE_SANDBOX_URL" }, preprod: { doc: "Pre-production URL for Apigee", format: String, default: "https://api.preprod.orange-sonatel.com", env: "JUF_APIGEE_PREPROD_URL" } }, client_id: { doc: "The Apigee client ID", format: String, default: "<CLIENT_ID>", env: "JUF_APIGEE_CLIENT_ID" }, client_secret: { doc: "The Apigee client secret", format: String, default: "<CLIENT_SECRET>", env: "JUF_APIGEE_CLIENT_SECRET" }, decode_qr_sp_authorization: { doc: "The Apigee SP authorization", format: String, default: "<SP_AUTHORIZATION>", env: "JUF_APIGEE_DECODE_QR_SP_AUTHORIZATION" } }; // config/src/apm.js var apmConfig = { verifyServerCert: { doc: "Verify APM server certificate", format: "Boolean", default: false, env: "JUF_ELK_APM_VERIF_CERT" }, logLevel: { doc: "Log level for APM", format: String, default: "info", env: "JUF_ELK_APM_LOG_LEVEL" }, environment: { doc: "Environment name for APM", format: String, default: "<JUF_JS>", env: "JUF_ELK_APM_ENV_NAME" }, serviceName: { doc: "Service name for APM", format: String, default: "<JUF_JS>", env: "JUF_ELK_APM_SERVICE_NAME" }, secretToken: { doc: "Secret token for APM", format: String, default: "<PASSWORD>", env: "JUF_ELK_APM_SECRET_TOKEN" }, serverUrl: { doc: "Server URL for APM", format: String, default: "http://127.0.0.1:8200", env: "JUF_ELK_APM_SERVER" } }; // config/src/environment.js var environmentConfig = { env: { doc: "The application environment", format: ["test", "dev", "development", "pprod", "prod", "production"], default: "dev", env: "NODE_ENV" }, port: { doc: "The port to bind", format: "port", default: 3e3, env: "PORT" } }; // config/index.js var PLACEHOLDER_PATTERN = /^<.+>$/; var configSchema = { ...environmentConfig, apigee: apigeeConfig, apm: apmConfig }; var envLoaded = false; var ensureEnvLoaded = () => { if (!envLoaded) { _dotenv2.default.config(); envLoaded = true; } }; var loadConfigFromEnv = (schema) => { const config = {}; Object.entries(schema).forEach(([key, value]) => { if (typeof value === "object" && value !== null && !Array.isArray(value)) { if ("env" in value && "default" in value) { const envValue = process.env[value.env]; let finalValue = envValue !== void 0 ? envValue : value.default; if (typeof value.format === "function") { finalValue = value.format(finalValue); } config[key] = finalValue; } else { config[key] = loadConfigFromEnv(value); } } else { config[key] = value; } }); return config; }; var validateApigeeConfig = (config) => { const required = [ { key: "client_id", env: "JUF_APIGEE_CLIENT_ID" }, { key: "client_secret", env: "JUF_APIGEE_CLIENT_SECRET" } ]; for (const { key, env } of required) { const value = config[key]; if (!value || PLACEHOLDER_PATTERN.test(value)) { throw new Error( `Missing ${env} \u2014 set it in your .env file or environment variables. Get your credentials at https://developer.orange-sonatel.com` ); } } }; var envConfig = { /** * @function get * @description Retrieves the configuration for a given schema name. * @param {string} name - The name of the schema to retrieve. * @returns {Object} - The configuration object for the specified schema. */ get: (name) => { ensureEnvLoaded(); const config = loadConfigFromEnv(configSchema[name]); if (name === "apigee") { validateApigeeConfig(config); } return config; } }; // src/core/apiUrl.js var getApiUrl = () => { const { url: { production, sandbox, preprod }, onProd, onPProd } = envConfig.get("apigee"); if (onProd) return production; if (onPProd) return preprod; return sandbox; }; var getApiEnv = () => { const { onProd, onPProd } = envConfig.get("apigee"); return { onProd: Boolean(onProd), onPProd: Boolean(onPProd) }; }; // src/core/requester.js var _axios = require('axios'); var _axios2 = _interopRequireDefault(_axios); var _axiosretry = require('axios-retry'); var _axiosretry2 = _interopRequireDefault(_axiosretry); var _https = require('https'); var _https2 = _interopRequireDefault(_https); var Requester = class { /** * Creates a new Axios instance with custom configuration and per-instance retry. * * @static * @method bootstrap * @memberof Shared * @param {object} config - Custom configuration for Axios. * @param {string} config.baseURL - Base URL for requests. * @param {object} [config.agentParams] - HTTPS agent parameters. * @param {object|false} [config.retry] - Retry configuration, or false to disable. * @param {number} [config.retry.retries] - Number of retries (default: 2). * @param {number} [config.retry.baseDelay] - Base delay in ms for exponential backoff (default: 2000). * @returns {import('axios').AxiosInstance} A configured Axios instance. */ static bootstrap(config) { const { agentParams, retry, ...axiosConfig } = config; const instance = _axios2.default.create({ httpsAgent: new _https2.default.Agent(agentParams || {}), ...axiosConfig }); if (retry !== false) { _axiosretry2.default.call(void 0, instance, { retries: _nullishCoalesce(_optionalChain([retry, 'optionalAccess', _ => _.retries]), () => ( MAX_RETRY_ATTEMPTS)), retryDelay: (retryCount) => retryCount * (_nullishCoalesce(_optionalChain([retry, 'optionalAccess', _2 => _2.baseDelay]), () => ( RETRY_BASE_DELAY_MS))), retryCondition: (error) => _optionalChain([error, 'access', _3 => _3.response, 'optionalAccess', _4 => _4.status]) === RETRYABLE_STATUS_CODE }); } return instance; } }; var requester_default = Requester; // src/core/errors.js var JufError = class extends Error { /** @type {number} */ /** @type {string} */ /** @type {*} */ /** * @param {string} message - Human-readable error message. * @param {number} [status=500] - HTTP-equivalent status code. * @param {string} [code='JUF_ERROR'] - Machine-readable error code. * @param {*} [details=null] - Additional error context. */ constructor(message, status = HTTP_INTERNAL_SERVER_ERROR, code = "JUF_ERROR", details = null) { super(message); this.name = this.constructor.name; this.status = status; this.code = code; this.details = details; } /** * Returns a consistent JSON-serializable error response. * @returns {{ success: false, error: { code: string, message: string, details: * } }} */ toJSON() { return { success: false, error: { code: this.code, message: this.message, ...this.details && { details: this.details } } }; } }; var ValidationError = class extends JufError { /** * @param {string} message - Description of the validation failure. * @param {*} [details=null] - Field-level details about what failed. */ constructor(message, details = null) { super(message, HTTP_BAD_REQUEST, "JUF_VALIDATION_ERROR", details); } }; var AuthenticationError = class extends JufError { /** * @param {string} [message='Authentication failed. Check your credentials and try again.'] - Error message. * @param {*} [details=null] - API response details. */ constructor(message = "Authentication failed. Check your credentials and try again.", details = null) { super(message, HTTP_UNAUTHORIZED, "JUF_AUTH_ERROR", details); } }; var ExternalServiceError = class extends JufError { /** * @param {string} message - Description of the service failure. * @param {number} [status=502] - HTTP status from the upstream service. * @param {*} [details=null] - Upstream response body or error details. */ constructor(message, status = HTTP_BAD_GATEWAY, details = null) { super(message, status, "JUF_EXTERNAL_SERVICE_ERROR", details); } }; var fromAxiosError = (error, fallbackMessage) => { const status = _optionalChain([error, 'access', _5 => _5.response, 'optionalAccess', _6 => _6.status]) || HTTP_INTERNAL_SERVER_ERROR; const upstream = _optionalChain([error, 'access', _7 => _7.response, 'optionalAccess', _8 => _8.data]); const message = _optionalChain([upstream, 'optionalAccess', _9 => _9.message]) || _optionalChain([upstream, 'optionalAccess', _10 => _10.error_description]) || fallbackMessage; const details = _optionalChain([upstream, 'optionalAccess', _11 => _11.detail]) || _optionalChain([upstream, 'optionalAccess', _12 => _12.error]) || null; return new ExternalServiceError(message, status, details); }; exports.logger = logger; exports.AUTH_CACHE_TTL_SECONDS = AUTH_CACHE_TTL_SECONDS; exports.MAX_RETRY_ATTEMPTS = MAX_RETRY_ATTEMPTS; exports.RETRY_BASE_DELAY_MS = RETRY_BASE_DELAY_MS; exports.RETRYABLE_STATUS_CODE = RETRYABLE_STATUS_CODE; exports.CURRENCY_UNIT = CURRENCY_UNIT; exports.SMS_CHANNEL = SMS_CHANNEL; exports.OAUTH_CONTENT_TYPE = OAUTH_CONTENT_TYPE; exports.OAUTH_GRANT_TYPE = OAUTH_GRANT_TYPE; exports.OAUTH_TOKEN_PATH = OAUTH_TOKEN_PATH; exports.HTTP_BAD_REQUEST = HTTP_BAD_REQUEST; exports.HTTP_UNAUTHORIZED = HTTP_UNAUTHORIZED; exports.HTTP_INTERNAL_SERVER_ERROR = HTTP_INTERNAL_SERVER_ERROR; exports.HTTP_BAD_GATEWAY = HTTP_BAD_GATEWAY; exports.CachingSystem = CachingSystem; exports.cache_default = cache_default; exports.envConfig = envConfig; exports.getApiUrl = getApiUrl; exports.getApiEnv = getApiEnv; exports.requester_default = requester_default; exports.JufError = JufError; exports.ValidationError = ValidationError; exports.AuthenticationError = AuthenticationError; exports.ExternalServiceError = ExternalServiceError; exports.fromAxiosError = fromAxiosError; //# sourceMappingURL=chunk-W5WEVJMX.cjs.map