pathao-merchant-sdk 2.3.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.0.0](https://github.com/Sifat07/pathao-merchant-sdk/compare/pathao-merchant-sdk-v2.3.2...pathao-merchant-sdk-v3.0.0) (2026-10-02)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * new PathaoApiService(config) no longer reads PATHAO_* environment variables for missing fields or PATHAO_TIMEOUT. Pass every field explicitly, or use PathaoApiService.fromEnv().
14
+
15
+ ### Bug Fixes
16
+
17
+ * close [#12](https://github.com/Sifat07/pathao-merchant-sdk/issues/12)–[#20](https://github.com/Sifat07/pathao-merchant-sdk/issues/20) (security, retries, webhooks, phones, explicit config, types, CI) ([#21](https://github.com/Sifat07/pathao-merchant-sdk/issues/21)) ([9702f72](https://github.com/Sifat07/pathao-merchant-sdk/commit/9702f724e8bc5fced2ec644d746d29b6242b0baf))
18
+
19
+ ## [2.3.2](https://github.com/Sifat07/pathao-merchant-sdk/compare/pathao-merchant-sdk-v2.3.1...pathao-merchant-sdk-v2.3.2) (2026-09-23)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * stop 429s tripping the circuit breaker, document webhook trust ([4861fc7](https://github.com/Sifat07/pathao-merchant-sdk/commit/4861fc7177c1872dfbbeb271c28f832ab82435c1))
25
+
8
26
  ## [2.3.1](https://github.com/Sifat07/pathao-merchant-sdk/compare/pathao-merchant-sdk-v2.3.0...pathao-merchant-sdk-v2.3.1) (2026-09-17)
9
27
 
10
28
 
package/CONTRIBUTING.md CHANGED
@@ -10,10 +10,9 @@ Thank you for your interest in contributing to the Pathao Merchant SDK! This doc
10
10
  git clone https://github.com/YOUR_USERNAME/pathao-merchant-sdk.git
11
11
  cd pathao-merchant-sdk
12
12
  ```
13
- 3. **Install dependencies**:
13
+ 3. **Install dependencies** (the repo pins pnpm 10 via `packageManager`; `corepack enable` makes `pnpm` use it):
14
14
  ```bash
15
- npm install
16
- # or
15
+ corepack enable
17
16
  pnpm install
18
17
  ```
19
18
 
@@ -37,9 +36,10 @@ git checkout -b fix/your-bug-fix
37
36
  ### 3. Run Tests
38
37
 
39
38
  ```bash
40
- npm test
41
- npm run build
42
- npm run type-check
39
+ pnpm run lint
40
+ pnpm run type-check
41
+ pnpm test
42
+ pnpm run build
43
43
  ```
44
44
 
45
45
  ### 4. Commit Your Changes
@@ -159,8 +159,8 @@ What should happen
159
159
  What actually happens
160
160
 
161
161
  **Environment**
162
- - SDK Version: 1.2.0
163
- - Node Version: 18.0.0
162
+ - SDK Version: 3.0.0
163
+ - Node Version: 22.x
164
164
  - OS: macOS/Windows/Linux
165
165
  ```
166
166
 
package/README.md CHANGED
@@ -28,6 +28,7 @@ An **unofficial** TypeScript SDK for the [Pathao Courier Merchant API](https://m
28
28
  - [Error Handling](#error-handling)
29
29
  - [Webhooks](#webhooks)
30
30
  - [TypeScript Types](#typescript-types)
31
+ - [Upgrading to 3.0.0](#upgrading-to-300)
31
32
  - [Contributing](#contributing)
32
33
  - [License](#license)
33
34
  - [Changelog](#changelog)
@@ -124,6 +125,8 @@ PATHAO_PASSWORD=your-password
124
125
  PATHAO_TIMEOUT=30000
125
126
  ```
126
127
 
128
+ Only `PathaoApiService.fromEnv()` reads these variables. `new PathaoApiService(config)`, `fromConfig()`, `sandbox()` and `production()` use exactly the config you pass, so a blank field fails validation instead of silently picking up another account's credentials from the environment (important when each tenant brings their own Pathao account).
129
+
127
130
  If you use `dotenv`, load it before initializing the SDK:
128
131
 
129
132
  ```typescript
@@ -175,6 +178,7 @@ const pathao = new PathaoApiService(config, {
175
178
  threshold: 5, // Failures before opening circuit (default: 5)
176
179
  timeout: 60_000, // Ms before attempting to close circuit (default: 60000)
177
180
  },
181
+ minRequestIntervalMs: 0, // Min gap between requests, queued (default: 0, off)
178
182
  });
179
183
  ```
180
184
 
@@ -193,7 +197,7 @@ const order = await pathao.createOrder({
193
197
  store_id: 12345, // Required — your store ID
194
198
  merchant_order_id: "ORDER-001", // Optional — your internal tracking ID
195
199
  recipient_name: "John Doe", // Required — 3–100 characters
196
- recipient_phone: "01712345678", // Required — 11 digits, starts with 01
200
+ recipient_phone: "01712345678", // Required — BD mobile; +880… / dashes are normalised before sending
197
201
  recipient_secondary_phone: "01812345678", // Optional
198
202
  recipient_address: "House 10, Road 5, Dhanmondi, Dhaka", // Required — 10–220 chars
199
203
  recipient_city: 1, // Optional — auto-detected if omitted
@@ -338,7 +342,8 @@ All helpers are static and can be used before constructing the SDK:
338
342
  ```typescript
339
343
  import { PathaoApiService } from "pathao-merchant-sdk";
340
344
 
341
- PathaoApiService.validatePhoneNumber("01712345678"); // true — 11 digits, starts with 01
345
+ PathaoApiService.validatePhoneNumber("+8801712345678"); // true — BD mobile, 013–019
346
+ PathaoApiService.normalizePhoneNumber("+880 1712-345678"); // "01712345678" (null if invalid)
342
347
  PathaoApiService.validateContactNumber("01712345678"); // true — same rules
343
348
  PathaoApiService.validateAddress("House 10, Road 5, Dhanmondi, Dhaka"); // true — 10–220 chars
344
349
  PathaoApiService.validateStoreAddress("House 10, Road 5, Dhanmondi"); // true — 15–120 chars
@@ -347,6 +352,8 @@ PathaoApiService.validateRecipientName("John Doe"); // true — 3–100 chars
347
352
  PathaoApiService.validateStoreName("My Store"); // true — 3–50 chars
348
353
  ```
349
354
 
355
+ `createOrder` and `createBulkOrder` run the phone, name, address and weight checks before sending (a bulk failure names the index, e.g. `orders[3]: …`), and send the normalised phone numbers.
356
+
350
357
  ---
351
358
 
352
359
  ## Error Handling
@@ -360,6 +367,7 @@ try {
360
367
  const order = await pathao.createOrder(orderData);
361
368
  } catch (err) {
362
369
  if (err instanceof PathaoApiError) {
370
+ console.error("Kind:", err.kind, "retryable:", err.retryable); // e.g. "validation", false
363
371
  console.error("HTTP status:", err.status); // e.g. 422
364
372
  console.error("Pathao code:", err.code); // Pathao internal error code
365
373
  console.error("Type:", err.type); // e.g. "ValidationException"
@@ -370,6 +378,23 @@ try {
370
378
  }
371
379
  ```
372
380
 
381
+ ### Error kinds
382
+
383
+ Branch on `err.kind` instead of matching message text. `err.retryable` is `true` for `unavailable` and `rate_limited`.
384
+
385
+ | `kind` | When |
386
+ | -------------- | ----------------------------------------------------------- |
387
+ | `validation` | Rejected by the SDK before sending, or HTTP 400/422 |
388
+ | `config` | Missing/invalid `baseURL` or credentials |
389
+ | `auth` | HTTP 401 |
390
+ | `forbidden` | HTTP 403 |
391
+ | `not_found` | HTTP 404 |
392
+ | `rate_limited` | HTTP 429 |
393
+ | `unavailable` | 5xx, timeout, network failure, circuit breaker open |
394
+ | `unexpected` | Anything else; inspect `err.responseData` |
395
+
396
+ A retryable error on `createOrder` / `createBulkOrder` may still have booked the parcel. Look the order up before retrying a create.
397
+
373
398
  ### Common error scenarios
374
399
 
375
400
  | Status | Cause |
@@ -377,8 +402,32 @@ try {
377
402
  | 400 | Bad request / missing required fields |
378
403
  | 401 | Invalid or expired credentials |
379
404
  | 422 | Validation failure — check `err.errors` for field details |
380
- | 429 | Rate limited — SDK retries automatically after `Retry-After` |
381
- | 503 | Circuit breaker open — too many consecutive failures |
405
+ | 429 | Rate limited — see below |
406
+ | 503 | Circuit breaker open — repeated network, 5xx or 401 failures |
407
+
408
+ ### Rate limits
409
+
410
+ Pathao doesn't document its limits. Measured against its gateway (Sep 2026): **60 requests per rolling 60 seconds**, and the `429` carries **no `Retry-After` header**. The SDK therefore does not retry a `429` unless the server sends `Retry-After`; it throws `PathaoApiError` with `status: 429` so you can back off. 429s never open the circuit breaker.
411
+
412
+ For bulk work (e.g. polling `getOrderStatus` for many orders) pass `minRequestIntervalMs: 1500`. The client then queues its requests (token grants and retries included) one every 1.5 s, about 40/min, leaving headroom for webhooks and other callers sharing the same credentials. Spacing is per instance: share one instance across the process.
413
+
414
+ Both numbers are exported: `PATHAO_RATE_LIMIT_PER_MINUTE` (60) and `PATHAO_STATUS_RETENTION_DAYS` (90, roughly how long `getOrderStatus` finds an order).
415
+
416
+ ### Retries
417
+
418
+ The SDK retries a `5xx` up to twice, but only for requests that are safe to repeat: GETs, token grants and `calculatePrice`. `createOrder`, `createBulkOrder` and `createStore` are **never** retried, because a `5xx` (e.g. a gateway `504` in front of a slow success) doesn't mean the order wasn't booked. If you retry a create yourself, look the order up first, or you may book the parcel twice.
419
+
420
+ ### Order lifecycle
421
+
422
+ Webhook events and `order_status_slug` describe the same journey. Despite its name, `order_status_slug` is a display label (`"Pending"`, `"In Transit"`, `"Return"`), and Pathao's own plugin spells the same states differently (`Pickup_Requested`, `At_the_Sorting_HUB`), so the SDK doesn't type it. `toLifecycleStatus` maps any of these spellings, or a webhook event (`order.pickup-requested`), to one of `created`, `picked_up`, `in_transit`, `out_for_delivery`, `delivered`, `partial`, `on_hold`, `returning`, `returned`, `cancelled`, or `unknown`.
423
+
424
+ ```typescript
425
+ import { toLifecycleStatus, isFinalLifecycleStatus } from "pathao-merchant-sdk"; // also exported from /webhooks
426
+
427
+ toLifecycleStatus("order.return-id-created"); // "returning" — not back yet, don't restock
428
+ toLifecycleStatus(info.data.order_status_slug);
429
+ isFinalLifecycleStatus("delivered"); // true
430
+ ```
382
431
 
383
432
  ---
384
433
 
@@ -389,10 +438,13 @@ The webhooks module is a **separate entry point** with zero runtime dependencies
389
438
  ### How Pathao webhooks work
390
439
 
391
440
  1. Pathao sends a POST request with a JSON payload to your URL.
392
- 2. The `X-PATHAO-Signature` header contains your configured webhook secret verbatim.
393
- 3. Your endpoint must respond within 10 seconds with an `X-Pathao-Merchant-Webhook-Integration-Secret` header whose value equals your webhook secret.
441
+ 2. The `X-PATHAO-Signature` header contains the webhook secret verbatim.
442
+ 3. Your endpoint must respond within 10 seconds with an `X-Pathao-Merchant-Webhook-Integration-Secret` header whose value equals the webhook secret.
394
443
  4. The HTTP status code should be 2xx.
395
444
 
445
+ > [!WARNING]
446
+ > **Webhooks are not authenticated.** The webhook secret is one fixed value shared by every merchant (it is hardcoded in Pathao's own open-source WooCommerce plugin), so anyone can send a request that looks like it came from Pathao. Never apply status, fee or COD amounts straight from a payload. Use the webhook only as a signal: look up your own order by `consignment_id`, then fetch the real state with `getOrderStatus()` and act on that. An unguessable callback URL (e.g. `/webhooks/pathao/<random-token>`) cuts down junk traffic, since the URL is the one thing only you and Pathao know.
447
+
396
448
  ### Setup requirements
397
449
 
398
450
  - Your URL must be publicly reachable over HTTPS with a valid SSL certificate.
@@ -443,6 +495,12 @@ handler.on("error", (err) => {
443
495
  console.error("Webhook error:", err.message);
444
496
  });
445
497
 
498
+ // Payloads whose `event` isn't a known PathaoWebhookEvent arrive here, never
499
+ // under their own name (so a forged {"event":"error"} can't fire "error").
500
+ handler.on("unknown", (payload) => {
501
+ console.warn("Unrecognised Pathao event:", payload.event);
502
+ });
503
+
446
504
  app.post(
447
505
  "/webhooks/pathao",
448
506
  express.raw({ type: "application/json" }),
@@ -549,6 +607,9 @@ All payloads also include `updated_at` (MySQL datetime) and `timestamp` (ISO 860
549
607
  ```typescript
550
608
  import type {
551
609
  PathaoConfig,
610
+ PathaoClientOptions,
611
+ PathaoErrorKind,
612
+ PathaoLifecycleStatus,
552
613
  PathaoOrderRequest,
553
614
  PathaoOrderResponse,
554
615
  PathaoStoreRequest,
@@ -565,6 +626,7 @@ import type {
565
626
  WebhookEventPayloadMap,
566
627
  OrderDeliveredPayload,
567
628
  OrderReturnIdCreatedPayload,
629
+ UnknownWebhookPayload,
568
630
  PathaoWebhookEvent,
569
631
  } from "pathao-merchant-sdk/webhooks";
570
632
 
@@ -574,6 +636,61 @@ type PaidPayload = WebhookEventPayloadMap[PathaoWebhookEvent.ORDER_PAID];
574
636
 
575
637
  ---
576
638
 
639
+ ## Upgrading to 3.0.0
640
+
641
+ 3.0.0 has **one breaking change**. Most apps need no code changes; check the table below if you catch specific errors or listen for unusual webhook events.
642
+
643
+ ### Required: explicit config no longer reads environment variables
644
+
645
+ In 2.x, `new PathaoApiService(config)` (and `fromConfig()`, `sandbox()`, `production()`) filled any blank field from `PATHAO_*` environment variables and read `PATHAO_TIMEOUT`. In a multi-tenant app, a tenant with a blank field silently used the platform's own Pathao account. In 3.0.0 those constructors use **exactly** the config you pass. Only `fromEnv()` reads the environment.
646
+
647
+ **You are affected if** you leave config fields blank or omit them and rely on the environment to fill them in, or set `PATHAO_TIMEOUT` without using `fromEnv()`.
648
+
649
+ ```typescript
650
+ // 2.x — blank fields came from PATHAO_* env vars
651
+ const pathao = new PathaoApiService({ baseURL: "https://api-hermes.pathao.com" } as PathaoConfig);
652
+ const sandbox = PathaoApiService.sandbox({ clientId: "", clientSecret: "", username: "", password: "" });
653
+
654
+ // 3.0.0 — either read everything from the environment...
655
+ const pathao = PathaoApiService.fromEnv(); // PATHAO_BASE_URL, _CLIENT_ID, _CLIENT_SECRET, _USERNAME, _PASSWORD, _TIMEOUT
656
+
657
+ // ...or pass every field yourself
658
+ const pathao = new PathaoApiService({
659
+ baseURL: process.env.PATHAO_BASE_URL!,
660
+ clientId: process.env.PATHAO_CLIENT_ID!,
661
+ clientSecret: process.env.PATHAO_CLIENT_SECRET!,
662
+ username: process.env.PATHAO_USERNAME!,
663
+ password: process.env.PATHAO_PASSWORD!,
664
+ timeout: 30_000,
665
+ });
666
+ ```
667
+
668
+ If you already pass every field explicitly, nothing changes. A missing field now fails on the first API call with `PathaoApiError` (`kind: "config"`) instead of quietly using another account.
669
+
670
+ ### Behaviour changes to check
671
+
672
+ | Change | Affects you if… | What to do |
673
+ | --- | --- | --- |
674
+ | `createOrder`, `createBulkOrder` and `createStore` are no longer auto-retried on `5xx` | You relied on the SDK to retry failed creates | Retry yourself, but look the order up first: a `5xx` may hide a booking that went through. See [Retries](#retries). |
675
+ | `createOrder` / `createBulkOrder` validate the secondary phone, recipient name (3–100) and address (10–220) before sending | You send data Pathao would have rejected with a `422` | Fix the data. The error is `PathaoApiError` with `kind: "validation"`; bulk errors name the order (`orders[2]: …`). |
676
+ | Phone numbers are normalised before sending, and only operator prefixes `013`–`019` are valid | You send `+880…` / `017-…` (now accepted), or `011…` / `012…` (now rejected; BTRC lists these as unused) | Nothing for real customers' numbers |
677
+ | Webhook events not in `PathaoWebhookEvent` are emitted as `'unknown'` | You call `handler.on("some.event")` for a name the SDK doesn't list | Listen on `'unknown'` and check `payload.event`. The `'webhook'` catch-all still fires for every event. |
678
+ | A webhook `event` must be a string | You process malformed payloads | Nothing; they now throw `PathaoWebhookError` |
679
+ | Any `3xx` response is an error (redirects are never followed) | Your `baseURL` points at something that redirects | Use the final URL |
680
+ | Some error messages were reworded (config errors, `formatPhoneNumber`) | You match on `err.message` | Switch to `err.kind` |
681
+ | ESM projects (`moduleResolution: node16`/`nodenext`) get the ESM type declarations | You worked around the old CJS-typed imports | Remove the workaround |
682
+
683
+ ### New in 3.0.0 (optional)
684
+
685
+ - `err.kind` and `err.retryable` on `PathaoApiError` — see [Error kinds](#error-kinds)
686
+ - `toLifecycleStatus()` / `isFinalLifecycleStatus()` — see [Order lifecycle](#order-lifecycle)
687
+ - `minRequestIntervalMs` option — see [Rate limits](#rate-limits)
688
+ - `PathaoApiService.normalizePhoneNumber()`, `PATHAO_RATE_LIMIT_PER_MINUTE`, `PATHAO_STATUS_RETENTION_DAYS`
689
+ - `order_status` (optional) on order webhook payload types
690
+ - Debug logs no longer include token responses
691
+
692
+ ---
693
+
577
694
  ## Contributing
578
695
 
579
696
  Contributions are welcome. Please open an issue first for significant changes.
@@ -584,6 +701,8 @@ Contributions are welcome. Please open an issue first for significant changes.
584
701
 
585
702
  ## Development
586
703
 
704
+ The repo pins pnpm 10 via `packageManager`. Run `corepack enable` once so `pnpm` uses it; pnpm 11+ ignores the `pnpm` settings in `package.json` and `pnpm install --frozen-lockfile` fails.
705
+
587
706
  ```bash
588
707
  pnpm install
589
708
  pnpm run build # compile CJS + ESM + .d.ts
@@ -604,6 +723,18 @@ Open an issue on [GitHub](https://github.com/sifat07/pathao-merchant-sdk/issues)
604
723
 
605
724
  ## Changelog
606
725
 
726
+ Full history: [CHANGELOG.md](CHANGELOG.md).
727
+
728
+ ### 3.0.0
729
+
730
+ - **Breaking:** explicit config no longer falls back to `PATHAO_*` env vars; use `fromEnv()` — see [Upgrading to 3.0.0](#upgrading-to-300)
731
+ - Order and store creation are never auto-retried on `5xx` (prevents duplicate consignments)
732
+ - Debug logs redact token responses; redirects are never followed
733
+ - Webhooks: unknown event names go to `'unknown'`, never to reserved EventEmitter events
734
+ - Phones normalised (`+880…` accepted); orders fully validated before sending
735
+ - `PathaoApiError.kind` / `.retryable`, `toLifecycleStatus()`, `minRequestIntervalMs`, rate/retention constants
736
+ - Correct ESM types; `/webhooks` resolves under `node10`; CI on pnpm 10 and Node 18–24
737
+
607
738
  ### 2.3.0 — 2026-04-16
608
739
 
609
740
  - Replaced broken `pnpm audit` with OSV Scanner (scoped to production dependencies)
@@ -628,7 +759,7 @@ Open an issue on [GitHub](https://github.com/sifat07/pathao-merchant-sdk/issues)
628
759
  - Factory methods: `fromEnv()`, `fromConfig()`, `sandbox()`, `production()`
629
760
  - Debug logging (`debug` option) — `Authorization` header redacted
630
761
  - Configurable circuit breaker (throws `PathaoApiError` code 503 when open)
631
- - Retry logic: 429 reads `Retry-After`, 5xx exponential backoff (max 2 retries)
762
+ - Retry logic: 429 retried only when the server sends `Retry-After`; 5xx exponential backoff (max 2 retries)
632
763
  - Deferred config validation — constructor never throws
633
764
  - HTTPS enforcement in `validateConfiguration()`
634
765
  - `User-Agent: pathao-merchant-sdk node/<version>` header
package/dist/index.d.mts CHANGED
@@ -1,11 +1,12 @@
1
+ export { P as PATHAO_RATE_LIMIT_PER_MINUTE, a as PATHAO_STATUS_RETENTION_DAYS, b as PathaoLifecycleStatus, i as isFinalLifecycleStatus, t as toLifecycleStatus } from './status-xIApOisZ.mjs';
2
+
1
3
  /**
2
4
  * Pathao Courier API Types
3
5
  *
4
6
  * Official API Details:
5
7
  * - Authentication: OAuth2 with client_id, client_secret, username, password
6
8
  * - All endpoints use /aladdin/api/v1/ prefix
7
- * - Base URL can be set via PATHAO_BASE_URL environment variable or constructor config
8
- * - Timeout can be set via PATHAO_TIMEOUT environment variable or constructor config
9
+ * - Config is passed explicitly, or read from PATHAO_* env vars by fromEnv()
9
10
  */
10
11
  interface PathaoAuthResponse {
11
12
  token_type: string;
@@ -206,8 +207,8 @@ interface PathaoBulkOrderResponse {
206
207
  * API Details (based on public documentation):
207
208
  * - Authentication: OAuth2 with client_id, client_secret, username, password
208
209
  * - All endpoints use /aladdin/api/v1/ prefix
209
- * - Base URL can be set via PATHAO_BASE_URL environment variable or constructor config
210
- * - Timeout can be set via PATHAO_TIMEOUT environment variable or constructor config
210
+ * - The constructor reads only the config it is given. Use fromEnv() to read
211
+ * PATHAO_* environment variables (including PATHAO_TIMEOUT).
211
212
  *
212
213
  * Features implemented:
213
214
  * - Token-based authentication with refresh token support
@@ -218,7 +219,25 @@ interface PathaoBulkOrderResponse {
218
219
  * - City, zone, and area management
219
220
  */
220
221
 
222
+ type PathaoErrorKind =
223
+ /** Input rejected, by this SDK before sending or by Pathao (HTTP 400/422). */
224
+ 'validation'
225
+ /** Missing or wrong baseURL / credentials, caught before any request. */
226
+ | 'config'
227
+ /** Rejected credentials or token (HTTP 401). */
228
+ | 'auth'
229
+ /** HTTP 403. */
230
+ | 'forbidden'
231
+ /** Unknown consignment, store or resource (HTTP 404). */
232
+ | 'not_found'
233
+ /** HTTP 429. Back off; see PATHAO_RATE_LIMIT_PER_MINUTE. */
234
+ | 'rate_limited'
235
+ /** 5xx, timeout, network failure or open circuit breaker. */
236
+ | 'unavailable'
237
+ /** A response this SDK doesn't understand. Inspect `responseData`. */
238
+ | 'unexpected';
221
239
  declare class PathaoApiError extends Error {
240
+ kind: PathaoErrorKind;
222
241
  status: number | undefined;
223
242
  code: number | undefined;
224
243
  type: string | undefined;
@@ -232,12 +251,31 @@ declare class PathaoApiError extends Error {
232
251
  errors?: Record<string, string[]> | undefined;
233
252
  validation?: Record<string, string[]> | undefined;
234
253
  responseData?: unknown;
254
+ kind?: PathaoErrorKind | undefined;
235
255
  });
256
+ /**
257
+ * True for transient failures (`unavailable`, `rate_limited`). For
258
+ * createOrder / createBulkOrder a 5xx may hide a booking that went through:
259
+ * look the order up before retrying a create.
260
+ */
261
+ get retryable(): boolean;
236
262
  }
237
263
  interface CircuitBreakerConfig {
238
264
  threshold?: number;
239
265
  timeout?: number;
240
266
  }
267
+ interface PathaoClientOptions {
268
+ /** Log requests and responses (tokens and Authorization redacted). */
269
+ debug?: boolean;
270
+ circuitBreaker?: CircuitBreakerConfig;
271
+ /**
272
+ * Minimum gap between requests from this instance, in ms. Requests queue
273
+ * instead of tripping Pathao's 60/min limit (PATHAO_RATE_LIMIT_PER_MINUTE).
274
+ * 1500 keeps one instance near 40/min, leaving headroom for other callers
275
+ * sharing the credentials. Default 0 (no spacing).
276
+ */
277
+ minRequestIntervalMs?: number;
278
+ }
241
279
  declare class PathaoApiService {
242
280
  private pathaoClient;
243
281
  private accessToken;
@@ -248,11 +286,10 @@ declare class PathaoApiService {
248
286
  private authPromise;
249
287
  private hasValidated;
250
288
  private debug;
289
+ private minRequestIntervalMs;
290
+ private nextRequestAt;
251
291
  private circuitBreaker;
252
- constructor(config: PathaoConfig, options?: {
253
- debug?: boolean;
254
- circuitBreaker?: CircuitBreakerConfig;
255
- });
292
+ constructor(config: PathaoConfig, options?: PathaoClientOptions);
256
293
  private validateConfiguration;
257
294
  private ensureAuthenticated;
258
295
  private performAuthentication;
@@ -261,6 +298,7 @@ declare class PathaoApiService {
261
298
  private getErrorMessage;
262
299
  private toPathaoApiError;
263
300
  private handleCircuitBreaker;
301
+ private waitForRequestSlot;
264
302
  private delay;
265
303
  createOrder(orderData: PathaoOrderRequest): Promise<PathaoOrderResponse>;
266
304
  createStore(storeData: PathaoStoreRequest): Promise<PathaoStoreCreateResponse>;
@@ -272,6 +310,13 @@ declare class PathaoApiService {
272
310
  getAreas(zoneId: number): Promise<PathaoAreaResponse>;
273
311
  getOrderStatus(consignmentId: string): Promise<PathaoOrderStatusResponse>;
274
312
  createBulkOrder(orders: PathaoOrderRequest[]): Promise<PathaoBulkOrderResponse>;
313
+ private static prepareOrder;
314
+ /**
315
+ * Normalise a Bangladeshi mobile number to 01XXXXXXXXX. Accepts +8801…,
316
+ * 8801… and 01…, with spaces, dashes, dots or parentheses. Returns null for
317
+ * anything that isn't a BD mobile number (operator prefixes 013–019).
318
+ */
319
+ static normalizePhoneNumber(phone: string): string | null;
275
320
  static validatePhoneNumber(phone: string): boolean;
276
321
  static formatPhoneNumber(phone: string): string;
277
322
  static validateAddress(address: string): boolean;
@@ -282,22 +327,10 @@ declare class PathaoApiService {
282
327
  static validateContactNumber(phone: string): boolean;
283
328
  static validateStoreAddress(address: string): boolean;
284
329
  clearAuth(): void;
285
- static fromEnv(options?: {
286
- debug?: boolean;
287
- circuitBreaker?: CircuitBreakerConfig;
288
- }): PathaoApiService;
289
- static fromConfig(config: PathaoConfig, options?: {
290
- debug?: boolean;
291
- circuitBreaker?: CircuitBreakerConfig;
292
- }): PathaoApiService;
293
- static sandbox(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
294
- debug?: boolean;
295
- circuitBreaker?: CircuitBreakerConfig;
296
- }): PathaoApiService;
297
- static production(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
298
- debug?: boolean;
299
- circuitBreaker?: CircuitBreakerConfig;
300
- }): PathaoApiService;
330
+ static fromEnv(options?: PathaoClientOptions): PathaoApiService;
331
+ static fromConfig(config: PathaoConfig, options?: PathaoClientOptions): PathaoApiService;
332
+ static sandbox(credentials: Omit<PathaoConfig, 'baseURL'>, options?: PathaoClientOptions): PathaoApiService;
333
+ static production(credentials: Omit<PathaoConfig, 'baseURL'>, options?: PathaoClientOptions): PathaoApiService;
301
334
  }
302
335
 
303
- export { type CircuitBreakerConfig, DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoBulkOrderResponse, type PathaoCityResponse, type PathaoConfig, type PathaoError, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };
336
+ export { type CircuitBreakerConfig, DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoBulkOrderResponse, type PathaoCityResponse, type PathaoClientOptions, type PathaoConfig, type PathaoError, type PathaoErrorKind, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
+ export { P as PATHAO_RATE_LIMIT_PER_MINUTE, a as PATHAO_STATUS_RETENTION_DAYS, b as PathaoLifecycleStatus, i as isFinalLifecycleStatus, t as toLifecycleStatus } from './status-xIApOisZ.js';
2
+
1
3
  /**
2
4
  * Pathao Courier API Types
3
5
  *
4
6
  * Official API Details:
5
7
  * - Authentication: OAuth2 with client_id, client_secret, username, password
6
8
  * - All endpoints use /aladdin/api/v1/ prefix
7
- * - Base URL can be set via PATHAO_BASE_URL environment variable or constructor config
8
- * - Timeout can be set via PATHAO_TIMEOUT environment variable or constructor config
9
+ * - Config is passed explicitly, or read from PATHAO_* env vars by fromEnv()
9
10
  */
10
11
  interface PathaoAuthResponse {
11
12
  token_type: string;
@@ -206,8 +207,8 @@ interface PathaoBulkOrderResponse {
206
207
  * API Details (based on public documentation):
207
208
  * - Authentication: OAuth2 with client_id, client_secret, username, password
208
209
  * - All endpoints use /aladdin/api/v1/ prefix
209
- * - Base URL can be set via PATHAO_BASE_URL environment variable or constructor config
210
- * - Timeout can be set via PATHAO_TIMEOUT environment variable or constructor config
210
+ * - The constructor reads only the config it is given. Use fromEnv() to read
211
+ * PATHAO_* environment variables (including PATHAO_TIMEOUT).
211
212
  *
212
213
  * Features implemented:
213
214
  * - Token-based authentication with refresh token support
@@ -218,7 +219,25 @@ interface PathaoBulkOrderResponse {
218
219
  * - City, zone, and area management
219
220
  */
220
221
 
222
+ type PathaoErrorKind =
223
+ /** Input rejected, by this SDK before sending or by Pathao (HTTP 400/422). */
224
+ 'validation'
225
+ /** Missing or wrong baseURL / credentials, caught before any request. */
226
+ | 'config'
227
+ /** Rejected credentials or token (HTTP 401). */
228
+ | 'auth'
229
+ /** HTTP 403. */
230
+ | 'forbidden'
231
+ /** Unknown consignment, store or resource (HTTP 404). */
232
+ | 'not_found'
233
+ /** HTTP 429. Back off; see PATHAO_RATE_LIMIT_PER_MINUTE. */
234
+ | 'rate_limited'
235
+ /** 5xx, timeout, network failure or open circuit breaker. */
236
+ | 'unavailable'
237
+ /** A response this SDK doesn't understand. Inspect `responseData`. */
238
+ | 'unexpected';
221
239
  declare class PathaoApiError extends Error {
240
+ kind: PathaoErrorKind;
222
241
  status: number | undefined;
223
242
  code: number | undefined;
224
243
  type: string | undefined;
@@ -232,12 +251,31 @@ declare class PathaoApiError extends Error {
232
251
  errors?: Record<string, string[]> | undefined;
233
252
  validation?: Record<string, string[]> | undefined;
234
253
  responseData?: unknown;
254
+ kind?: PathaoErrorKind | undefined;
235
255
  });
256
+ /**
257
+ * True for transient failures (`unavailable`, `rate_limited`). For
258
+ * createOrder / createBulkOrder a 5xx may hide a booking that went through:
259
+ * look the order up before retrying a create.
260
+ */
261
+ get retryable(): boolean;
236
262
  }
237
263
  interface CircuitBreakerConfig {
238
264
  threshold?: number;
239
265
  timeout?: number;
240
266
  }
267
+ interface PathaoClientOptions {
268
+ /** Log requests and responses (tokens and Authorization redacted). */
269
+ debug?: boolean;
270
+ circuitBreaker?: CircuitBreakerConfig;
271
+ /**
272
+ * Minimum gap between requests from this instance, in ms. Requests queue
273
+ * instead of tripping Pathao's 60/min limit (PATHAO_RATE_LIMIT_PER_MINUTE).
274
+ * 1500 keeps one instance near 40/min, leaving headroom for other callers
275
+ * sharing the credentials. Default 0 (no spacing).
276
+ */
277
+ minRequestIntervalMs?: number;
278
+ }
241
279
  declare class PathaoApiService {
242
280
  private pathaoClient;
243
281
  private accessToken;
@@ -248,11 +286,10 @@ declare class PathaoApiService {
248
286
  private authPromise;
249
287
  private hasValidated;
250
288
  private debug;
289
+ private minRequestIntervalMs;
290
+ private nextRequestAt;
251
291
  private circuitBreaker;
252
- constructor(config: PathaoConfig, options?: {
253
- debug?: boolean;
254
- circuitBreaker?: CircuitBreakerConfig;
255
- });
292
+ constructor(config: PathaoConfig, options?: PathaoClientOptions);
256
293
  private validateConfiguration;
257
294
  private ensureAuthenticated;
258
295
  private performAuthentication;
@@ -261,6 +298,7 @@ declare class PathaoApiService {
261
298
  private getErrorMessage;
262
299
  private toPathaoApiError;
263
300
  private handleCircuitBreaker;
301
+ private waitForRequestSlot;
264
302
  private delay;
265
303
  createOrder(orderData: PathaoOrderRequest): Promise<PathaoOrderResponse>;
266
304
  createStore(storeData: PathaoStoreRequest): Promise<PathaoStoreCreateResponse>;
@@ -272,6 +310,13 @@ declare class PathaoApiService {
272
310
  getAreas(zoneId: number): Promise<PathaoAreaResponse>;
273
311
  getOrderStatus(consignmentId: string): Promise<PathaoOrderStatusResponse>;
274
312
  createBulkOrder(orders: PathaoOrderRequest[]): Promise<PathaoBulkOrderResponse>;
313
+ private static prepareOrder;
314
+ /**
315
+ * Normalise a Bangladeshi mobile number to 01XXXXXXXXX. Accepts +8801…,
316
+ * 8801… and 01…, with spaces, dashes, dots or parentheses. Returns null for
317
+ * anything that isn't a BD mobile number (operator prefixes 013–019).
318
+ */
319
+ static normalizePhoneNumber(phone: string): string | null;
275
320
  static validatePhoneNumber(phone: string): boolean;
276
321
  static formatPhoneNumber(phone: string): string;
277
322
  static validateAddress(address: string): boolean;
@@ -282,22 +327,10 @@ declare class PathaoApiService {
282
327
  static validateContactNumber(phone: string): boolean;
283
328
  static validateStoreAddress(address: string): boolean;
284
329
  clearAuth(): void;
285
- static fromEnv(options?: {
286
- debug?: boolean;
287
- circuitBreaker?: CircuitBreakerConfig;
288
- }): PathaoApiService;
289
- static fromConfig(config: PathaoConfig, options?: {
290
- debug?: boolean;
291
- circuitBreaker?: CircuitBreakerConfig;
292
- }): PathaoApiService;
293
- static sandbox(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
294
- debug?: boolean;
295
- circuitBreaker?: CircuitBreakerConfig;
296
- }): PathaoApiService;
297
- static production(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
298
- debug?: boolean;
299
- circuitBreaker?: CircuitBreakerConfig;
300
- }): PathaoApiService;
330
+ static fromEnv(options?: PathaoClientOptions): PathaoApiService;
331
+ static fromConfig(config: PathaoConfig, options?: PathaoClientOptions): PathaoApiService;
332
+ static sandbox(credentials: Omit<PathaoConfig, 'baseURL'>, options?: PathaoClientOptions): PathaoApiService;
333
+ static production(credentials: Omit<PathaoConfig, 'baseURL'>, options?: PathaoClientOptions): PathaoApiService;
301
334
  }
302
335
 
303
- export { type CircuitBreakerConfig, DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoBulkOrderResponse, type PathaoCityResponse, type PathaoConfig, type PathaoError, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };
336
+ export { type CircuitBreakerConfig, DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoBulkOrderResponse, type PathaoCityResponse, type PathaoClientOptions, type PathaoConfig, type PathaoError, type PathaoErrorKind, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };