npm.io
0.6.0 • Published 3 months ago

@sonatel-os/juf

Licence
MIT
Version
0.6.0
Deps
6
Size
321 kB
Vulns
0
Weekly
0

JUF.js

npm Node ESM CJS TypeScript License Tests

The community SDK for Orange Money, SMS, Email & Sonatel APIs.
Built on the Orange Developer Platform — open to all developers.


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',
});

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: 'user@example.com',
  from: 'hello@myapp.com',
  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,
});
Full response shape
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).


MIT License — See LICENCE
Made with care by Mohamed Johnson at Sonatel
Built on the Orange Developer Platform