UNPKG

@sonatel-os/juf

Version:

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

508 lines (497 loc) 14.4 kB
// 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 = 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 import NodeCache from "node-cache"; 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 NodeCache({ 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 import dotenv from "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) { dotenv.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 import axios from "axios"; import axiosRetry from "axios-retry"; import https from "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 = axios.create({ httpsAgent: new https.Agent(agentParams || {}), ...axiosConfig }); if (retry !== false) { axiosRetry(instance, { retries: retry?.retries ?? MAX_RETRY_ATTEMPTS, retryDelay: (retryCount) => retryCount * (retry?.baseDelay ?? RETRY_BASE_DELAY_MS), retryCondition: (error) => error.response?.status === RETRYABLE_STATUS_CODE }); } return instance; } }; var requester_default = Requester; // src/core/errors.js var JufError = class extends Error { /** @type {number} */ status; /** @type {string} */ code; /** @type {*} */ details; /** * @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 = error.response?.status || HTTP_INTERNAL_SERVER_ERROR; const upstream = error.response?.data; const message = upstream?.message || upstream?.error_description || fallbackMessage; const details = upstream?.detail || upstream?.error || null; return new ExternalServiceError(message, status, details); }; export { logger, AUTH_CACHE_TTL_SECONDS, MAX_RETRY_ATTEMPTS, RETRY_BASE_DELAY_MS, RETRYABLE_STATUS_CODE, CURRENCY_UNIT, SMS_CHANNEL, OAUTH_CONTENT_TYPE, OAUTH_GRANT_TYPE, OAUTH_TOKEN_PATH, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_INTERNAL_SERVER_ERROR, HTTP_BAD_GATEWAY, CachingSystem, cache_default, envConfig, getApiUrl, getApiEnv, requester_default, JufError, ValidationError, AuthenticationError, ExternalServiceError, fromAxiosError }; //# sourceMappingURL=chunk-4T5F3RH2.js.map