npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sonatel-os/juf

v0.6.0

Published

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

Readme


Why JUF?

The Orange Developer Platform exposes powerful APIs for payments, messaging, and more — but provides no official SDK. JUF fills that gap.

| What you get | Without JUF | With JUF | |---|---|---| | OAuth2 | Manual token fetch, caching, refresh | Automatic — one call, cached 240s | | Payments | Raw HTTP, manual payload construction | payment.preparePaymentCheckout() | | QR Codes | Build payloads, format amounts, manage headers | payment.createPaymentQRCode() | | SMS / Email | Auth + HTTP + error parsing | communication.sendSMS() | | Error handling | Parse each endpoint's error shape | Consistent JufError hierarchy | | Validation | Hope for the best | Superstruct schemas catch bad input before it hits the API |


Getting Started

1. Get your API credentials

Sign up at developer.orange-sonatel.com (free), create an application, and grab your client_id and client_secret. Sandbox access is immediate — no approval needed.

2. Install

yarn add @sonatel-os/juf
# or
npm install @sonatel-os/juf

3. Configure

cp .env.example .env
JUF_APIGEE_CLIENT_ID="<your-client-id>"
JUF_APIGEE_CLIENT_SECRET="<your-client-secret>"

See .env.example for all options (production, preprod, APM, logging).

4. Use

import { authentication, communication, payment } from '@sonatel-os/juf';

// Authenticate (tokens are cached automatically)
const { access_token } = await authentication.debug();

// Accept a payment via QR code
const { qrCode, deepLinks } = await payment.createPaymentQRCode({
  merchant: { code: 123456, sitename: 'CoolShop' },
  bill: { amount: 2500, reference: 'ORDER-42' },
});

// Send a confirmation SMS
await communication.sendSMS({
  body: 'Payment received! Thank you.',
  to: '+221770000000',
  senderName: 'CoolShop',
});

Subpath Imports (recommended)

Import only what you need for smaller bundles and clearer dependency graphs:

// Instead of importing everything:
import { authentication, payment } from '@sonatel-os/juf';

// Import only the domain you need:
import { Authentication } from '@sonatel-os/juf/auth';
import { Payment } from '@sonatel-os/juf/payment';
import { Communication } from '@sonatel-os/juf/communication';
import { ValidationError, AuthenticationError } from '@sonatel-os/juf/core';

// With DI, you control initialization:
const auth = new Authentication({ config, cache, client, logger });
const pay = new Payment({ authService: auth, client, config, logger });

| Subpath | Exports | |---|---| | @sonatel-os/juf/auth | Authentication class | | @sonatel-os/juf/communication | Communication class, EmailStructure, SmsStructure | | @sonatel-os/juf/payment | Payment, QRCodeDecoder classes, CheckoutPaymentStructure, QRCodePaymentStructure, QRCodeDecodePaymentStructure | | @sonatel-os/juf/core | Errors, validation, logger, cache, constants, requester |

The root import (@sonatel-os/juf) still works and will continue to work until v2.0.0.


API Reference

Authentication

import { Authentication } from '@sonatel-os/juf/auth';
const auth = Authentication.init();

auth.debug()

Fetches a fresh OAuth2 token or returns the cached one (TTL: 240s).

const { access_token, token_type, expires_in } = await auth.debug();

Communication

import { Communication } from '@sonatel-os/juf/communication';
const comm = Communication.init();

comm.sendEmail({ subject, to, from, body, html? })

const { id, status } = await comm.sendEmail({
  subject: 'Welcome!',
  to: '[email protected]',
  from: '[email protected]',
  body: '<h1>Welcome aboard!</h1>',
  html: true,
});

| Param | Type | Required | Description | |---|---|:---:|---| | subject | string | Yes | Email subject line | | to | string | Yes | Recipient address | | from | string | Yes | Sender address | | body | string | Yes | Email content | | html | boolean | — | true if body is HTML |

comm.sendSMS({ body, to, senderName, confidential?, scheduledFor? })

const { id, status } = await comm.sendSMS({
  body: 'Your OTP is 4829',
  to: '+221770000000',
  senderName: 'MyApp',
});

| Param | Type | Required | Default | Description | |---|---|:---:|:---:|---| | body | string | Yes | — | Message content | | to | string | Yes | — | Phone number | | senderName | string | Yes | — | Sender display name | | confidential | boolean | — | true | Mark as confidential | | scheduledFor | string | — | — | ISO 8601 datetime |


Payment

import { Payment } from '@sonatel-os/juf/payment';
const pay = Payment.init();

pay.preparePaymentCheckout({ merchant, bill, urls })

Creates a payment session and returns a checkout link.

const { link, secret } = await pay.preparePaymentCheckout({
  merchant: { code: 123456, sitename: 'your-sitename' },
  bill: { amount: 1000, reference: 'INV-2024-001' },
  urls: {
    success: 'https://my.site/success',
    failed: 'https://my.site/failed',
    cancel: 'https://my.site/canceled',
    callback: 'https://my.site/webhook',
  },
});
// Redirect your user to `link`

pay.createPaymentQRCode({ merchant, bill, urls?, metadata?, validity? })

Generates a QR code for mobile payment apps (Orange Money, MaxIt).

const { qrId, qrCode, deepLinks, shortLink } = await pay.createPaymentQRCode({
  merchant: { code: 123456, sitename: 'your-sitename' },
  bill: { amount: 500, reference: 'TIP-007' },
  metadata: { table: 12, waiter: 'Amadou' },
  validity: 300,
});

| Field | Type | Description | |---|---|---| | deepLink | string | Universal deep link | | deepLinks.MAXIT | string | MaxIt-specific link | | deepLinks.OM | string | Orange Money link | | qrCode | string | Base64 QR code image | | validity | number | Seconds remaining | | metadata | object | Your custom metadata | | shortLink | string | Shortened payment URL | | qrId | string | QR code identifier |

pay.decodeQrCode({ id })

Reads back the contents of a generated QR code. This is a privileged operation — only applications with explicit decode_qr_sp_authorization credentials can use it. It uses static SP authorization instead of the OAuth2 Bearer token.

const { content } = await pay.decodeQrCode({ id: 'doyaT9sH3rGFph_ZuKIs' });
console.log(content.amount, content.reference);

You can also use the standalone QRCodeDecoder class directly:

import { QRCodeDecoder } from '@sonatel-os/juf/payment';
const decoder = QRCodeDecoder.init();
const { content } = await decoder.decode({ id: 'doyaT9sH3rGFph_ZuKIs' });

Error Handling

Every error thrown by JUF follows one consistent shape:

import { ValidationError, AuthenticationError, ExternalServiceError } from '@sonatel-os/juf/core';

try {
  await pay.preparePaymentCheckout({ /* bad data */ });
} catch (error) {
  console.log(error.toJSON());
  // {
  //   success: false,
  //   error: {
  //     code: 'JUF_VALIDATION_ERROR',
  //     message: 'Validation failed for preparePaymentCheckout: ...',
  //     details: [...]
  //   }
  // }
}

| Error Class | Code | Status | When | |---|---|:---:|---| | ValidationError | JUF_VALIDATION_ERROR | 400 | Bad input (wrong types, missing fields, invalid URLs) | | AuthenticationError | JUF_AUTH_ERROR | 401 | OAuth2 failure (bad credentials, expired) | | ExternalServiceError | JUF_EXTERNAL_SERVICE_ERROR | varies | Upstream API error |

All errors extend JufError which extends native Errorinstanceof checks work as expected.


Project Structure

src/
  auth/               # OAuth2 client credentials flow
  communication/      # Email & SMS via Apigee
  payment/            # Checkout, QR codes, decode
  core/               # Errors, validation, logger, cache, HTTP client
config/               # Environment-based configuration loader
tests/                # 174 tests — unit, service, contract

Bundled Logger

JUF ships with @sonatel-os/juf-xpress-logger included — no extra install needed. It's a structured Express-aware logger maintained in its own SDK repository.

import { logger } from '@sonatel-os/juf';

logger.bootstrap({
  appName: 'my-service',
  logConsole: true,
});

JUF also has its own internal lightweight logger (used for library diagnostics) separate from juf-xpress-logger. Set JUF_LOG_LEVEL=debug to see internal debug output.


Notes

  • Tokens are cached for 240 seconds — no redundant auth calls
  • All redirect URLs are validated (HTTP/HTTPS only) to prevent open redirect attacks
  • Validation errors are thrown, not silently swallowed — always use try/catch
  • If the QR code service is down during checkout, the flow gracefully falls back to USSD
  • The internal logger sanitizes sensitive fields (tokens, secrets, passwords) before logging
  • Services support dependency injection for full testability

Contributing

git clone <repo-url> && cd juf-js
yarn install
yarn test          # Run 174 tests
yarn lint          # ESLint check
yarn build         # Dual ESM/CJS build

Commits must follow Conventional Commits (enforced by commitlint).