@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
JavaScript
;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