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