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

@classytic/carrier-bd

v0.1.0

Published

Bangladesh carrier adapters for @classytic/carrier — RedX, Pathao, Steadfast. Implements the unified CarrierAdapter port with a shared resilient HTTP client (circuit breaker, retry, timeout). Optional zero-config address→area resolution via @classytic/bd-

Readme

@classytic/carrier-bd

Bangladesh carrier adapters for @classytic/carrier. Three drop-in last-mile couriers, one contract.

| Carrier | Strengths | COD | Webhooks | Auth | |---|---|---|---|---| | RedX | Nationwide coverage, detailed tracking timeline, charge calculator. | ✅ | ✅ | Static JWT | | Pathao | Fastest in Dhaka metro, competitive intra-city pricing. | ✅ | ✅ | OAuth password grant (auto refresh) | | Steadfast | Simplest API, flat-rate pricing, 48 h payouts. | ✅ | ✅ | API key + secret |

All three implement the unified CarrierAdapter port, so you pick carriers per shipment via declarative rules — the router never branches on vendor code.


Install

npm install @classytic/carrier @classytic/carrier-bd @classytic/primitives

Quick start

import { CarrierRouter, CarrierRegistry } from '@classytic/carrier';
import { PathaoAdapter, RedXAdapter, SteadfastAdapter } from '@classytic/carrier-bd';

const router = new CarrierRouter({
  registry: new CarrierRegistry([
    new PathaoAdapter({
      // Pathao OAuth — adapter manages token lifecycle
      credentials: {
        clientId: process.env.PATHAO_CLIENT_ID!,
        clientSecret: process.env.PATHAO_CLIENT_SECRET!,
        username: process.env.PATHAO_USERNAME!,
        password: process.env.PATHAO_PASSWORD!,
      },
      defaultStoreCode: Number(process.env.PATHAO_DEFAULT_STORE_ID),
    }),
    new RedXAdapter({
      apiKey: process.env.REDX_API_KEY!,
      defaultPickupStoreId: Number(process.env.REDX_STORE_ID),
    }),
    new SteadfastAdapter({
      apiKey: process.env.STEADFAST_API_KEY!,
      apiSecret: process.env.STEADFAST_SECRET!,
    }),
  ]),
  rules: [
    { carrier: 'pathao', priority: 30, match: { country: 'BD', postalPrefix: '12' } },
    { carrier: 'redx', priority: 20, match: { country: 'BD' } },
    { carrier: 'steadfast', priority: 10, match: { country: 'BD' } },
  ],
  ranking: 'priority',
});

const quotes = await router.rateShop(shipment, ctx);
const { label } = await router.purchase(shipment, ctx);

Pathao auth — OAuth password grant (built-in)

The Pathao Aladdin API uses an OAuth password grant — issue an access token with client_id + client_secret + username + password, refresh before expiry. The adapter handles this automatically:

new PathaoAdapter({
  credentials: { clientId, clientSecret, username, password },
  // OR the legacy path: pre-issued bearer token
  // apiKey: '<bearer>',
});

When credentials is set, the adapter:

  • Issues a token on the first authenticated call.
  • Coalesces concurrent first-call token requests into one issue-token POST.
  • Refreshes 5 min before expiry using grant_type=refresh_token.
  • Falls back to a fresh password grant if a refresh attempt fails or returns a malformed payload.

Live API helpers also use this auth pipeline:

const cities = await pathao.listCities();
const zones = await pathao.listZones(cityId);
const areas = await pathao.listAreas(zoneId);
const stores = await pathao.listStores();

Address → carrier-area resolution

Most callers don't want to pass metadata.deliveryAreaId on every shipment — the host's domain typically only knows the destination postal code and city. Wire an AreaResolver and the adapter figures out the carrier-specific id.

Static resolver — @classytic/bd-areas

import { RedXAdapter } from '@classytic/carrier-bd';
import { createBdAreasResolver } from '@classytic/carrier-bd/bd-areas-resolver';

const adapter = new RedXAdapter({
  apiKey: process.env.REDX_API_KEY!,
  defaultPickupStoreId: 1,
  areaResolver: createBdAreasResolver(),
});

@classytic/bd-areas is an optional peer dep — the core @classytic/carrier-bd doesn't import it. The resolver lives at a separate subpath so hosts that don't need it never pay the dataset cost.

Live resolver — createPathaoLiveResolver(adapter)

For Pathao specifically, the bd-areas dataset doesn't ship Pathao IDs (Pathao zones are landmark-level — they don't fit the unified Area shape). The adapter ships a live resolver that calls Pathao's own API (cached) to resolve a destination address → Pathao zone id:

import { PathaoAdapter, createPathaoLiveResolver } from '@classytic/carrier-bd';

const pathao = new PathaoAdapter({ credentials });
pathao.areaResolver = createPathaoLiveResolver(pathao);
// or pass at construction

Reuses the same adapter's token cache + circuit breaker. Cache TTL defaults to 1 h, configurable via cacheTtlMs. Returns null for non-Pathao carriers so it composes safely.

Composite resolver — createCompositeResolver([static, live])

Production setup: try the cheap static resolver first, fall back to the live resolver if the dataset doesn't have the answer:

import { createBdAreasResolver } from '@classytic/carrier-bd/bd-areas-resolver';
import { createCompositeResolver, createPathaoLiveResolver } from '@classytic/carrier-bd';

const pathao = new PathaoAdapter({ credentials });
const resolver = createCompositeResolver([
  createBdAreasResolver(),         // free, offline, RedX-good
  createPathaoLiveResolver(pathao), // network fallback for Pathao
]);
pathao.areaResolver = resolver;

Errors from individual resolvers are swallowed by default — pass { failFast: true } to bubble. Pass { onError } to log without breaking the chain.

Resolution precedence

For every quoteShipment / buyLabel call:

  1. metadata.deliveryAreaId on the shipment — caller knows the id, use it verbatim.
  2. areaResolver.resolve(destination, carrier) — if wired.
  3. NeitherProviderValidationError naming what to supply.

BD-specific shipment metadata

const shipment: ShipmentInput = {
  origin: { /* ... */ },
  destination: { name: 'Alice', phone: '+8801...', line1: 'House 22', city: 'Dhaka', country: 'BD' },
  packages: [{ weightGrams: 750 }],
  codAmount: { amount: 1500, currency: 'BDT' },
  metadata: {
    // Universal
    deliveryAreaId: 100,
    deliveryAreaName: 'Gulshan',         // RedX needs name + id
    pickupStoreId: 1,                     // overrides constructor default
    deliveryInstructions: 'Call before delivery',
    merchantInvoiceId: 'ORD-2026-00042',  // defaults to shipment.reference

    // Pathao-specific (hierarchical)
    deliveryCityId: 1,                    // Dhaka
    deliveryZoneId: 10,                   // Gulshan (overrides resolver)
  },
};

The BdShipmentMetadata TypeScript type documents every key.


Pathao bulk-upload CSV exporter

For merchants who don't want full API integration — Pathao's web dashboard accepts a CSV at Orders → Bulk Upload. Build it server-side or in the browser:

import {
  buildPathaoBulkCsv,
  defaultPathaoCsvFilename,
  type PathaoCsvRow,
} from '@classytic/carrier-bd';

const rows: PathaoCsvRow[] = orders.map((o) => ({
  itemType: 'parcel',
  recipientName: o.deliveryAddress.recipientName,
  recipientPhone: o.deliveryAddress.recipientPhone,
  recipientAddress: `${o.deliveryAddress.addressLine1}, ${o.deliveryAddress.addressLine2}`,
  recipientCity: o.deliveryAddress.city,
  recipientZone: o.deliveryAddress.zone,
  amountToCollect: o.totals.grandTotal.amount,
  itemQuantity: o.items.length,
  itemWeight: 0.5,
}));

const { csv, totalRows, skipped } = buildPathaoBulkCsv(rows);
// csv = RFC-4180 string. Skipped rows include row index + reason.
// `failFast: true` throws on the first invalid row.

Validation rules: name ≥ 2 chars, phone 10–15 digits, address ≥ 5 chars, weight 0.5–10 kg, city + zone non-empty. Phones are always quoted so Excel doesn't reformat them as scientific notation.


Capabilities matrix

| Feature | RedX | Pathao | Steadfast | |---|---|---|---| | rateShopping | ❌ | ❌ | ❌ (flat rate) | | cod | ✅ | ✅ | ✅ | | addressVerification | ✅ | ❌ | ❌ | | voidLabel | ✅ | ❌ (contact CS) | ❌ (mark in-app) | | multiPackage | ❌ | ❌ | ❌ | | international | ❌ | ❌ | ❌ | | webhooks | ✅ | ✅ | ✅ |

Adapters without a capability are skipped automatically by the @classytic/carrier router when the shipment requires it.


Resilient HTTP out of the box

Every adapter uses a built-in HTTP client with:

  • Circuit breaker per provider — opens after N consecutive failures, half-opens after a cooldown.
  • Exponential backoff retry for 408 / 429 / 5xx.
  • Per-request timeout via AbortController.
  • Injectable fetch / sleep / nowMs for deterministic tests.
new RedXAdapter({
  apiKey: '...',
  http: { timeoutMs: 10_000, maxRetries: 2, failureThreshold: 3 },
});

Webhooks

Each adapter exposes ingestWebhook(payload, headers) which normalises vendor payloads to NormalizedWebhookEvent[]:

app.post('/webhooks/:carrier', async (req, reply) => {
  const adapter = registry.get(req.params.carrier);
  const events = adapter.ingestWebhook(req.body, req.headers);
  for (const evt of events) await yourEventBus.publish('carrier:tracking.updated', evt);
  reply.send({ ok: true });
});

Signature verification is intentionally host-owned — each carrier's HMAC rules differ and are often account-specific.


Tests

Three tiers per testing-infrastructure.md. Single vitest.config.ts with projects: [unit, integration, e2e].

| Command | What runs | |---|---| | npm test | unit + integration (default CI path) — pure mocked-fetch tests | | npm run test:unit | adapter behaviour, OAuth lifecycle, resolvers, CSV exporter | | npm run test:integration | adapter ↔ resolver wiring + cross-carrier contract suite | | npm run test:e2e | env-gated live RedX + Pathao sandbox — exercises the full happy path |

Sandbox creds live in .env (gitignored). dotenv/config is auto-loaded by vitest:

PATHAO_API_URL=https://courier-api-sandbox.pathao.com
PATHAO_CLIENT_ID=... PATHAO_CLIENT_SECRET=...
[email protected] PATHAO_PASSWORD=lovePathao
REDX_API_URL=https://sandbox.redx.com.bd/v1.0.0-beta REDX_API_KEY=<jwt>

The e2e suites auto-discover live cities, zones, areas, and pickup stores from each carrier's API.


Extending

Write additional BD carriers (eCourier, Paperfly, Sundarban, etc.) by implementing CarrierAdapter from @classytic/carrier. The HTTP client, status-mapping helpers, and address utilities in this package are exported for reuse:

import {
  createHttpClient,
  inferFromMessage,
  flattenAddress,
  totalWeightGrams,
  type ProviderStatusMap,
} from '@classytic/carrier-bd';

License

MIT