@lacspace/courier 1.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/LICENSE +51 -0
- package/README.md +148 -0
- package/dist/index.cjs +301 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +170 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Lacspace Free Licence
|
|
2
|
+
Version 1.0, August 2026
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Lacspace
|
|
5
|
+
|
|
6
|
+
PREAMBLE
|
|
7
|
+
|
|
8
|
+
This software is published by Lacspace under the Lacspace Free Licence — a free,
|
|
9
|
+
permissive licence that lets you use this software for any purpose, including in
|
|
10
|
+
commercial products and services, at no cost. It grants the same freedoms as
|
|
11
|
+
common permissive open-source licences; the only condition is that this notice
|
|
12
|
+
travels with the software. The canonical, always-current text of this licence is
|
|
13
|
+
maintained at https://lacspace.com/licenses/lacspace-free-1.0
|
|
14
|
+
|
|
15
|
+
GRANT OF RIGHTS
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person or organisation
|
|
18
|
+
obtaining a copy of this software and its associated documentation and data files
|
|
19
|
+
(the "Software"), to deal in the Software without restriction, including without
|
|
20
|
+
limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
21
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
22
|
+
Software is furnished to do so, subject to the conditions below. These rights are
|
|
23
|
+
granted for any purpose, personal or commercial, and are perpetual, worldwide,
|
|
24
|
+
non-exclusive, and royalty-free.
|
|
25
|
+
|
|
26
|
+
CONDITIONS
|
|
27
|
+
|
|
28
|
+
The above copyright notice, this permission notice, and the name of this licence
|
|
29
|
+
("Lacspace Free Licence") shall be included in all copies or substantial portions
|
|
30
|
+
of the Software.
|
|
31
|
+
|
|
32
|
+
TRADEMARKS
|
|
33
|
+
|
|
34
|
+
This licence does not grant permission to use the trade names, trademarks, service
|
|
35
|
+
marks, logos, or product names of Lacspace, except as required to reproduce the
|
|
36
|
+
notice above or to describe the origin of the Software in a truthful manner.
|
|
37
|
+
|
|
38
|
+
DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
42
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
43
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
|
|
44
|
+
AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
|
|
45
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
The Lacspace Free Licence is a source-available, permissive licence and is not (as
|
|
50
|
+
of this version) an OSI-approved licence. In substance it grants the same freedoms
|
|
51
|
+
as the MIT Licence. Learn more at https://lacspace.com/licenses
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @lacspace/courier
|
|
4
|
+
|
|
5
|
+
**Courier / last-mile delivery toolkit — a canonical delivery state machine, a Pathao (Nepal) adapter, and inbound webhook verification + status normalization. Over global `fetch` and Web Crypto.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lacspace/courier)
|
|
8
|
+
[](https://packagephobia.com/result?p=@lacspace/courier)
|
|
9
|
+
[](https://bundlephobia.com/package/@lacspace/courier)
|
|
10
|
+
[](https://www.npmjs.com/package/@lacspace/courier)
|
|
11
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
> Stop clicking Confirmed → Pickup → Transit → Delivered by hand. This is the courier layer for a multi-vendor shop: one **canonical delivery state machine**, a **Pathao "Aladdin" Merchant API v1** adapter (token auto-refresh, order creation, price/city/zone/area lookups), and **inbound webhooks** — verify the shared-secret / HMAC signature and normalize any carrier event into your own status vocabulary. Zero dependencies, isomorphic, fully typed.
|
|
16
|
+
|
|
17
|
+
- 🚦 **State machine** — one `DeliveryStatus` vocabulary + guarded `transition()` that refuses illegal jumps and marks terminal states
|
|
18
|
+
- 🇳🇵 **Pathao adapter** — `issueToken` (cached + auto-refresh), `createOrder`, `priceCalculation`, `cities`/`zones`/`areas`
|
|
19
|
+
- 📡 **Inbound webhooks** — `verifyWebhookSignature` (HMAC-SHA256, timing-safe) + `parsePathaoWebhook` / `normalizePathaoStatus`
|
|
20
|
+
- 🔌 **Adapter-shaped** — code against `CourierAdapter`; swap or add carriers without touching your order flow
|
|
21
|
+
- ⚡ Isomorphic — Node 18+, edge runtimes & browsers · global `fetch` + Web Crypto only · 📦 ESM + CJS · zero deps
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @lacspace/courier # or pnpm add / yarn add / bun add
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## The delivery state machine
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { transition, canTransition, isTerminal } from "@lacspace/courier";
|
|
33
|
+
|
|
34
|
+
const order = { id: "A1", status: "confirmed" as const };
|
|
35
|
+
|
|
36
|
+
const next = transition(order, "picked_up"); // → new object, status "picked_up"
|
|
37
|
+
canTransition("pending", "delivered"); // false — can't skip the chain
|
|
38
|
+
isTerminal("delivered"); // true
|
|
39
|
+
|
|
40
|
+
transition({ status: "delivered" as const }, "returned");
|
|
41
|
+
// throws CourierError { code: "illegal_transition" }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`transition()` never mutates — it returns a shallow copy with the new `status`. The allowed moves live in `DELIVERY_TRANSITIONS`.
|
|
45
|
+
|
|
46
|
+
## Pathao adapter
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createPathaoAdapter, PATHAO_SANDBOX_BASE_URL } from "@lacspace/courier";
|
|
50
|
+
|
|
51
|
+
const pathao = createPathaoAdapter({
|
|
52
|
+
clientId: process.env.PATHAO_CLIENT_ID!,
|
|
53
|
+
clientSecret: process.env.PATHAO_CLIENT_SECRET!,
|
|
54
|
+
username: process.env.PATHAO_USERNAME!,
|
|
55
|
+
password: process.env.PATHAO_PASSWORD!,
|
|
56
|
+
storeId: Number(process.env.PATHAO_STORE_ID),
|
|
57
|
+
// baseUrl defaults to production api-hermes.pathao.com;
|
|
58
|
+
// use PATHAO_SANDBOX_BASE_URL for the courier-api-sandbox host.
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const shipment = await pathao.createOrder({
|
|
62
|
+
recipientName: "Ram Thapa",
|
|
63
|
+
recipientPhone: "9800000000",
|
|
64
|
+
recipientAddress: "Baneshwor, Kathmandu",
|
|
65
|
+
cityId: 1, zoneId: 2, areaId: 3,
|
|
66
|
+
amountToCollect: 1500, // COD; 0 for prepaid
|
|
67
|
+
itemQuantity: 1,
|
|
68
|
+
itemWeight: 0.5, // kg
|
|
69
|
+
description: "T-shirt",
|
|
70
|
+
merchantOrderId: "SHOP-42",
|
|
71
|
+
});
|
|
72
|
+
// shipment.trackingId === Pathao consignment_id, status "confirmed"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The token is issued lazily, cached, and auto-refreshed a minute before it expires. `priceCalculation()`, `cities()`, `zones(cityId)` and `areas(zoneId)` are also exposed.
|
|
76
|
+
|
|
77
|
+
> **Note:** Pathao has no clean public track-by-consignment endpoint in v1. `track()` throws `CourierError { code: "unsupported" }` on purpose — Pathao reports status via **webhooks** (below).
|
|
78
|
+
|
|
79
|
+
## Inbound webhooks
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import {
|
|
83
|
+
verifyPathaoWebhook,
|
|
84
|
+
verifyWebhookSignature,
|
|
85
|
+
parsePathaoWebhook,
|
|
86
|
+
PATHAO_WEBHOOK_ACK_HEADER,
|
|
87
|
+
transition,
|
|
88
|
+
} from "@lacspace/courier";
|
|
89
|
+
|
|
90
|
+
// In your webhook route (raw body string in hand):
|
|
91
|
+
if (!verifyPathaoWebhook({ headerSecret: req.header("X-PATHAO-Signature"), expectedSecret: SECRET })) {
|
|
92
|
+
return res.status(401).end();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const evt = parsePathaoWebhook(rawBody); // { event, status, consignmentId, merchantOrderId, raw }
|
|
96
|
+
const order = await db.orders.findByConsignment(evt.consignmentId);
|
|
97
|
+
await db.orders.save(transition(order, evt.status)); // guarded advance
|
|
98
|
+
|
|
99
|
+
// Pathao expects a 202 that echoes the integration secret back:
|
|
100
|
+
res.setHeader(PATHAO_WEBHOOK_ACK_HEADER, SECRET).status(202).end();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For carriers that sign the body (rather than a shared header), use the generic HMAC verifier:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const ok = await verifyWebhookSignature(rawBody, signatureHeader, secret); // HMAC-SHA256 hex, timing-safe
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API
|
|
110
|
+
|
|
111
|
+
| Export | Description |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `DeliveryStatus` | `pending \| confirmed \| picked_up \| in_transit \| out_for_delivery \| delivered \| returned \| cancelled \| failed \| on_hold` |
|
|
114
|
+
| `DELIVERY_TRANSITIONS` | `Record<DeliveryStatus, DeliveryStatus[]>` — allowed forward moves |
|
|
115
|
+
| `canTransition(from, to)` / `isTerminal(s)` | state-machine guards |
|
|
116
|
+
| `transition(order, to)` | new object with updated `status`; throws on illegal move |
|
|
117
|
+
| `CourierError` | `Error` with optional `code` / `status` |
|
|
118
|
+
| `CourierAdapter` / `CourierShipment` / `CreateOrderInput` | carrier-agnostic contract |
|
|
119
|
+
| `createPathaoAdapter(config)` | Pathao adapter + `issueToken` / `priceCalculation` / `cities` / `zones` / `areas` |
|
|
120
|
+
| `PATHAO_PROD_BASE_URL` / `PATHAO_SANDBOX_BASE_URL` | Pathao hosts |
|
|
121
|
+
| `verifyWebhookSignature(payload, signature, secret)` | HMAC-SHA256 hex, timing-safe |
|
|
122
|
+
| `verifyPathaoWebhook({ headerSecret, expectedSecret })` | timing-safe shared-secret compare |
|
|
123
|
+
| `parsePathaoWebhook(body)` / `normalizePathaoStatus(event)` | event → canonical status |
|
|
124
|
+
| `PATHAO_STATUS_MAP` / `PATHAO_WEBHOOK_ACK_HEADER` | Pathao event map + ack header name |
|
|
125
|
+
|
|
126
|
+
All crypto uses Web Crypto (`globalThis.crypto.subtle`) — never hand-rolled. `timingSafeEqual` is exported too.
|
|
127
|
+
|
|
128
|
+
## Licensing
|
|
129
|
+
|
|
130
|
+
This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
|
|
131
|
+
|
|
132
|
+
Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
|
|
133
|
+
|
|
134
|
+
<!-- LACSPACE-DEV-PLATFORM -->
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## The Lacspace Developer Platform
|
|
139
|
+
|
|
140
|
+
`@lacspace/courier` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
141
|
+
|
|
142
|
+
- 🗂️ **All packages** — https://developer.lacspace.com/packages
|
|
143
|
+
- 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
|
|
144
|
+
- 🧪 **Live playground** — https://developer.lacspace.com/playground
|
|
145
|
+
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
146
|
+
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
147
|
+
|
|
148
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var DELIVERY_TRANSITIONS = {
|
|
5
|
+
pending: ["confirmed", "cancelled"],
|
|
6
|
+
confirmed: ["picked_up", "cancelled", "on_hold"],
|
|
7
|
+
picked_up: ["in_transit", "returned", "on_hold"],
|
|
8
|
+
in_transit: ["out_for_delivery", "returned", "failed", "on_hold"],
|
|
9
|
+
out_for_delivery: ["delivered", "failed", "returned"],
|
|
10
|
+
on_hold: ["in_transit", "cancelled", "returned"],
|
|
11
|
+
delivered: [],
|
|
12
|
+
returned: [],
|
|
13
|
+
cancelled: [],
|
|
14
|
+
failed: []
|
|
15
|
+
};
|
|
16
|
+
function canTransition(from, to) {
|
|
17
|
+
return DELIVERY_TRANSITIONS[from].includes(to);
|
|
18
|
+
}
|
|
19
|
+
function isTerminal(s) {
|
|
20
|
+
return DELIVERY_TRANSITIONS[s].length === 0;
|
|
21
|
+
}
|
|
22
|
+
var CourierError = class extends Error {
|
|
23
|
+
constructor(message, opts) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "CourierError";
|
|
26
|
+
this.code = opts?.code;
|
|
27
|
+
this.status = opts?.status;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function transition(order, to) {
|
|
31
|
+
if (!canTransition(order.status, to)) {
|
|
32
|
+
throw new CourierError(
|
|
33
|
+
`Illegal delivery transition: ${order.status} \u2192 ${to}`,
|
|
34
|
+
{ code: "illegal_transition" }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return { ...order, status: to };
|
|
38
|
+
}
|
|
39
|
+
var enc = new TextEncoder();
|
|
40
|
+
function getSubtle() {
|
|
41
|
+
const subtle = globalThis.crypto?.subtle;
|
|
42
|
+
if (!subtle) {
|
|
43
|
+
throw new CourierError(
|
|
44
|
+
"@lacspace/courier: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.",
|
|
45
|
+
{ code: "no_web_crypto" }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return subtle;
|
|
49
|
+
}
|
|
50
|
+
function toHex(bytes) {
|
|
51
|
+
let out = "";
|
|
52
|
+
for (let i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, "0");
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
async function hmacSha256Hex(secret, message) {
|
|
56
|
+
const subtle = getSubtle();
|
|
57
|
+
const key = await subtle.importKey(
|
|
58
|
+
"raw",
|
|
59
|
+
enc.encode(secret),
|
|
60
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
61
|
+
false,
|
|
62
|
+
["sign"]
|
|
63
|
+
);
|
|
64
|
+
const sig = await subtle.sign("HMAC", key, enc.encode(message));
|
|
65
|
+
return toHex(new Uint8Array(sig));
|
|
66
|
+
}
|
|
67
|
+
function timingSafeEqual(a, b) {
|
|
68
|
+
const ab = enc.encode(a);
|
|
69
|
+
const bb = enc.encode(b);
|
|
70
|
+
let diff = ab.length ^ bb.length;
|
|
71
|
+
const n = Math.max(ab.length, bb.length);
|
|
72
|
+
for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
|
|
73
|
+
return diff === 0;
|
|
74
|
+
}
|
|
75
|
+
async function verifyWebhookSignature(payload, signature, secret) {
|
|
76
|
+
const expected = await hmacSha256Hex(secret, payload);
|
|
77
|
+
return timingSafeEqual(expected, signature.trim().toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
var PATHAO_PROD_BASE_URL = "https://api-hermes.pathao.com";
|
|
80
|
+
var PATHAO_SANDBOX_BASE_URL = "https://courier-api-sandbox.pathao.com";
|
|
81
|
+
var TOKEN_SKEW_MS = 6e4;
|
|
82
|
+
function createPathaoAdapter(config) {
|
|
83
|
+
const baseUrl = (config.baseUrl ?? PATHAO_PROD_BASE_URL).replace(/\/+$/, "");
|
|
84
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
85
|
+
if (typeof doFetch !== "function") {
|
|
86
|
+
throw new CourierError(
|
|
87
|
+
"@lacspace/courier: global fetch is unavailable; pass config.fetch.",
|
|
88
|
+
{ code: "no_fetch" }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
let token = null;
|
|
92
|
+
async function readError(res) {
|
|
93
|
+
let body;
|
|
94
|
+
try {
|
|
95
|
+
body = await res.json();
|
|
96
|
+
} catch {
|
|
97
|
+
try {
|
|
98
|
+
body = await res.text();
|
|
99
|
+
} catch {
|
|
100
|
+
body = void 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (body && typeof body === "object") {
|
|
104
|
+
const rec = body;
|
|
105
|
+
const msg = rec.message ?? rec.error ?? rec.errors;
|
|
106
|
+
if (msg) return typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
107
|
+
}
|
|
108
|
+
if (typeof body === "string" && body) return body;
|
|
109
|
+
return res.statusText || `HTTP ${res.status}`;
|
|
110
|
+
}
|
|
111
|
+
async function issueToken() {
|
|
112
|
+
const res = await doFetch(`${baseUrl}/aladdin/api/v1/issue-token`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
Accept: "application/json",
|
|
116
|
+
"Content-Type": "application/json"
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
client_id: config.clientId,
|
|
120
|
+
client_secret: config.clientSecret,
|
|
121
|
+
grant_type: "password",
|
|
122
|
+
username: config.username,
|
|
123
|
+
password: config.password
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
throw new CourierError(`Pathao token request failed: ${await readError(res)}`, {
|
|
128
|
+
code: "token_failed",
|
|
129
|
+
status: res.status
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
if (!data.access_token) {
|
|
134
|
+
throw new CourierError("Pathao token response had no access_token.", {
|
|
135
|
+
code: "token_failed",
|
|
136
|
+
status: res.status
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const ttlMs = (typeof data.expires_in === "number" ? data.expires_in : 3600) * 1e3;
|
|
140
|
+
token = { accessToken: data.access_token, expiresAt: Date.now() + ttlMs };
|
|
141
|
+
return token.accessToken;
|
|
142
|
+
}
|
|
143
|
+
async function getToken() {
|
|
144
|
+
if (token && Date.now() < token.expiresAt - TOKEN_SKEW_MS) return token.accessToken;
|
|
145
|
+
return issueToken();
|
|
146
|
+
}
|
|
147
|
+
async function authed(path, init) {
|
|
148
|
+
const bearer = await getToken();
|
|
149
|
+
const res = await doFetch(`${baseUrl}${path}`, {
|
|
150
|
+
method: init.method,
|
|
151
|
+
headers: {
|
|
152
|
+
Accept: "application/json",
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
Authorization: `Bearer ${bearer}`
|
|
155
|
+
},
|
|
156
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
throw new CourierError(`Pathao ${init.method} ${path} failed: ${await readError(res)}`, {
|
|
160
|
+
code: "request_failed",
|
|
161
|
+
status: res.status
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return await res.json();
|
|
165
|
+
}
|
|
166
|
+
async function createOrder(input) {
|
|
167
|
+
const body = {
|
|
168
|
+
store_id: config.storeId,
|
|
169
|
+
recipient_name: input.recipientName,
|
|
170
|
+
recipient_phone: input.recipientPhone,
|
|
171
|
+
recipient_address: input.recipientAddress,
|
|
172
|
+
recipient_city: input.cityId,
|
|
173
|
+
recipient_zone: input.zoneId,
|
|
174
|
+
recipient_area: input.areaId,
|
|
175
|
+
delivery_type: 48,
|
|
176
|
+
item_type: 2,
|
|
177
|
+
special_instruction: input.specialInstruction,
|
|
178
|
+
item_quantity: input.itemQuantity,
|
|
179
|
+
item_weight: input.itemWeight,
|
|
180
|
+
amount_to_collect: input.amountToCollect,
|
|
181
|
+
item_description: input.description
|
|
182
|
+
};
|
|
183
|
+
if (input.merchantOrderId !== void 0) body.merchant_order_id = input.merchantOrderId;
|
|
184
|
+
const data = await authed("/aladdin/api/v1/orders", { method: "POST", body });
|
|
185
|
+
const consignmentId = data.data?.consignment_id;
|
|
186
|
+
if (!consignmentId) {
|
|
187
|
+
throw new CourierError("Pathao order response had no consignment_id.", {
|
|
188
|
+
code: "request_failed"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
trackingId: consignmentId,
|
|
193
|
+
status: "confirmed",
|
|
194
|
+
carrier: "pathao",
|
|
195
|
+
raw: data
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async function track(_trackingId) {
|
|
199
|
+
throw new CourierError(
|
|
200
|
+
"Pathao reports delivery status via webhooks, not a public track endpoint; use verifyPathaoWebhook + parsePathaoWebhook.",
|
|
201
|
+
{ code: "unsupported" }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
name: "pathao",
|
|
206
|
+
issueToken,
|
|
207
|
+
createOrder,
|
|
208
|
+
track,
|
|
209
|
+
priceCalculation: (input) => authed("/aladdin/api/v1/merchant/price-plan", {
|
|
210
|
+
method: "POST",
|
|
211
|
+
body: {
|
|
212
|
+
store_id: input.storeId ?? config.storeId,
|
|
213
|
+
item_type: input.itemType ?? 2,
|
|
214
|
+
delivery_type: input.deliveryType ?? 48,
|
|
215
|
+
item_weight: input.itemWeight,
|
|
216
|
+
recipient_city: input.recipientCity,
|
|
217
|
+
recipient_zone: input.recipientZone
|
|
218
|
+
}
|
|
219
|
+
}),
|
|
220
|
+
cities: () => authed("/aladdin/api/v1/city-list", { method: "GET" }),
|
|
221
|
+
zones: (cityId) => authed(`/aladdin/api/v1/cities/${cityId}/zone-list`, { method: "GET" }),
|
|
222
|
+
areas: (zoneId) => authed(`/aladdin/api/v1/zones/${zoneId}/area-list`, { method: "GET" })
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
var PATHAO_WEBHOOK_ACK_HEADER = "X-Pathao-Merchant-Webhook-Integration-Secret";
|
|
226
|
+
var PATHAO_STATUS_MAP = {
|
|
227
|
+
"order.created": "confirmed",
|
|
228
|
+
"order.updated": "confirmed",
|
|
229
|
+
"order.pickup-requested": "confirmed",
|
|
230
|
+
"order.assigned-for-pickup": "confirmed",
|
|
231
|
+
"order.picked": "picked_up",
|
|
232
|
+
"order.pickup-failed": "failed",
|
|
233
|
+
"order.at-the-sorting-hub": "in_transit",
|
|
234
|
+
"order.in-transit": "in_transit",
|
|
235
|
+
"order.received-at-last-mile-hub": "in_transit",
|
|
236
|
+
"order.assigned-for-delivery": "out_for_delivery",
|
|
237
|
+
"order.delivered": "delivered",
|
|
238
|
+
"order.partial-delivery": "delivered",
|
|
239
|
+
"order.delivery-failed": "failed",
|
|
240
|
+
"order.on-hold": "on_hold",
|
|
241
|
+
"order.returning": "returned",
|
|
242
|
+
"order.return": "returned",
|
|
243
|
+
"order.returned": "returned",
|
|
244
|
+
"order.exchanged": "delivered",
|
|
245
|
+
"order.paid": "delivered",
|
|
246
|
+
"order.paid-return": "returned"
|
|
247
|
+
};
|
|
248
|
+
function normalizePathaoStatus(event) {
|
|
249
|
+
return PATHAO_STATUS_MAP[event];
|
|
250
|
+
}
|
|
251
|
+
function parsePathaoWebhook(body) {
|
|
252
|
+
let obj;
|
|
253
|
+
if (typeof body === "string") {
|
|
254
|
+
try {
|
|
255
|
+
obj = JSON.parse(body);
|
|
256
|
+
} catch {
|
|
257
|
+
throw new CourierError("Pathao webhook body is not valid JSON.", { code: "invalid_body" });
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
obj = body;
|
|
261
|
+
}
|
|
262
|
+
const event = obj.event;
|
|
263
|
+
if (typeof event !== "string" || !event) {
|
|
264
|
+
throw new CourierError("Pathao webhook has no 'event' field.", { code: "invalid_body" });
|
|
265
|
+
}
|
|
266
|
+
const status = normalizePathaoStatus(event);
|
|
267
|
+
if (!status) {
|
|
268
|
+
throw new CourierError(`Unknown Pathao webhook event: ${event}`, { code: "unknown_event" });
|
|
269
|
+
}
|
|
270
|
+
const consignmentId = obj.consignment_id;
|
|
271
|
+
const merchantOrderId = obj.merchant_order_id;
|
|
272
|
+
return {
|
|
273
|
+
event,
|
|
274
|
+
status,
|
|
275
|
+
consignmentId: typeof consignmentId === "string" ? consignmentId : void 0,
|
|
276
|
+
merchantOrderId: typeof merchantOrderId === "string" ? merchantOrderId : void 0,
|
|
277
|
+
raw: obj
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function verifyPathaoWebhook(opts) {
|
|
281
|
+
if (!opts.headerSecret) return false;
|
|
282
|
+
return timingSafeEqual(opts.headerSecret, opts.expectedSecret);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
exports.CourierError = CourierError;
|
|
286
|
+
exports.DELIVERY_TRANSITIONS = DELIVERY_TRANSITIONS;
|
|
287
|
+
exports.PATHAO_PROD_BASE_URL = PATHAO_PROD_BASE_URL;
|
|
288
|
+
exports.PATHAO_SANDBOX_BASE_URL = PATHAO_SANDBOX_BASE_URL;
|
|
289
|
+
exports.PATHAO_STATUS_MAP = PATHAO_STATUS_MAP;
|
|
290
|
+
exports.PATHAO_WEBHOOK_ACK_HEADER = PATHAO_WEBHOOK_ACK_HEADER;
|
|
291
|
+
exports.canTransition = canTransition;
|
|
292
|
+
exports.createPathaoAdapter = createPathaoAdapter;
|
|
293
|
+
exports.isTerminal = isTerminal;
|
|
294
|
+
exports.normalizePathaoStatus = normalizePathaoStatus;
|
|
295
|
+
exports.parsePathaoWebhook = parsePathaoWebhook;
|
|
296
|
+
exports.timingSafeEqual = timingSafeEqual;
|
|
297
|
+
exports.transition = transition;
|
|
298
|
+
exports.verifyPathaoWebhook = verifyPathaoWebhook;
|
|
299
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
300
|
+
//# sourceMappingURL=index.cjs.map
|
|
301
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/courier\n *\n * Courier / last-mile delivery toolkit for multi-vendor commerce. Three things\n * every delivery integration re-implements — done once, correctly:\n *\n * 1. **A canonical delivery state machine** — one vocabulary of statuses\n * (`pending → confirmed → picked_up → in_transit → out_for_delivery →\n * delivered`, plus `returned / cancelled / failed / on_hold`) with a\n * guarded `transition()` that refuses illegal jumps.\n * 2. **A Pathao (Nepal) adapter** — the Pathao \"Aladdin\" Merchant API v1:\n * token issue + auto-refresh, order creation, price/city/zone/area lookups.\n * 3. **Inbound webhooks** — verify a webhook's shared-secret / HMAC signature\n * (timing-safe, over Web Crypto) and normalize a carrier's event name into\n * a canonical `DeliveryStatus` so Confirmed → Delivered stops being a\n * manual admin click.\n *\n * Built on global `fetch` and Web Crypto (`globalThis.crypto.subtle`) — no Node\n * built-ins, no dependencies. Isomorphic: Node 18+, edge runtimes and browsers.\n */\n\n/* ------------------------------------------------------------------ *\n * Canonical delivery status + state machine\n * ------------------------------------------------------------------ */\n\n/** The one delivery vocabulary every carrier is normalized into. */\nexport type DeliveryStatus =\n | \"pending\"\n | \"confirmed\"\n | \"picked_up\"\n | \"in_transit\"\n | \"out_for_delivery\"\n | \"delivered\"\n | \"returned\"\n | \"cancelled\"\n | \"failed\"\n | \"on_hold\";\n\n/**\n * Allowed forward transitions for each status. Terminal states map to `[]`.\n * The set is deliberately permissive on the unhappy paths (returns, holds,\n * failures) but forbids skipping or reversing the happy path.\n */\nexport const DELIVERY_TRANSITIONS: Record<DeliveryStatus, DeliveryStatus[]> = {\n pending: [\"confirmed\", \"cancelled\"],\n confirmed: [\"picked_up\", \"cancelled\", \"on_hold\"],\n picked_up: [\"in_transit\", \"returned\", \"on_hold\"],\n in_transit: [\"out_for_delivery\", \"returned\", \"failed\", \"on_hold\"],\n out_for_delivery: [\"delivered\", \"failed\", \"returned\"],\n on_hold: [\"in_transit\", \"cancelled\", \"returned\"],\n delivered: [],\n returned: [],\n cancelled: [],\n failed: [],\n};\n\n/** `true` if `to` is a legal next state from `from`. */\nexport function canTransition(from: DeliveryStatus, to: DeliveryStatus): boolean {\n return DELIVERY_TRANSITIONS[from].includes(to);\n}\n\n/** `true` if `s` is a terminal state with no further transitions. */\nexport function isTerminal(s: DeliveryStatus): boolean {\n return DELIVERY_TRANSITIONS[s].length === 0;\n}\n\n/** Error thrown by every part of this toolkit; carries an optional `code`/`status`. */\nexport class CourierError extends Error {\n code?: string;\n status?: number;\n constructor(message: string, opts?: { code?: string; status?: number }) {\n super(message);\n this.name = \"CourierError\";\n this.code = opts?.code;\n this.status = opts?.status;\n }\n}\n\n/**\n * Return a shallow copy of `order` with its `status` advanced to `to`.\n * Throws `CourierError` (code `illegal_transition`) if the move is not allowed.\n */\nexport function transition<T extends { status: DeliveryStatus }>(order: T, to: DeliveryStatus): T {\n if (!canTransition(order.status, to)) {\n throw new CourierError(\n `Illegal delivery transition: ${order.status} → ${to}`,\n { code: \"illegal_transition\" },\n );\n }\n return { ...order, status: to };\n}\n\n/* ------------------------------------------------------------------ *\n * Generic courier adapter interface\n * ------------------------------------------------------------------ */\n\n/** A carrier-agnostic view of one shipment. */\nexport interface CourierShipment {\n trackingId: string;\n status: DeliveryStatus;\n carrier: string;\n /** The raw carrier response, kept for auditing / debugging. */\n raw?: unknown;\n}\n\n/** Generic order input; a reasonable superset across carriers. */\nexport interface CreateOrderInput {\n recipientName: string;\n recipientPhone: string;\n recipientAddress: string;\n /** Carrier location ids (Pathao uses numeric city/zone/area). */\n cityId?: number;\n zoneId?: number;\n areaId?: number;\n /** Cash-on-delivery amount to collect (0 for prepaid). */\n amountToCollect: number;\n itemQuantity: number;\n /** Weight in kilograms. */\n itemWeight: number;\n description: string;\n specialInstruction?: string;\n /** Optional merchant-side order id echoed back for reconciliation. */\n merchantOrderId?: string;\n}\n\n/** The minimum every carrier adapter implements. */\nexport interface CourierAdapter {\n name: string;\n createOrder(input: CreateOrderInput): Promise<CourierShipment>;\n track(trackingId: string): Promise<CourierShipment>;\n}\n\n/* ------------------------------------------------------------------ *\n * Web Crypto + hex helpers (isomorphic, zero-dependency)\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\n\nfunction getSubtle(): SubtleCrypto {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new CourierError(\n \"@lacspace/courier: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.\",\n { code: \"no_web_crypto\" },\n );\n }\n return subtle;\n}\n\n/** Lowercase hex string of raw bytes. */\nfunction toHex(bytes: Uint8Array): string {\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) out += bytes[i]!.toString(16).padStart(2, \"0\");\n return out;\n}\n\n/** HMAC-SHA256 over `message` with `secret`, returned as lowercase hex. */\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const subtle = getSubtle();\n const key = await subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await subtle.sign(\"HMAC\", key, enc.encode(message));\n return toHex(new Uint8Array(sig));\n}\n\n/**\n * Constant-time comparison of two strings. Accumulates the difference over the\n * maximum length so it never early-returns on a length or character mismatch.\n */\nexport function timingSafeEqual(a: string, b: string): boolean {\n const ab = enc.encode(a);\n const bb = enc.encode(b);\n let diff = ab.length ^ bb.length;\n const n = Math.max(ab.length, bb.length);\n for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);\n return diff === 0;\n}\n\n/* ------------------------------------------------------------------ *\n * Generic webhook signature verification\n * ------------------------------------------------------------------ */\n\n/**\n * Verify an inbound webhook's `HMAC-SHA256` hex signature over the *raw*\n * request body string. Timing-safe; never throws on a bad signature.\n */\nexport async function verifyWebhookSignature(\n payload: string,\n signature: string,\n secret: string,\n): Promise<boolean> {\n const expected = await hmacSha256Hex(secret, payload);\n return timingSafeEqual(expected, signature.trim().toLowerCase());\n}\n\n/* ------------------------------------------------------------------ *\n * Pathao \"Aladdin\" Merchant API v1 adapter\n * ------------------------------------------------------------------ */\n\n/**\n * Configuration for the Pathao adapter.\n *\n * `baseUrl` defaults to production `https://api-hermes.pathao.com`. For the\n * Pathao sandbox use `https://courier-api-sandbox.pathao.com`.\n */\nexport interface PathaoConfig {\n baseUrl?: string;\n clientId: string;\n clientSecret: string;\n username: string;\n password: string;\n storeId: number;\n /** Inject a `fetch` implementation (defaults to global `fetch`). */\n fetch?: typeof fetch;\n}\n\n/** Production base URL for the Pathao Merchant API. */\nexport const PATHAO_PROD_BASE_URL = \"https://api-hermes.pathao.com\";\n/** Sandbox base URL for the Pathao Merchant API. */\nexport const PATHAO_SANDBOX_BASE_URL = \"https://courier-api-sandbox.pathao.com\";\n\nexport interface PathaoPriceInput {\n itemType?: number;\n deliveryType?: number;\n itemWeight: number;\n recipientCity: number;\n recipientZone: number;\n storeId?: number;\n}\n\n/** The extra Pathao-specific methods surfaced beyond the generic adapter. */\nexport interface PathaoAdapter extends CourierAdapter {\n issueToken(): Promise<string>;\n priceCalculation(input: PathaoPriceInput): Promise<Record<string, unknown>>;\n cities(): Promise<Record<string, unknown>>;\n zones(cityId: number): Promise<Record<string, unknown>>;\n areas(zoneId: number): Promise<Record<string, unknown>>;\n}\n\ninterface TokenState {\n accessToken: string;\n /** Absolute epoch ms at which the token should be considered expired. */\n expiresAt: number;\n}\n\n/** Refresh the token this many ms before it actually expires. */\nconst TOKEN_SKEW_MS = 60_000;\n\n/**\n * Create a Pathao Merchant API v1 adapter. Handles token issue + auto-refresh\n * and maps the generic `CreateOrderInput` onto Pathao's order body.\n */\nexport function createPathaoAdapter(config: PathaoConfig): PathaoAdapter {\n const baseUrl = (config.baseUrl ?? PATHAO_PROD_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch = config.fetch ?? globalThis.fetch;\n if (typeof doFetch !== \"function\") {\n throw new CourierError(\n \"@lacspace/courier: global fetch is unavailable; pass config.fetch.\",\n { code: \"no_fetch\" },\n );\n }\n\n let token: TokenState | null = null;\n\n async function readError(res: Response): Promise<string> {\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n try {\n body = await res.text();\n } catch {\n body = undefined;\n }\n }\n if (body && typeof body === \"object\") {\n const rec = body as Record<string, unknown>;\n const msg = rec.message ?? rec.error ?? rec.errors;\n if (msg) return typeof msg === \"string\" ? msg : JSON.stringify(msg);\n }\n if (typeof body === \"string\" && body) return body;\n return res.statusText || `HTTP ${res.status}`;\n }\n\n async function issueToken(): Promise<string> {\n const res = await doFetch(`${baseUrl}/aladdin/api/v1/issue-token`, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n grant_type: \"password\",\n username: config.username,\n password: config.password,\n }),\n });\n if (!res.ok) {\n throw new CourierError(`Pathao token request failed: ${await readError(res)}`, {\n code: \"token_failed\",\n status: res.status,\n });\n }\n const data = (await res.json()) as { access_token?: string; expires_in?: number };\n if (!data.access_token) {\n throw new CourierError(\"Pathao token response had no access_token.\", {\n code: \"token_failed\",\n status: res.status,\n });\n }\n const ttlMs = (typeof data.expires_in === \"number\" ? data.expires_in : 3600) * 1000;\n token = { accessToken: data.access_token, expiresAt: Date.now() + ttlMs };\n return token.accessToken;\n }\n\n async function getToken(): Promise<string> {\n if (token && Date.now() < token.expiresAt - TOKEN_SKEW_MS) return token.accessToken;\n return issueToken();\n }\n\n async function authed<R = Record<string, unknown>>(\n path: string,\n init: { method: \"GET\" | \"POST\"; body?: unknown },\n ): Promise<R> {\n const bearer = await getToken();\n const res = await doFetch(`${baseUrl}${path}`, {\n method: init.method,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${bearer}`,\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n if (!res.ok) {\n throw new CourierError(`Pathao ${init.method} ${path} failed: ${await readError(res)}`, {\n code: \"request_failed\",\n status: res.status,\n });\n }\n return (await res.json()) as R;\n }\n\n async function createOrder(input: CreateOrderInput): Promise<CourierShipment> {\n const body: Record<string, unknown> = {\n store_id: config.storeId,\n recipient_name: input.recipientName,\n recipient_phone: input.recipientPhone,\n recipient_address: input.recipientAddress,\n recipient_city: input.cityId,\n recipient_zone: input.zoneId,\n recipient_area: input.areaId,\n delivery_type: 48,\n item_type: 2,\n special_instruction: input.specialInstruction,\n item_quantity: input.itemQuantity,\n item_weight: input.itemWeight,\n amount_to_collect: input.amountToCollect,\n item_description: input.description,\n };\n if (input.merchantOrderId !== undefined) body.merchant_order_id = input.merchantOrderId;\n\n const data = await authed<{\n data?: { consignment_id?: string; order_status?: string; merchant_order_id?: string };\n }>(\"/aladdin/api/v1/orders\", { method: \"POST\", body });\n\n const consignmentId = data.data?.consignment_id;\n if (!consignmentId) {\n throw new CourierError(\"Pathao order response had no consignment_id.\", {\n code: \"request_failed\",\n });\n }\n return {\n trackingId: consignmentId,\n status: \"confirmed\",\n carrier: \"pathao\",\n raw: data,\n };\n }\n\n async function track(_trackingId: string): Promise<CourierShipment> {\n throw new CourierError(\n \"Pathao reports delivery status via webhooks, not a public track endpoint; use verifyPathaoWebhook + parsePathaoWebhook.\",\n { code: \"unsupported\" },\n );\n }\n\n return {\n name: \"pathao\",\n issueToken,\n createOrder,\n track,\n priceCalculation: (input: PathaoPriceInput) =>\n authed(\"/aladdin/api/v1/merchant/price-plan\", {\n method: \"POST\",\n body: {\n store_id: input.storeId ?? config.storeId,\n item_type: input.itemType ?? 2,\n delivery_type: input.deliveryType ?? 48,\n item_weight: input.itemWeight,\n recipient_city: input.recipientCity,\n recipient_zone: input.recipientZone,\n },\n }),\n cities: () => authed(\"/aladdin/api/v1/city-list\", { method: \"GET\" }),\n zones: (cityId: number) =>\n authed(`/aladdin/api/v1/cities/${cityId}/zone-list`, { method: \"GET\" }),\n areas: (zoneId: number) =>\n authed(`/aladdin/api/v1/zones/${zoneId}/area-list`, { method: \"GET\" }),\n };\n}\n\n/* ------------------------------------------------------------------ *\n * Pathao inbound webhooks\n * ------------------------------------------------------------------ */\n\n/**\n * Header Pathao requires the merchant endpoint to echo back (with HTTP 202)\n * to acknowledge a webhook, carrying the integration secret as its value.\n */\nexport const PATHAO_WEBHOOK_ACK_HEADER = \"X-Pathao-Merchant-Webhook-Integration-Secret\";\n\n/** Maps Pathao webhook event names to canonical delivery statuses. */\nexport const PATHAO_STATUS_MAP: Record<string, DeliveryStatus> = {\n \"order.created\": \"confirmed\",\n \"order.updated\": \"confirmed\",\n \"order.pickup-requested\": \"confirmed\",\n \"order.assigned-for-pickup\": \"confirmed\",\n \"order.picked\": \"picked_up\",\n \"order.pickup-failed\": \"failed\",\n \"order.at-the-sorting-hub\": \"in_transit\",\n \"order.in-transit\": \"in_transit\",\n \"order.received-at-last-mile-hub\": \"in_transit\",\n \"order.assigned-for-delivery\": \"out_for_delivery\",\n \"order.delivered\": \"delivered\",\n \"order.partial-delivery\": \"delivered\",\n \"order.delivery-failed\": \"failed\",\n \"order.on-hold\": \"on_hold\",\n \"order.returning\": \"returned\",\n \"order.return\": \"returned\",\n \"order.returned\": \"returned\",\n \"order.exchanged\": \"delivered\",\n \"order.paid\": \"delivered\",\n \"order.paid-return\": \"returned\",\n};\n\n/** Map a Pathao event name to a canonical status, or `undefined` if unknown. */\nexport function normalizePathaoStatus(event: string): DeliveryStatus | undefined {\n return PATHAO_STATUS_MAP[event];\n}\n\n/** A parsed, normalized Pathao webhook event. */\nexport interface PathaoWebhookEvent {\n event: string;\n status: DeliveryStatus;\n consignmentId?: string;\n merchantOrderId?: string;\n raw: Record<string, unknown>;\n}\n\n/**\n * Parse a Pathao webhook body (raw JSON string or already-parsed object) into a\n * normalized `PathaoWebhookEvent`. Throws `CourierError` on invalid JSON, a\n * missing `event`, or an unrecognized event name.\n */\nexport function parsePathaoWebhook(body: string | Record<string, unknown>): PathaoWebhookEvent {\n let obj: Record<string, unknown>;\n if (typeof body === \"string\") {\n try {\n obj = JSON.parse(body) as Record<string, unknown>;\n } catch {\n throw new CourierError(\"Pathao webhook body is not valid JSON.\", { code: \"invalid_body\" });\n }\n } else {\n obj = body;\n }\n\n const event = obj.event;\n if (typeof event !== \"string\" || !event) {\n throw new CourierError(\"Pathao webhook has no 'event' field.\", { code: \"invalid_body\" });\n }\n\n const status = normalizePathaoStatus(event);\n if (!status) {\n throw new CourierError(`Unknown Pathao webhook event: ${event}`, { code: \"unknown_event\" });\n }\n\n const consignmentId = obj.consignment_id;\n const merchantOrderId = obj.merchant_order_id;\n\n return {\n event,\n status,\n consignmentId: typeof consignmentId === \"string\" ? consignmentId : undefined,\n merchantOrderId: typeof merchantOrderId === \"string\" ? merchantOrderId : undefined,\n raw: obj,\n };\n}\n\n/**\n * Verify a Pathao webhook's shared secret. Pathao's integration sends a secret\n * header (e.g. `X-PATHAO-Signature`) the merchant configures; this does a\n * timing-safe string compare of the received header against the expected value.\n *\n * A `null`/`undefined`/empty header always fails. On success the merchant\n * endpoint is also expected to echo `PATHAO_WEBHOOK_ACK_HEADER` with the secret\n * and respond `202 Accepted`.\n */\nexport function verifyPathaoWebhook(opts: {\n headerSecret: string | null | undefined;\n expectedSecret: string;\n}): boolean {\n if (!opts.headerSecret) return false;\n return timingSafeEqual(opts.headerSecret, opts.expectedSecret);\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/courier
|
|
3
|
+
*
|
|
4
|
+
* Courier / last-mile delivery toolkit for multi-vendor commerce. Three things
|
|
5
|
+
* every delivery integration re-implements — done once, correctly:
|
|
6
|
+
*
|
|
7
|
+
* 1. **A canonical delivery state machine** — one vocabulary of statuses
|
|
8
|
+
* (`pending → confirmed → picked_up → in_transit → out_for_delivery →
|
|
9
|
+
* delivered`, plus `returned / cancelled / failed / on_hold`) with a
|
|
10
|
+
* guarded `transition()` that refuses illegal jumps.
|
|
11
|
+
* 2. **A Pathao (Nepal) adapter** — the Pathao "Aladdin" Merchant API v1:
|
|
12
|
+
* token issue + auto-refresh, order creation, price/city/zone/area lookups.
|
|
13
|
+
* 3. **Inbound webhooks** — verify a webhook's shared-secret / HMAC signature
|
|
14
|
+
* (timing-safe, over Web Crypto) and normalize a carrier's event name into
|
|
15
|
+
* a canonical `DeliveryStatus` so Confirmed → Delivered stops being a
|
|
16
|
+
* manual admin click.
|
|
17
|
+
*
|
|
18
|
+
* Built on global `fetch` and Web Crypto (`globalThis.crypto.subtle`) — no Node
|
|
19
|
+
* built-ins, no dependencies. Isomorphic: Node 18+, edge runtimes and browsers.
|
|
20
|
+
*/
|
|
21
|
+
/** The one delivery vocabulary every carrier is normalized into. */
|
|
22
|
+
type DeliveryStatus = "pending" | "confirmed" | "picked_up" | "in_transit" | "out_for_delivery" | "delivered" | "returned" | "cancelled" | "failed" | "on_hold";
|
|
23
|
+
/**
|
|
24
|
+
* Allowed forward transitions for each status. Terminal states map to `[]`.
|
|
25
|
+
* The set is deliberately permissive on the unhappy paths (returns, holds,
|
|
26
|
+
* failures) but forbids skipping or reversing the happy path.
|
|
27
|
+
*/
|
|
28
|
+
declare const DELIVERY_TRANSITIONS: Record<DeliveryStatus, DeliveryStatus[]>;
|
|
29
|
+
/** `true` if `to` is a legal next state from `from`. */
|
|
30
|
+
declare function canTransition(from: DeliveryStatus, to: DeliveryStatus): boolean;
|
|
31
|
+
/** `true` if `s` is a terminal state with no further transitions. */
|
|
32
|
+
declare function isTerminal(s: DeliveryStatus): boolean;
|
|
33
|
+
/** Error thrown by every part of this toolkit; carries an optional `code`/`status`. */
|
|
34
|
+
declare class CourierError extends Error {
|
|
35
|
+
code?: string;
|
|
36
|
+
status?: number;
|
|
37
|
+
constructor(message: string, opts?: {
|
|
38
|
+
code?: string;
|
|
39
|
+
status?: number;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Return a shallow copy of `order` with its `status` advanced to `to`.
|
|
44
|
+
* Throws `CourierError` (code `illegal_transition`) if the move is not allowed.
|
|
45
|
+
*/
|
|
46
|
+
declare function transition<T extends {
|
|
47
|
+
status: DeliveryStatus;
|
|
48
|
+
}>(order: T, to: DeliveryStatus): T;
|
|
49
|
+
/** A carrier-agnostic view of one shipment. */
|
|
50
|
+
interface CourierShipment {
|
|
51
|
+
trackingId: string;
|
|
52
|
+
status: DeliveryStatus;
|
|
53
|
+
carrier: string;
|
|
54
|
+
/** The raw carrier response, kept for auditing / debugging. */
|
|
55
|
+
raw?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/** Generic order input; a reasonable superset across carriers. */
|
|
58
|
+
interface CreateOrderInput {
|
|
59
|
+
recipientName: string;
|
|
60
|
+
recipientPhone: string;
|
|
61
|
+
recipientAddress: string;
|
|
62
|
+
/** Carrier location ids (Pathao uses numeric city/zone/area). */
|
|
63
|
+
cityId?: number;
|
|
64
|
+
zoneId?: number;
|
|
65
|
+
areaId?: number;
|
|
66
|
+
/** Cash-on-delivery amount to collect (0 for prepaid). */
|
|
67
|
+
amountToCollect: number;
|
|
68
|
+
itemQuantity: number;
|
|
69
|
+
/** Weight in kilograms. */
|
|
70
|
+
itemWeight: number;
|
|
71
|
+
description: string;
|
|
72
|
+
specialInstruction?: string;
|
|
73
|
+
/** Optional merchant-side order id echoed back for reconciliation. */
|
|
74
|
+
merchantOrderId?: string;
|
|
75
|
+
}
|
|
76
|
+
/** The minimum every carrier adapter implements. */
|
|
77
|
+
interface CourierAdapter {
|
|
78
|
+
name: string;
|
|
79
|
+
createOrder(input: CreateOrderInput): Promise<CourierShipment>;
|
|
80
|
+
track(trackingId: string): Promise<CourierShipment>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Constant-time comparison of two strings. Accumulates the difference over the
|
|
84
|
+
* maximum length so it never early-returns on a length or character mismatch.
|
|
85
|
+
*/
|
|
86
|
+
declare function timingSafeEqual(a: string, b: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Verify an inbound webhook's `HMAC-SHA256` hex signature over the *raw*
|
|
89
|
+
* request body string. Timing-safe; never throws on a bad signature.
|
|
90
|
+
*/
|
|
91
|
+
declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Configuration for the Pathao adapter.
|
|
94
|
+
*
|
|
95
|
+
* `baseUrl` defaults to production `https://api-hermes.pathao.com`. For the
|
|
96
|
+
* Pathao sandbox use `https://courier-api-sandbox.pathao.com`.
|
|
97
|
+
*/
|
|
98
|
+
interface PathaoConfig {
|
|
99
|
+
baseUrl?: string;
|
|
100
|
+
clientId: string;
|
|
101
|
+
clientSecret: string;
|
|
102
|
+
username: string;
|
|
103
|
+
password: string;
|
|
104
|
+
storeId: number;
|
|
105
|
+
/** Inject a `fetch` implementation (defaults to global `fetch`). */
|
|
106
|
+
fetch?: typeof fetch;
|
|
107
|
+
}
|
|
108
|
+
/** Production base URL for the Pathao Merchant API. */
|
|
109
|
+
declare const PATHAO_PROD_BASE_URL = "https://api-hermes.pathao.com";
|
|
110
|
+
/** Sandbox base URL for the Pathao Merchant API. */
|
|
111
|
+
declare const PATHAO_SANDBOX_BASE_URL = "https://courier-api-sandbox.pathao.com";
|
|
112
|
+
interface PathaoPriceInput {
|
|
113
|
+
itemType?: number;
|
|
114
|
+
deliveryType?: number;
|
|
115
|
+
itemWeight: number;
|
|
116
|
+
recipientCity: number;
|
|
117
|
+
recipientZone: number;
|
|
118
|
+
storeId?: number;
|
|
119
|
+
}
|
|
120
|
+
/** The extra Pathao-specific methods surfaced beyond the generic adapter. */
|
|
121
|
+
interface PathaoAdapter extends CourierAdapter {
|
|
122
|
+
issueToken(): Promise<string>;
|
|
123
|
+
priceCalculation(input: PathaoPriceInput): Promise<Record<string, unknown>>;
|
|
124
|
+
cities(): Promise<Record<string, unknown>>;
|
|
125
|
+
zones(cityId: number): Promise<Record<string, unknown>>;
|
|
126
|
+
areas(zoneId: number): Promise<Record<string, unknown>>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create a Pathao Merchant API v1 adapter. Handles token issue + auto-refresh
|
|
130
|
+
* and maps the generic `CreateOrderInput` onto Pathao's order body.
|
|
131
|
+
*/
|
|
132
|
+
declare function createPathaoAdapter(config: PathaoConfig): PathaoAdapter;
|
|
133
|
+
/**
|
|
134
|
+
* Header Pathao requires the merchant endpoint to echo back (with HTTP 202)
|
|
135
|
+
* to acknowledge a webhook, carrying the integration secret as its value.
|
|
136
|
+
*/
|
|
137
|
+
declare const PATHAO_WEBHOOK_ACK_HEADER = "X-Pathao-Merchant-Webhook-Integration-Secret";
|
|
138
|
+
/** Maps Pathao webhook event names to canonical delivery statuses. */
|
|
139
|
+
declare const PATHAO_STATUS_MAP: Record<string, DeliveryStatus>;
|
|
140
|
+
/** Map a Pathao event name to a canonical status, or `undefined` if unknown. */
|
|
141
|
+
declare function normalizePathaoStatus(event: string): DeliveryStatus | undefined;
|
|
142
|
+
/** A parsed, normalized Pathao webhook event. */
|
|
143
|
+
interface PathaoWebhookEvent {
|
|
144
|
+
event: string;
|
|
145
|
+
status: DeliveryStatus;
|
|
146
|
+
consignmentId?: string;
|
|
147
|
+
merchantOrderId?: string;
|
|
148
|
+
raw: Record<string, unknown>;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse a Pathao webhook body (raw JSON string or already-parsed object) into a
|
|
152
|
+
* normalized `PathaoWebhookEvent`. Throws `CourierError` on invalid JSON, a
|
|
153
|
+
* missing `event`, or an unrecognized event name.
|
|
154
|
+
*/
|
|
155
|
+
declare function parsePathaoWebhook(body: string | Record<string, unknown>): PathaoWebhookEvent;
|
|
156
|
+
/**
|
|
157
|
+
* Verify a Pathao webhook's shared secret. Pathao's integration sends a secret
|
|
158
|
+
* header (e.g. `X-PATHAO-Signature`) the merchant configures; this does a
|
|
159
|
+
* timing-safe string compare of the received header against the expected value.
|
|
160
|
+
*
|
|
161
|
+
* A `null`/`undefined`/empty header always fails. On success the merchant
|
|
162
|
+
* endpoint is also expected to echo `PATHAO_WEBHOOK_ACK_HEADER` with the secret
|
|
163
|
+
* and respond `202 Accepted`.
|
|
164
|
+
*/
|
|
165
|
+
declare function verifyPathaoWebhook(opts: {
|
|
166
|
+
headerSecret: string | null | undefined;
|
|
167
|
+
expectedSecret: string;
|
|
168
|
+
}): boolean;
|
|
169
|
+
|
|
170
|
+
export { type CourierAdapter, CourierError, type CourierShipment, type CreateOrderInput, DELIVERY_TRANSITIONS, type DeliveryStatus, PATHAO_PROD_BASE_URL, PATHAO_SANDBOX_BASE_URL, PATHAO_STATUS_MAP, PATHAO_WEBHOOK_ACK_HEADER, type PathaoAdapter, type PathaoConfig, type PathaoPriceInput, type PathaoWebhookEvent, canTransition, createPathaoAdapter, isTerminal, normalizePathaoStatus, parsePathaoWebhook, timingSafeEqual, transition, verifyPathaoWebhook, verifyWebhookSignature };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/courier
|
|
3
|
+
*
|
|
4
|
+
* Courier / last-mile delivery toolkit for multi-vendor commerce. Three things
|
|
5
|
+
* every delivery integration re-implements — done once, correctly:
|
|
6
|
+
*
|
|
7
|
+
* 1. **A canonical delivery state machine** — one vocabulary of statuses
|
|
8
|
+
* (`pending → confirmed → picked_up → in_transit → out_for_delivery →
|
|
9
|
+
* delivered`, plus `returned / cancelled / failed / on_hold`) with a
|
|
10
|
+
* guarded `transition()` that refuses illegal jumps.
|
|
11
|
+
* 2. **A Pathao (Nepal) adapter** — the Pathao "Aladdin" Merchant API v1:
|
|
12
|
+
* token issue + auto-refresh, order creation, price/city/zone/area lookups.
|
|
13
|
+
* 3. **Inbound webhooks** — verify a webhook's shared-secret / HMAC signature
|
|
14
|
+
* (timing-safe, over Web Crypto) and normalize a carrier's event name into
|
|
15
|
+
* a canonical `DeliveryStatus` so Confirmed → Delivered stops being a
|
|
16
|
+
* manual admin click.
|
|
17
|
+
*
|
|
18
|
+
* Built on global `fetch` and Web Crypto (`globalThis.crypto.subtle`) — no Node
|
|
19
|
+
* built-ins, no dependencies. Isomorphic: Node 18+, edge runtimes and browsers.
|
|
20
|
+
*/
|
|
21
|
+
/** The one delivery vocabulary every carrier is normalized into. */
|
|
22
|
+
type DeliveryStatus = "pending" | "confirmed" | "picked_up" | "in_transit" | "out_for_delivery" | "delivered" | "returned" | "cancelled" | "failed" | "on_hold";
|
|
23
|
+
/**
|
|
24
|
+
* Allowed forward transitions for each status. Terminal states map to `[]`.
|
|
25
|
+
* The set is deliberately permissive on the unhappy paths (returns, holds,
|
|
26
|
+
* failures) but forbids skipping or reversing the happy path.
|
|
27
|
+
*/
|
|
28
|
+
declare const DELIVERY_TRANSITIONS: Record<DeliveryStatus, DeliveryStatus[]>;
|
|
29
|
+
/** `true` if `to` is a legal next state from `from`. */
|
|
30
|
+
declare function canTransition(from: DeliveryStatus, to: DeliveryStatus): boolean;
|
|
31
|
+
/** `true` if `s` is a terminal state with no further transitions. */
|
|
32
|
+
declare function isTerminal(s: DeliveryStatus): boolean;
|
|
33
|
+
/** Error thrown by every part of this toolkit; carries an optional `code`/`status`. */
|
|
34
|
+
declare class CourierError extends Error {
|
|
35
|
+
code?: string;
|
|
36
|
+
status?: number;
|
|
37
|
+
constructor(message: string, opts?: {
|
|
38
|
+
code?: string;
|
|
39
|
+
status?: number;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Return a shallow copy of `order` with its `status` advanced to `to`.
|
|
44
|
+
* Throws `CourierError` (code `illegal_transition`) if the move is not allowed.
|
|
45
|
+
*/
|
|
46
|
+
declare function transition<T extends {
|
|
47
|
+
status: DeliveryStatus;
|
|
48
|
+
}>(order: T, to: DeliveryStatus): T;
|
|
49
|
+
/** A carrier-agnostic view of one shipment. */
|
|
50
|
+
interface CourierShipment {
|
|
51
|
+
trackingId: string;
|
|
52
|
+
status: DeliveryStatus;
|
|
53
|
+
carrier: string;
|
|
54
|
+
/** The raw carrier response, kept for auditing / debugging. */
|
|
55
|
+
raw?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/** Generic order input; a reasonable superset across carriers. */
|
|
58
|
+
interface CreateOrderInput {
|
|
59
|
+
recipientName: string;
|
|
60
|
+
recipientPhone: string;
|
|
61
|
+
recipientAddress: string;
|
|
62
|
+
/** Carrier location ids (Pathao uses numeric city/zone/area). */
|
|
63
|
+
cityId?: number;
|
|
64
|
+
zoneId?: number;
|
|
65
|
+
areaId?: number;
|
|
66
|
+
/** Cash-on-delivery amount to collect (0 for prepaid). */
|
|
67
|
+
amountToCollect: number;
|
|
68
|
+
itemQuantity: number;
|
|
69
|
+
/** Weight in kilograms. */
|
|
70
|
+
itemWeight: number;
|
|
71
|
+
description: string;
|
|
72
|
+
specialInstruction?: string;
|
|
73
|
+
/** Optional merchant-side order id echoed back for reconciliation. */
|
|
74
|
+
merchantOrderId?: string;
|
|
75
|
+
}
|
|
76
|
+
/** The minimum every carrier adapter implements. */
|
|
77
|
+
interface CourierAdapter {
|
|
78
|
+
name: string;
|
|
79
|
+
createOrder(input: CreateOrderInput): Promise<CourierShipment>;
|
|
80
|
+
track(trackingId: string): Promise<CourierShipment>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Constant-time comparison of two strings. Accumulates the difference over the
|
|
84
|
+
* maximum length so it never early-returns on a length or character mismatch.
|
|
85
|
+
*/
|
|
86
|
+
declare function timingSafeEqual(a: string, b: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Verify an inbound webhook's `HMAC-SHA256` hex signature over the *raw*
|
|
89
|
+
* request body string. Timing-safe; never throws on a bad signature.
|
|
90
|
+
*/
|
|
91
|
+
declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Configuration for the Pathao adapter.
|
|
94
|
+
*
|
|
95
|
+
* `baseUrl` defaults to production `https://api-hermes.pathao.com`. For the
|
|
96
|
+
* Pathao sandbox use `https://courier-api-sandbox.pathao.com`.
|
|
97
|
+
*/
|
|
98
|
+
interface PathaoConfig {
|
|
99
|
+
baseUrl?: string;
|
|
100
|
+
clientId: string;
|
|
101
|
+
clientSecret: string;
|
|
102
|
+
username: string;
|
|
103
|
+
password: string;
|
|
104
|
+
storeId: number;
|
|
105
|
+
/** Inject a `fetch` implementation (defaults to global `fetch`). */
|
|
106
|
+
fetch?: typeof fetch;
|
|
107
|
+
}
|
|
108
|
+
/** Production base URL for the Pathao Merchant API. */
|
|
109
|
+
declare const PATHAO_PROD_BASE_URL = "https://api-hermes.pathao.com";
|
|
110
|
+
/** Sandbox base URL for the Pathao Merchant API. */
|
|
111
|
+
declare const PATHAO_SANDBOX_BASE_URL = "https://courier-api-sandbox.pathao.com";
|
|
112
|
+
interface PathaoPriceInput {
|
|
113
|
+
itemType?: number;
|
|
114
|
+
deliveryType?: number;
|
|
115
|
+
itemWeight: number;
|
|
116
|
+
recipientCity: number;
|
|
117
|
+
recipientZone: number;
|
|
118
|
+
storeId?: number;
|
|
119
|
+
}
|
|
120
|
+
/** The extra Pathao-specific methods surfaced beyond the generic adapter. */
|
|
121
|
+
interface PathaoAdapter extends CourierAdapter {
|
|
122
|
+
issueToken(): Promise<string>;
|
|
123
|
+
priceCalculation(input: PathaoPriceInput): Promise<Record<string, unknown>>;
|
|
124
|
+
cities(): Promise<Record<string, unknown>>;
|
|
125
|
+
zones(cityId: number): Promise<Record<string, unknown>>;
|
|
126
|
+
areas(zoneId: number): Promise<Record<string, unknown>>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create a Pathao Merchant API v1 adapter. Handles token issue + auto-refresh
|
|
130
|
+
* and maps the generic `CreateOrderInput` onto Pathao's order body.
|
|
131
|
+
*/
|
|
132
|
+
declare function createPathaoAdapter(config: PathaoConfig): PathaoAdapter;
|
|
133
|
+
/**
|
|
134
|
+
* Header Pathao requires the merchant endpoint to echo back (with HTTP 202)
|
|
135
|
+
* to acknowledge a webhook, carrying the integration secret as its value.
|
|
136
|
+
*/
|
|
137
|
+
declare const PATHAO_WEBHOOK_ACK_HEADER = "X-Pathao-Merchant-Webhook-Integration-Secret";
|
|
138
|
+
/** Maps Pathao webhook event names to canonical delivery statuses. */
|
|
139
|
+
declare const PATHAO_STATUS_MAP: Record<string, DeliveryStatus>;
|
|
140
|
+
/** Map a Pathao event name to a canonical status, or `undefined` if unknown. */
|
|
141
|
+
declare function normalizePathaoStatus(event: string): DeliveryStatus | undefined;
|
|
142
|
+
/** A parsed, normalized Pathao webhook event. */
|
|
143
|
+
interface PathaoWebhookEvent {
|
|
144
|
+
event: string;
|
|
145
|
+
status: DeliveryStatus;
|
|
146
|
+
consignmentId?: string;
|
|
147
|
+
merchantOrderId?: string;
|
|
148
|
+
raw: Record<string, unknown>;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse a Pathao webhook body (raw JSON string or already-parsed object) into a
|
|
152
|
+
* normalized `PathaoWebhookEvent`. Throws `CourierError` on invalid JSON, a
|
|
153
|
+
* missing `event`, or an unrecognized event name.
|
|
154
|
+
*/
|
|
155
|
+
declare function parsePathaoWebhook(body: string | Record<string, unknown>): PathaoWebhookEvent;
|
|
156
|
+
/**
|
|
157
|
+
* Verify a Pathao webhook's shared secret. Pathao's integration sends a secret
|
|
158
|
+
* header (e.g. `X-PATHAO-Signature`) the merchant configures; this does a
|
|
159
|
+
* timing-safe string compare of the received header against the expected value.
|
|
160
|
+
*
|
|
161
|
+
* A `null`/`undefined`/empty header always fails. On success the merchant
|
|
162
|
+
* endpoint is also expected to echo `PATHAO_WEBHOOK_ACK_HEADER` with the secret
|
|
163
|
+
* and respond `202 Accepted`.
|
|
164
|
+
*/
|
|
165
|
+
declare function verifyPathaoWebhook(opts: {
|
|
166
|
+
headerSecret: string | null | undefined;
|
|
167
|
+
expectedSecret: string;
|
|
168
|
+
}): boolean;
|
|
169
|
+
|
|
170
|
+
export { type CourierAdapter, CourierError, type CourierShipment, type CreateOrderInput, DELIVERY_TRANSITIONS, type DeliveryStatus, PATHAO_PROD_BASE_URL, PATHAO_SANDBOX_BASE_URL, PATHAO_STATUS_MAP, PATHAO_WEBHOOK_ACK_HEADER, type PathaoAdapter, type PathaoConfig, type PathaoPriceInput, type PathaoWebhookEvent, canTransition, createPathaoAdapter, isTerminal, normalizePathaoStatus, parsePathaoWebhook, timingSafeEqual, transition, verifyPathaoWebhook, verifyWebhookSignature };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DELIVERY_TRANSITIONS = {
|
|
3
|
+
pending: ["confirmed", "cancelled"],
|
|
4
|
+
confirmed: ["picked_up", "cancelled", "on_hold"],
|
|
5
|
+
picked_up: ["in_transit", "returned", "on_hold"],
|
|
6
|
+
in_transit: ["out_for_delivery", "returned", "failed", "on_hold"],
|
|
7
|
+
out_for_delivery: ["delivered", "failed", "returned"],
|
|
8
|
+
on_hold: ["in_transit", "cancelled", "returned"],
|
|
9
|
+
delivered: [],
|
|
10
|
+
returned: [],
|
|
11
|
+
cancelled: [],
|
|
12
|
+
failed: []
|
|
13
|
+
};
|
|
14
|
+
function canTransition(from, to) {
|
|
15
|
+
return DELIVERY_TRANSITIONS[from].includes(to);
|
|
16
|
+
}
|
|
17
|
+
function isTerminal(s) {
|
|
18
|
+
return DELIVERY_TRANSITIONS[s].length === 0;
|
|
19
|
+
}
|
|
20
|
+
var CourierError = class extends Error {
|
|
21
|
+
constructor(message, opts) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "CourierError";
|
|
24
|
+
this.code = opts?.code;
|
|
25
|
+
this.status = opts?.status;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function transition(order, to) {
|
|
29
|
+
if (!canTransition(order.status, to)) {
|
|
30
|
+
throw new CourierError(
|
|
31
|
+
`Illegal delivery transition: ${order.status} \u2192 ${to}`,
|
|
32
|
+
{ code: "illegal_transition" }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return { ...order, status: to };
|
|
36
|
+
}
|
|
37
|
+
var enc = new TextEncoder();
|
|
38
|
+
function getSubtle() {
|
|
39
|
+
const subtle = globalThis.crypto?.subtle;
|
|
40
|
+
if (!subtle) {
|
|
41
|
+
throw new CourierError(
|
|
42
|
+
"@lacspace/courier: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.",
|
|
43
|
+
{ code: "no_web_crypto" }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return subtle;
|
|
47
|
+
}
|
|
48
|
+
function toHex(bytes) {
|
|
49
|
+
let out = "";
|
|
50
|
+
for (let i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, "0");
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
async function hmacSha256Hex(secret, message) {
|
|
54
|
+
const subtle = getSubtle();
|
|
55
|
+
const key = await subtle.importKey(
|
|
56
|
+
"raw",
|
|
57
|
+
enc.encode(secret),
|
|
58
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
59
|
+
false,
|
|
60
|
+
["sign"]
|
|
61
|
+
);
|
|
62
|
+
const sig = await subtle.sign("HMAC", key, enc.encode(message));
|
|
63
|
+
return toHex(new Uint8Array(sig));
|
|
64
|
+
}
|
|
65
|
+
function timingSafeEqual(a, b) {
|
|
66
|
+
const ab = enc.encode(a);
|
|
67
|
+
const bb = enc.encode(b);
|
|
68
|
+
let diff = ab.length ^ bb.length;
|
|
69
|
+
const n = Math.max(ab.length, bb.length);
|
|
70
|
+
for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
|
|
71
|
+
return diff === 0;
|
|
72
|
+
}
|
|
73
|
+
async function verifyWebhookSignature(payload, signature, secret) {
|
|
74
|
+
const expected = await hmacSha256Hex(secret, payload);
|
|
75
|
+
return timingSafeEqual(expected, signature.trim().toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
var PATHAO_PROD_BASE_URL = "https://api-hermes.pathao.com";
|
|
78
|
+
var PATHAO_SANDBOX_BASE_URL = "https://courier-api-sandbox.pathao.com";
|
|
79
|
+
var TOKEN_SKEW_MS = 6e4;
|
|
80
|
+
function createPathaoAdapter(config) {
|
|
81
|
+
const baseUrl = (config.baseUrl ?? PATHAO_PROD_BASE_URL).replace(/\/+$/, "");
|
|
82
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
83
|
+
if (typeof doFetch !== "function") {
|
|
84
|
+
throw new CourierError(
|
|
85
|
+
"@lacspace/courier: global fetch is unavailable; pass config.fetch.",
|
|
86
|
+
{ code: "no_fetch" }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
let token = null;
|
|
90
|
+
async function readError(res) {
|
|
91
|
+
let body;
|
|
92
|
+
try {
|
|
93
|
+
body = await res.json();
|
|
94
|
+
} catch {
|
|
95
|
+
try {
|
|
96
|
+
body = await res.text();
|
|
97
|
+
} catch {
|
|
98
|
+
body = void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (body && typeof body === "object") {
|
|
102
|
+
const rec = body;
|
|
103
|
+
const msg = rec.message ?? rec.error ?? rec.errors;
|
|
104
|
+
if (msg) return typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
105
|
+
}
|
|
106
|
+
if (typeof body === "string" && body) return body;
|
|
107
|
+
return res.statusText || `HTTP ${res.status}`;
|
|
108
|
+
}
|
|
109
|
+
async function issueToken() {
|
|
110
|
+
const res = await doFetch(`${baseUrl}/aladdin/api/v1/issue-token`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
Accept: "application/json",
|
|
114
|
+
"Content-Type": "application/json"
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
client_id: config.clientId,
|
|
118
|
+
client_secret: config.clientSecret,
|
|
119
|
+
grant_type: "password",
|
|
120
|
+
username: config.username,
|
|
121
|
+
password: config.password
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
throw new CourierError(`Pathao token request failed: ${await readError(res)}`, {
|
|
126
|
+
code: "token_failed",
|
|
127
|
+
status: res.status
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
if (!data.access_token) {
|
|
132
|
+
throw new CourierError("Pathao token response had no access_token.", {
|
|
133
|
+
code: "token_failed",
|
|
134
|
+
status: res.status
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const ttlMs = (typeof data.expires_in === "number" ? data.expires_in : 3600) * 1e3;
|
|
138
|
+
token = { accessToken: data.access_token, expiresAt: Date.now() + ttlMs };
|
|
139
|
+
return token.accessToken;
|
|
140
|
+
}
|
|
141
|
+
async function getToken() {
|
|
142
|
+
if (token && Date.now() < token.expiresAt - TOKEN_SKEW_MS) return token.accessToken;
|
|
143
|
+
return issueToken();
|
|
144
|
+
}
|
|
145
|
+
async function authed(path, init) {
|
|
146
|
+
const bearer = await getToken();
|
|
147
|
+
const res = await doFetch(`${baseUrl}${path}`, {
|
|
148
|
+
method: init.method,
|
|
149
|
+
headers: {
|
|
150
|
+
Accept: "application/json",
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
Authorization: `Bearer ${bearer}`
|
|
153
|
+
},
|
|
154
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
throw new CourierError(`Pathao ${init.method} ${path} failed: ${await readError(res)}`, {
|
|
158
|
+
code: "request_failed",
|
|
159
|
+
status: res.status
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return await res.json();
|
|
163
|
+
}
|
|
164
|
+
async function createOrder(input) {
|
|
165
|
+
const body = {
|
|
166
|
+
store_id: config.storeId,
|
|
167
|
+
recipient_name: input.recipientName,
|
|
168
|
+
recipient_phone: input.recipientPhone,
|
|
169
|
+
recipient_address: input.recipientAddress,
|
|
170
|
+
recipient_city: input.cityId,
|
|
171
|
+
recipient_zone: input.zoneId,
|
|
172
|
+
recipient_area: input.areaId,
|
|
173
|
+
delivery_type: 48,
|
|
174
|
+
item_type: 2,
|
|
175
|
+
special_instruction: input.specialInstruction,
|
|
176
|
+
item_quantity: input.itemQuantity,
|
|
177
|
+
item_weight: input.itemWeight,
|
|
178
|
+
amount_to_collect: input.amountToCollect,
|
|
179
|
+
item_description: input.description
|
|
180
|
+
};
|
|
181
|
+
if (input.merchantOrderId !== void 0) body.merchant_order_id = input.merchantOrderId;
|
|
182
|
+
const data = await authed("/aladdin/api/v1/orders", { method: "POST", body });
|
|
183
|
+
const consignmentId = data.data?.consignment_id;
|
|
184
|
+
if (!consignmentId) {
|
|
185
|
+
throw new CourierError("Pathao order response had no consignment_id.", {
|
|
186
|
+
code: "request_failed"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
trackingId: consignmentId,
|
|
191
|
+
status: "confirmed",
|
|
192
|
+
carrier: "pathao",
|
|
193
|
+
raw: data
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function track(_trackingId) {
|
|
197
|
+
throw new CourierError(
|
|
198
|
+
"Pathao reports delivery status via webhooks, not a public track endpoint; use verifyPathaoWebhook + parsePathaoWebhook.",
|
|
199
|
+
{ code: "unsupported" }
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
name: "pathao",
|
|
204
|
+
issueToken,
|
|
205
|
+
createOrder,
|
|
206
|
+
track,
|
|
207
|
+
priceCalculation: (input) => authed("/aladdin/api/v1/merchant/price-plan", {
|
|
208
|
+
method: "POST",
|
|
209
|
+
body: {
|
|
210
|
+
store_id: input.storeId ?? config.storeId,
|
|
211
|
+
item_type: input.itemType ?? 2,
|
|
212
|
+
delivery_type: input.deliveryType ?? 48,
|
|
213
|
+
item_weight: input.itemWeight,
|
|
214
|
+
recipient_city: input.recipientCity,
|
|
215
|
+
recipient_zone: input.recipientZone
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
cities: () => authed("/aladdin/api/v1/city-list", { method: "GET" }),
|
|
219
|
+
zones: (cityId) => authed(`/aladdin/api/v1/cities/${cityId}/zone-list`, { method: "GET" }),
|
|
220
|
+
areas: (zoneId) => authed(`/aladdin/api/v1/zones/${zoneId}/area-list`, { method: "GET" })
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
var PATHAO_WEBHOOK_ACK_HEADER = "X-Pathao-Merchant-Webhook-Integration-Secret";
|
|
224
|
+
var PATHAO_STATUS_MAP = {
|
|
225
|
+
"order.created": "confirmed",
|
|
226
|
+
"order.updated": "confirmed",
|
|
227
|
+
"order.pickup-requested": "confirmed",
|
|
228
|
+
"order.assigned-for-pickup": "confirmed",
|
|
229
|
+
"order.picked": "picked_up",
|
|
230
|
+
"order.pickup-failed": "failed",
|
|
231
|
+
"order.at-the-sorting-hub": "in_transit",
|
|
232
|
+
"order.in-transit": "in_transit",
|
|
233
|
+
"order.received-at-last-mile-hub": "in_transit",
|
|
234
|
+
"order.assigned-for-delivery": "out_for_delivery",
|
|
235
|
+
"order.delivered": "delivered",
|
|
236
|
+
"order.partial-delivery": "delivered",
|
|
237
|
+
"order.delivery-failed": "failed",
|
|
238
|
+
"order.on-hold": "on_hold",
|
|
239
|
+
"order.returning": "returned",
|
|
240
|
+
"order.return": "returned",
|
|
241
|
+
"order.returned": "returned",
|
|
242
|
+
"order.exchanged": "delivered",
|
|
243
|
+
"order.paid": "delivered",
|
|
244
|
+
"order.paid-return": "returned"
|
|
245
|
+
};
|
|
246
|
+
function normalizePathaoStatus(event) {
|
|
247
|
+
return PATHAO_STATUS_MAP[event];
|
|
248
|
+
}
|
|
249
|
+
function parsePathaoWebhook(body) {
|
|
250
|
+
let obj;
|
|
251
|
+
if (typeof body === "string") {
|
|
252
|
+
try {
|
|
253
|
+
obj = JSON.parse(body);
|
|
254
|
+
} catch {
|
|
255
|
+
throw new CourierError("Pathao webhook body is not valid JSON.", { code: "invalid_body" });
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
obj = body;
|
|
259
|
+
}
|
|
260
|
+
const event = obj.event;
|
|
261
|
+
if (typeof event !== "string" || !event) {
|
|
262
|
+
throw new CourierError("Pathao webhook has no 'event' field.", { code: "invalid_body" });
|
|
263
|
+
}
|
|
264
|
+
const status = normalizePathaoStatus(event);
|
|
265
|
+
if (!status) {
|
|
266
|
+
throw new CourierError(`Unknown Pathao webhook event: ${event}`, { code: "unknown_event" });
|
|
267
|
+
}
|
|
268
|
+
const consignmentId = obj.consignment_id;
|
|
269
|
+
const merchantOrderId = obj.merchant_order_id;
|
|
270
|
+
return {
|
|
271
|
+
event,
|
|
272
|
+
status,
|
|
273
|
+
consignmentId: typeof consignmentId === "string" ? consignmentId : void 0,
|
|
274
|
+
merchantOrderId: typeof merchantOrderId === "string" ? merchantOrderId : void 0,
|
|
275
|
+
raw: obj
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function verifyPathaoWebhook(opts) {
|
|
279
|
+
if (!opts.headerSecret) return false;
|
|
280
|
+
return timingSafeEqual(opts.headerSecret, opts.expectedSecret);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export { CourierError, DELIVERY_TRANSITIONS, PATHAO_PROD_BASE_URL, PATHAO_SANDBOX_BASE_URL, PATHAO_STATUS_MAP, PATHAO_WEBHOOK_ACK_HEADER, canTransition, createPathaoAdapter, isTerminal, normalizePathaoStatus, parsePathaoWebhook, timingSafeEqual, transition, verifyPathaoWebhook, verifyWebhookSignature };
|
|
284
|
+
//# sourceMappingURL=index.js.map
|
|
285
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AA2CO,IAAM,oBAAA,GAAiE;AAAA,EAC5E,OAAA,EAAS,CAAC,WAAA,EAAa,WAAW,CAAA;AAAA,EAClC,SAAA,EAAW,CAAC,WAAA,EAAa,WAAA,EAAa,SAAS,CAAA;AAAA,EAC/C,SAAA,EAAW,CAAC,YAAA,EAAc,UAAA,EAAY,SAAS,CAAA;AAAA,EAC/C,UAAA,EAAY,CAAC,kBAAA,EAAoB,UAAA,EAAY,UAAU,SAAS,CAAA;AAAA,EAChE,gBAAA,EAAkB,CAAC,WAAA,EAAa,QAAA,EAAU,UAAU,CAAA;AAAA,EACpD,OAAA,EAAS,CAAC,YAAA,EAAc,WAAA,EAAa,UAAU,CAAA;AAAA,EAC/C,WAAW,EAAC;AAAA,EACZ,UAAU,EAAC;AAAA,EACX,WAAW,EAAC;AAAA,EACZ,QAAQ;AACV;AAGO,SAAS,aAAA,CAAc,MAAsB,EAAA,EAA6B;AAC/E,EAAA,OAAO,oBAAA,CAAqB,IAAI,CAAA,CAAE,QAAA,CAAS,EAAE,CAAA;AAC/C;AAGO,SAAS,WAAW,CAAA,EAA4B;AACrD,EAAA,OAAO,oBAAA,CAAqB,CAAC,CAAA,CAAE,MAAA,KAAW,CAAA;AAC5C;AAGO,IAAM,YAAA,GAAN,cAA2B,KAAA,CAAM;AAAA,EAGtC,WAAA,CAAY,SAAiB,IAAA,EAA2C;AACtE,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,OAAO,IAAA,EAAM,IAAA;AAClB,IAAA,IAAA,CAAK,SAAS,IAAA,EAAM,MAAA;AAAA,EACtB;AACF;AAMO,SAAS,UAAA,CAAiD,OAAU,EAAA,EAAuB;AAChG,EAAA,IAAI,CAAC,aAAA,CAAc,KAAA,CAAM,MAAA,EAAQ,EAAE,CAAA,EAAG;AACpC,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,CAAA,6BAAA,EAAgC,KAAA,CAAM,MAAM,CAAA,QAAA,EAAM,EAAE,CAAA,CAAA;AAAA,MACpD,EAAE,MAAM,oBAAA;AAAqB,KAC/B;AAAA,EACF;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,MAAA,EAAQ,EAAA,EAAG;AAChC;AA8CA,IAAM,GAAA,GAAM,IAAI,WAAA,EAAY;AAE5B,SAAS,SAAA,GAA0B;AACjC,EAAA,MAAM,MAAA,GAAS,WAAW,MAAA,EAAQ,MAAA;AAClC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,0FAAA;AAAA,MACA,EAAE,MAAM,eAAA;AAAgB,KAC1B;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAGA,SAAS,MAAM,KAAA,EAA2B;AACxC,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK,GAAA,IAAO,KAAA,CAAM,CAAC,EAAG,QAAA,CAAS,EAAE,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AACpF,EAAA,OAAO,GAAA;AACT;AAGA,eAAe,aAAA,CAAc,QAAgB,OAAA,EAAkC;AAC7E,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,SAAA;AAAA,IACvB,KAAA;AAAA,IACA,GAAA,CAAI,OAAO,MAAM,CAAA;AAAA,IACjB,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,IAChC,KAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AACA,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,IAAA,CAAK,QAAQ,GAAA,EAAK,GAAA,CAAI,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9D,EAAA,OAAO,KAAA,CAAM,IAAI,UAAA,CAAW,GAAG,CAAC,CAAA;AAClC;AAMO,SAAS,eAAA,CAAgB,GAAW,CAAA,EAAoB;AAC7D,EAAA,MAAM,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA;AACvB,EAAA,MAAM,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA;AACvB,EAAA,IAAI,IAAA,GAAO,EAAA,CAAG,MAAA,GAAS,EAAA,CAAG,MAAA;AAC1B,EAAA,MAAM,IAAI,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,GAAG,MAAM,CAAA;AACvC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK,IAAA,IAAA,CAAS,EAAA,CAAG,CAAC,CAAA,IAAK,CAAA,KAAM,EAAA,CAAG,CAAC,CAAA,IAAK,CAAA,CAAA;AAC7D,EAAA,OAAO,IAAA,KAAS,CAAA;AAClB;AAUA,eAAsB,sBAAA,CACpB,OAAA,EACA,SAAA,EACA,MAAA,EACkB;AAClB,EAAA,MAAM,QAAA,GAAW,MAAM,aAAA,CAAc,MAAA,EAAQ,OAAO,CAAA;AACpD,EAAA,OAAO,gBAAgB,QAAA,EAAU,SAAA,CAAU,IAAA,EAAK,CAAE,aAAa,CAAA;AACjE;AAwBO,IAAM,oBAAA,GAAuB;AAE7B,IAAM,uBAAA,GAA0B;AA2BvC,IAAM,aAAA,GAAgB,GAAA;AAMf,SAAS,oBAAoB,MAAA,EAAqC;AACvE,EAAA,MAAM,WAAW,MAAA,CAAO,OAAA,IAAW,oBAAA,EAAsB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAC3E,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,KAAA,IAAS,UAAA,CAAW,KAAA;AAC3C,EAAA,IAAI,OAAO,YAAY,UAAA,EAAY;AACjC,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,oEAAA;AAAA,MACA,EAAE,MAAM,UAAA;AAAW,KACrB;AAAA,EACF;AAEA,EAAA,IAAI,KAAA,GAA2B,IAAA;AAE/B,EAAA,eAAe,UAAU,GAAA,EAAgC;AACvD,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,IACxB,CAAA,CAAA,MAAQ;AACN,MAAA,IAAI;AACF,QAAA,IAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,MACxB,CAAA,CAAA,MAAQ;AACN,QAAA,IAAA,GAAO,MAAA;AAAA,MACT;AAAA,IACF;AACA,IAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,EAAU;AACpC,MAAA,MAAM,GAAA,GAAM,IAAA;AACZ,MAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,IAAW,GAAA,CAAI,SAAS,GAAA,CAAI,MAAA;AAC5C,MAAA,IAAI,GAAA,SAAY,OAAO,GAAA,KAAQ,WAAW,GAAA,GAAM,IAAA,CAAK,UAAU,GAAG,CAAA;AAAA,IACpE;AACA,IAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,EAAM,OAAO,IAAA;AAC7C,IAAA,OAAO,GAAA,CAAI,UAAA,IAAc,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAA;AAAA,EAC7C;AAEA,EAAA,eAAe,UAAA,GAA8B;AAC3C,IAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,CAAA,EAAG,OAAO,CAAA,2BAAA,CAAA,EAA+B;AAAA,MACjE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,MAAA,EAAQ,kBAAA;AAAA,QACR,cAAA,EAAgB;AAAA,OAClB;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACnB,WAAW,MAAA,CAAO,QAAA;AAAA,QAClB,eAAe,MAAA,CAAO,YAAA;AAAA,QACtB,UAAA,EAAY,UAAA;AAAA,QACZ,UAAU,MAAA,CAAO,QAAA;AAAA,QACjB,UAAU,MAAA,CAAO;AAAA,OAClB;AAAA,KACF,CAAA;AACD,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,YAAA,CAAa,CAAA,6BAAA,EAAgC,MAAM,SAAA,CAAU,GAAG,CAAC,CAAA,CAAA,EAAI;AAAA,QAC7E,IAAA,EAAM,cAAA;AAAA,QACN,QAAQ,GAAA,CAAI;AAAA,OACb,CAAA;AAAA,IACH;AACA,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACtB,MAAA,MAAM,IAAI,aAAa,4CAAA,EAA8C;AAAA,QACnE,IAAA,EAAM,cAAA;AAAA,QACN,QAAQ,GAAA,CAAI;AAAA,OACb,CAAA;AAAA,IACH;AACA,IAAA,MAAM,SAAS,OAAO,IAAA,CAAK,eAAe,QAAA,GAAW,IAAA,CAAK,aAAa,IAAA,IAAQ,GAAA;AAC/E,IAAA,KAAA,GAAQ,EAAE,aAAa,IAAA,CAAK,YAAA,EAAc,WAAW,IAAA,CAAK,GAAA,KAAQ,KAAA,EAAM;AACxE,IAAA,OAAO,KAAA,CAAM,WAAA;AAAA,EACf;AAEA,EAAA,eAAe,QAAA,GAA4B;AACzC,IAAA,IAAI,KAAA,IAAS,KAAK,GAAA,EAAI,GAAI,MAAM,SAAA,GAAY,aAAA,SAAsB,KAAA,CAAM,WAAA;AACxE,IAAA,OAAO,UAAA,EAAW;AAAA,EACpB;AAEA,EAAA,eAAe,MAAA,CACb,MACA,IAAA,EACY;AACZ,IAAA,MAAM,MAAA,GAAS,MAAM,QAAA,EAAS;AAC9B,IAAA,MAAM,MAAM,MAAM,OAAA,CAAQ,GAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI;AAAA,MAC7C,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,OAAA,EAAS;AAAA,QACP,MAAA,EAAQ,kBAAA;AAAA,QACR,cAAA,EAAgB,kBAAA;AAAA,QAChB,aAAA,EAAe,UAAU,MAAM,CAAA;AAAA,OACjC;AAAA,MACA,IAAA,EAAM,KAAK,IAAA,KAAS,MAAA,GAAY,SAAY,IAAA,CAAK,SAAA,CAAU,KAAK,IAAI;AAAA,KACrE,CAAA;AACD,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,YAAA,CAAa,CAAA,OAAA,EAAU,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,IAAI,CAAA,SAAA,EAAY,MAAM,SAAA,CAAU,GAAG,CAAC,CAAA,CAAA,EAAI;AAAA,QACtF,IAAA,EAAM,gBAAA;AAAA,QACN,QAAQ,GAAA,CAAI;AAAA,OACb,CAAA;AAAA,IACH;AACA,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AAEA,EAAA,eAAe,YAAY,KAAA,EAAmD;AAC5E,IAAA,MAAM,IAAA,GAAgC;AAAA,MACpC,UAAU,MAAA,CAAO,OAAA;AAAA,MACjB,gBAAgB,KAAA,CAAM,aAAA;AAAA,MACtB,iBAAiB,KAAA,CAAM,cAAA;AAAA,MACvB,mBAAmB,KAAA,CAAM,gBAAA;AAAA,MACzB,gBAAgB,KAAA,CAAM,MAAA;AAAA,MACtB,gBAAgB,KAAA,CAAM,MAAA;AAAA,MACtB,gBAAgB,KAAA,CAAM,MAAA;AAAA,MACtB,aAAA,EAAe,EAAA;AAAA,MACf,SAAA,EAAW,CAAA;AAAA,MACX,qBAAqB,KAAA,CAAM,kBAAA;AAAA,MAC3B,eAAe,KAAA,CAAM,YAAA;AAAA,MACrB,aAAa,KAAA,CAAM,UAAA;AAAA,MACnB,mBAAmB,KAAA,CAAM,eAAA;AAAA,MACzB,kBAAkB,KAAA,CAAM;AAAA,KAC1B;AACA,IAAA,IAAI,KAAA,CAAM,eAAA,KAAoB,MAAA,EAAW,IAAA,CAAK,oBAAoB,KAAA,CAAM,eAAA;AAExE,IAAA,MAAM,IAAA,GAAO,MAAM,MAAA,CAEhB,wBAAA,EAA0B,EAAE,MAAA,EAAQ,MAAA,EAAQ,MAAM,CAAA;AAErD,IAAA,MAAM,aAAA,GAAgB,KAAK,IAAA,EAAM,cAAA;AACjC,IAAA,IAAI,CAAC,aAAA,EAAe;AAClB,MAAA,MAAM,IAAI,aAAa,8CAAA,EAAgD;AAAA,QACrE,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AACA,IAAA,OAAO;AAAA,MACL,UAAA,EAAY,aAAA;AAAA,MACZ,MAAA,EAAQ,WAAA;AAAA,MACR,OAAA,EAAS,QAAA;AAAA,MACT,GAAA,EAAK;AAAA,KACP;AAAA,EACF;AAEA,EAAA,eAAe,MAAM,WAAA,EAA+C;AAClE,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,yHAAA;AAAA,MACA,EAAE,MAAM,aAAA;AAAc,KACxB;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAA;AAAA,IACN,UAAA;AAAA,IACA,WAAA;AAAA,IACA,KAAA;AAAA,IACA,gBAAA,EAAkB,CAAC,KAAA,KACjB,MAAA,CAAO,qCAAA,EAAuC;AAAA,MAC5C,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM;AAAA,QACJ,QAAA,EAAU,KAAA,CAAM,OAAA,IAAW,MAAA,CAAO,OAAA;AAAA,QAClC,SAAA,EAAW,MAAM,QAAA,IAAY,CAAA;AAAA,QAC7B,aAAA,EAAe,MAAM,YAAA,IAAgB,EAAA;AAAA,QACrC,aAAa,KAAA,CAAM,UAAA;AAAA,QACnB,gBAAgB,KAAA,CAAM,aAAA;AAAA,QACtB,gBAAgB,KAAA,CAAM;AAAA;AACxB,KACD,CAAA;AAAA,IACH,QAAQ,MAAM,MAAA,CAAO,6BAA6B,EAAE,MAAA,EAAQ,OAAO,CAAA;AAAA,IACnE,KAAA,EAAO,CAAC,MAAA,KACN,MAAA,CAAO,CAAA,uBAAA,EAA0B,MAAM,CAAA,UAAA,CAAA,EAAc,EAAE,MAAA,EAAQ,KAAA,EAAO,CAAA;AAAA,IACxE,KAAA,EAAO,CAAC,MAAA,KACN,MAAA,CAAO,CAAA,sBAAA,EAAyB,MAAM,CAAA,UAAA,CAAA,EAAc,EAAE,MAAA,EAAQ,KAAA,EAAO;AAAA,GACzE;AACF;AAUO,IAAM,yBAAA,GAA4B;AAGlC,IAAM,iBAAA,GAAoD;AAAA,EAC/D,eAAA,EAAiB,WAAA;AAAA,EACjB,eAAA,EAAiB,WAAA;AAAA,EACjB,wBAAA,EAA0B,WAAA;AAAA,EAC1B,2BAAA,EAA6B,WAAA;AAAA,EAC7B,cAAA,EAAgB,WAAA;AAAA,EAChB,qBAAA,EAAuB,QAAA;AAAA,EACvB,0BAAA,EAA4B,YAAA;AAAA,EAC5B,kBAAA,EAAoB,YAAA;AAAA,EACpB,iCAAA,EAAmC,YAAA;AAAA,EACnC,6BAAA,EAA+B,kBAAA;AAAA,EAC/B,iBAAA,EAAmB,WAAA;AAAA,EACnB,wBAAA,EAA0B,WAAA;AAAA,EAC1B,uBAAA,EAAyB,QAAA;AAAA,EACzB,eAAA,EAAiB,SAAA;AAAA,EACjB,iBAAA,EAAmB,UAAA;AAAA,EACnB,cAAA,EAAgB,UAAA;AAAA,EAChB,gBAAA,EAAkB,UAAA;AAAA,EAClB,iBAAA,EAAmB,WAAA;AAAA,EACnB,YAAA,EAAc,WAAA;AAAA,EACd,mBAAA,EAAqB;AACvB;AAGO,SAAS,sBAAsB,KAAA,EAA2C;AAC/E,EAAA,OAAO,kBAAkB,KAAK,CAAA;AAChC;AAgBO,SAAS,mBAAmB,IAAA,EAA4D;AAC7F,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,IAAI;AACF,MAAA,GAAA,GAAM,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,IACvB,CAAA,CAAA,MAAQ;AACN,MAAA,MAAM,IAAI,YAAA,CAAa,wCAAA,EAA0C,EAAE,IAAA,EAAM,gBAAgB,CAAA;AAAA,IAC3F;AAAA,EACF,CAAA,MAAO;AACL,IAAA,GAAA,GAAM,IAAA;AAAA,EACR;AAEA,EAAA,MAAM,QAAQ,GAAA,CAAI,KAAA;AAClB,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,KAAA,EAAO;AACvC,IAAA,MAAM,IAAI,YAAA,CAAa,sCAAA,EAAwC,EAAE,IAAA,EAAM,gBAAgB,CAAA;AAAA,EACzF;AAEA,EAAA,MAAM,MAAA,GAAS,sBAAsB,KAAK,CAAA;AAC1C,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,aAAa,CAAA,8BAAA,EAAiC,KAAK,IAAI,EAAE,IAAA,EAAM,iBAAiB,CAAA;AAAA,EAC5F;AAEA,EAAA,MAAM,gBAAgB,GAAA,CAAI,cAAA;AAC1B,EAAA,MAAM,kBAAkB,GAAA,CAAI,iBAAA;AAE5B,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,MAAA;AAAA,IACA,aAAA,EAAe,OAAO,aAAA,KAAkB,QAAA,GAAW,aAAA,GAAgB,MAAA;AAAA,IACnE,eAAA,EAAiB,OAAO,eAAA,KAAoB,QAAA,GAAW,eAAA,GAAkB,MAAA;AAAA,IACzE,GAAA,EAAK;AAAA,GACP;AACF;AAWO,SAAS,oBAAoB,IAAA,EAGxB;AACV,EAAA,IAAI,CAAC,IAAA,CAAK,YAAA,EAAc,OAAO,KAAA;AAC/B,EAAA,OAAO,eAAA,CAAgB,IAAA,CAAK,YAAA,EAAc,IAAA,CAAK,cAAc,CAAA;AAC/D","file":"index.js","sourcesContent":["/**\n * @lacspace/courier\n *\n * Courier / last-mile delivery toolkit for multi-vendor commerce. Three things\n * every delivery integration re-implements — done once, correctly:\n *\n * 1. **A canonical delivery state machine** — one vocabulary of statuses\n * (`pending → confirmed → picked_up → in_transit → out_for_delivery →\n * delivered`, plus `returned / cancelled / failed / on_hold`) with a\n * guarded `transition()` that refuses illegal jumps.\n * 2. **A Pathao (Nepal) adapter** — the Pathao \"Aladdin\" Merchant API v1:\n * token issue + auto-refresh, order creation, price/city/zone/area lookups.\n * 3. **Inbound webhooks** — verify a webhook's shared-secret / HMAC signature\n * (timing-safe, over Web Crypto) and normalize a carrier's event name into\n * a canonical `DeliveryStatus` so Confirmed → Delivered stops being a\n * manual admin click.\n *\n * Built on global `fetch` and Web Crypto (`globalThis.crypto.subtle`) — no Node\n * built-ins, no dependencies. Isomorphic: Node 18+, edge runtimes and browsers.\n */\n\n/* ------------------------------------------------------------------ *\n * Canonical delivery status + state machine\n * ------------------------------------------------------------------ */\n\n/** The one delivery vocabulary every carrier is normalized into. */\nexport type DeliveryStatus =\n | \"pending\"\n | \"confirmed\"\n | \"picked_up\"\n | \"in_transit\"\n | \"out_for_delivery\"\n | \"delivered\"\n | \"returned\"\n | \"cancelled\"\n | \"failed\"\n | \"on_hold\";\n\n/**\n * Allowed forward transitions for each status. Terminal states map to `[]`.\n * The set is deliberately permissive on the unhappy paths (returns, holds,\n * failures) but forbids skipping or reversing the happy path.\n */\nexport const DELIVERY_TRANSITIONS: Record<DeliveryStatus, DeliveryStatus[]> = {\n pending: [\"confirmed\", \"cancelled\"],\n confirmed: [\"picked_up\", \"cancelled\", \"on_hold\"],\n picked_up: [\"in_transit\", \"returned\", \"on_hold\"],\n in_transit: [\"out_for_delivery\", \"returned\", \"failed\", \"on_hold\"],\n out_for_delivery: [\"delivered\", \"failed\", \"returned\"],\n on_hold: [\"in_transit\", \"cancelled\", \"returned\"],\n delivered: [],\n returned: [],\n cancelled: [],\n failed: [],\n};\n\n/** `true` if `to` is a legal next state from `from`. */\nexport function canTransition(from: DeliveryStatus, to: DeliveryStatus): boolean {\n return DELIVERY_TRANSITIONS[from].includes(to);\n}\n\n/** `true` if `s` is a terminal state with no further transitions. */\nexport function isTerminal(s: DeliveryStatus): boolean {\n return DELIVERY_TRANSITIONS[s].length === 0;\n}\n\n/** Error thrown by every part of this toolkit; carries an optional `code`/`status`. */\nexport class CourierError extends Error {\n code?: string;\n status?: number;\n constructor(message: string, opts?: { code?: string; status?: number }) {\n super(message);\n this.name = \"CourierError\";\n this.code = opts?.code;\n this.status = opts?.status;\n }\n}\n\n/**\n * Return a shallow copy of `order` with its `status` advanced to `to`.\n * Throws `CourierError` (code `illegal_transition`) if the move is not allowed.\n */\nexport function transition<T extends { status: DeliveryStatus }>(order: T, to: DeliveryStatus): T {\n if (!canTransition(order.status, to)) {\n throw new CourierError(\n `Illegal delivery transition: ${order.status} → ${to}`,\n { code: \"illegal_transition\" },\n );\n }\n return { ...order, status: to };\n}\n\n/* ------------------------------------------------------------------ *\n * Generic courier adapter interface\n * ------------------------------------------------------------------ */\n\n/** A carrier-agnostic view of one shipment. */\nexport interface CourierShipment {\n trackingId: string;\n status: DeliveryStatus;\n carrier: string;\n /** The raw carrier response, kept for auditing / debugging. */\n raw?: unknown;\n}\n\n/** Generic order input; a reasonable superset across carriers. */\nexport interface CreateOrderInput {\n recipientName: string;\n recipientPhone: string;\n recipientAddress: string;\n /** Carrier location ids (Pathao uses numeric city/zone/area). */\n cityId?: number;\n zoneId?: number;\n areaId?: number;\n /** Cash-on-delivery amount to collect (0 for prepaid). */\n amountToCollect: number;\n itemQuantity: number;\n /** Weight in kilograms. */\n itemWeight: number;\n description: string;\n specialInstruction?: string;\n /** Optional merchant-side order id echoed back for reconciliation. */\n merchantOrderId?: string;\n}\n\n/** The minimum every carrier adapter implements. */\nexport interface CourierAdapter {\n name: string;\n createOrder(input: CreateOrderInput): Promise<CourierShipment>;\n track(trackingId: string): Promise<CourierShipment>;\n}\n\n/* ------------------------------------------------------------------ *\n * Web Crypto + hex helpers (isomorphic, zero-dependency)\n * ------------------------------------------------------------------ */\n\nconst enc = new TextEncoder();\n\nfunction getSubtle(): SubtleCrypto {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new CourierError(\n \"@lacspace/courier: Web Crypto (globalThis.crypto.subtle) is unavailable in this runtime.\",\n { code: \"no_web_crypto\" },\n );\n }\n return subtle;\n}\n\n/** Lowercase hex string of raw bytes. */\nfunction toHex(bytes: Uint8Array): string {\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) out += bytes[i]!.toString(16).padStart(2, \"0\");\n return out;\n}\n\n/** HMAC-SHA256 over `message` with `secret`, returned as lowercase hex. */\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const subtle = getSubtle();\n const key = await subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await subtle.sign(\"HMAC\", key, enc.encode(message));\n return toHex(new Uint8Array(sig));\n}\n\n/**\n * Constant-time comparison of two strings. Accumulates the difference over the\n * maximum length so it never early-returns on a length or character mismatch.\n */\nexport function timingSafeEqual(a: string, b: string): boolean {\n const ab = enc.encode(a);\n const bb = enc.encode(b);\n let diff = ab.length ^ bb.length;\n const n = Math.max(ab.length, bb.length);\n for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);\n return diff === 0;\n}\n\n/* ------------------------------------------------------------------ *\n * Generic webhook signature verification\n * ------------------------------------------------------------------ */\n\n/**\n * Verify an inbound webhook's `HMAC-SHA256` hex signature over the *raw*\n * request body string. Timing-safe; never throws on a bad signature.\n */\nexport async function verifyWebhookSignature(\n payload: string,\n signature: string,\n secret: string,\n): Promise<boolean> {\n const expected = await hmacSha256Hex(secret, payload);\n return timingSafeEqual(expected, signature.trim().toLowerCase());\n}\n\n/* ------------------------------------------------------------------ *\n * Pathao \"Aladdin\" Merchant API v1 adapter\n * ------------------------------------------------------------------ */\n\n/**\n * Configuration for the Pathao adapter.\n *\n * `baseUrl` defaults to production `https://api-hermes.pathao.com`. For the\n * Pathao sandbox use `https://courier-api-sandbox.pathao.com`.\n */\nexport interface PathaoConfig {\n baseUrl?: string;\n clientId: string;\n clientSecret: string;\n username: string;\n password: string;\n storeId: number;\n /** Inject a `fetch` implementation (defaults to global `fetch`). */\n fetch?: typeof fetch;\n}\n\n/** Production base URL for the Pathao Merchant API. */\nexport const PATHAO_PROD_BASE_URL = \"https://api-hermes.pathao.com\";\n/** Sandbox base URL for the Pathao Merchant API. */\nexport const PATHAO_SANDBOX_BASE_URL = \"https://courier-api-sandbox.pathao.com\";\n\nexport interface PathaoPriceInput {\n itemType?: number;\n deliveryType?: number;\n itemWeight: number;\n recipientCity: number;\n recipientZone: number;\n storeId?: number;\n}\n\n/** The extra Pathao-specific methods surfaced beyond the generic adapter. */\nexport interface PathaoAdapter extends CourierAdapter {\n issueToken(): Promise<string>;\n priceCalculation(input: PathaoPriceInput): Promise<Record<string, unknown>>;\n cities(): Promise<Record<string, unknown>>;\n zones(cityId: number): Promise<Record<string, unknown>>;\n areas(zoneId: number): Promise<Record<string, unknown>>;\n}\n\ninterface TokenState {\n accessToken: string;\n /** Absolute epoch ms at which the token should be considered expired. */\n expiresAt: number;\n}\n\n/** Refresh the token this many ms before it actually expires. */\nconst TOKEN_SKEW_MS = 60_000;\n\n/**\n * Create a Pathao Merchant API v1 adapter. Handles token issue + auto-refresh\n * and maps the generic `CreateOrderInput` onto Pathao's order body.\n */\nexport function createPathaoAdapter(config: PathaoConfig): PathaoAdapter {\n const baseUrl = (config.baseUrl ?? PATHAO_PROD_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch = config.fetch ?? globalThis.fetch;\n if (typeof doFetch !== \"function\") {\n throw new CourierError(\n \"@lacspace/courier: global fetch is unavailable; pass config.fetch.\",\n { code: \"no_fetch\" },\n );\n }\n\n let token: TokenState | null = null;\n\n async function readError(res: Response): Promise<string> {\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n try {\n body = await res.text();\n } catch {\n body = undefined;\n }\n }\n if (body && typeof body === \"object\") {\n const rec = body as Record<string, unknown>;\n const msg = rec.message ?? rec.error ?? rec.errors;\n if (msg) return typeof msg === \"string\" ? msg : JSON.stringify(msg);\n }\n if (typeof body === \"string\" && body) return body;\n return res.statusText || `HTTP ${res.status}`;\n }\n\n async function issueToken(): Promise<string> {\n const res = await doFetch(`${baseUrl}/aladdin/api/v1/issue-token`, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n grant_type: \"password\",\n username: config.username,\n password: config.password,\n }),\n });\n if (!res.ok) {\n throw new CourierError(`Pathao token request failed: ${await readError(res)}`, {\n code: \"token_failed\",\n status: res.status,\n });\n }\n const data = (await res.json()) as { access_token?: string; expires_in?: number };\n if (!data.access_token) {\n throw new CourierError(\"Pathao token response had no access_token.\", {\n code: \"token_failed\",\n status: res.status,\n });\n }\n const ttlMs = (typeof data.expires_in === \"number\" ? data.expires_in : 3600) * 1000;\n token = { accessToken: data.access_token, expiresAt: Date.now() + ttlMs };\n return token.accessToken;\n }\n\n async function getToken(): Promise<string> {\n if (token && Date.now() < token.expiresAt - TOKEN_SKEW_MS) return token.accessToken;\n return issueToken();\n }\n\n async function authed<R = Record<string, unknown>>(\n path: string,\n init: { method: \"GET\" | \"POST\"; body?: unknown },\n ): Promise<R> {\n const bearer = await getToken();\n const res = await doFetch(`${baseUrl}${path}`, {\n method: init.method,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${bearer}`,\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n if (!res.ok) {\n throw new CourierError(`Pathao ${init.method} ${path} failed: ${await readError(res)}`, {\n code: \"request_failed\",\n status: res.status,\n });\n }\n return (await res.json()) as R;\n }\n\n async function createOrder(input: CreateOrderInput): Promise<CourierShipment> {\n const body: Record<string, unknown> = {\n store_id: config.storeId,\n recipient_name: input.recipientName,\n recipient_phone: input.recipientPhone,\n recipient_address: input.recipientAddress,\n recipient_city: input.cityId,\n recipient_zone: input.zoneId,\n recipient_area: input.areaId,\n delivery_type: 48,\n item_type: 2,\n special_instruction: input.specialInstruction,\n item_quantity: input.itemQuantity,\n item_weight: input.itemWeight,\n amount_to_collect: input.amountToCollect,\n item_description: input.description,\n };\n if (input.merchantOrderId !== undefined) body.merchant_order_id = input.merchantOrderId;\n\n const data = await authed<{\n data?: { consignment_id?: string; order_status?: string; merchant_order_id?: string };\n }>(\"/aladdin/api/v1/orders\", { method: \"POST\", body });\n\n const consignmentId = data.data?.consignment_id;\n if (!consignmentId) {\n throw new CourierError(\"Pathao order response had no consignment_id.\", {\n code: \"request_failed\",\n });\n }\n return {\n trackingId: consignmentId,\n status: \"confirmed\",\n carrier: \"pathao\",\n raw: data,\n };\n }\n\n async function track(_trackingId: string): Promise<CourierShipment> {\n throw new CourierError(\n \"Pathao reports delivery status via webhooks, not a public track endpoint; use verifyPathaoWebhook + parsePathaoWebhook.\",\n { code: \"unsupported\" },\n );\n }\n\n return {\n name: \"pathao\",\n issueToken,\n createOrder,\n track,\n priceCalculation: (input: PathaoPriceInput) =>\n authed(\"/aladdin/api/v1/merchant/price-plan\", {\n method: \"POST\",\n body: {\n store_id: input.storeId ?? config.storeId,\n item_type: input.itemType ?? 2,\n delivery_type: input.deliveryType ?? 48,\n item_weight: input.itemWeight,\n recipient_city: input.recipientCity,\n recipient_zone: input.recipientZone,\n },\n }),\n cities: () => authed(\"/aladdin/api/v1/city-list\", { method: \"GET\" }),\n zones: (cityId: number) =>\n authed(`/aladdin/api/v1/cities/${cityId}/zone-list`, { method: \"GET\" }),\n areas: (zoneId: number) =>\n authed(`/aladdin/api/v1/zones/${zoneId}/area-list`, { method: \"GET\" }),\n };\n}\n\n/* ------------------------------------------------------------------ *\n * Pathao inbound webhooks\n * ------------------------------------------------------------------ */\n\n/**\n * Header Pathao requires the merchant endpoint to echo back (with HTTP 202)\n * to acknowledge a webhook, carrying the integration secret as its value.\n */\nexport const PATHAO_WEBHOOK_ACK_HEADER = \"X-Pathao-Merchant-Webhook-Integration-Secret\";\n\n/** Maps Pathao webhook event names to canonical delivery statuses. */\nexport const PATHAO_STATUS_MAP: Record<string, DeliveryStatus> = {\n \"order.created\": \"confirmed\",\n \"order.updated\": \"confirmed\",\n \"order.pickup-requested\": \"confirmed\",\n \"order.assigned-for-pickup\": \"confirmed\",\n \"order.picked\": \"picked_up\",\n \"order.pickup-failed\": \"failed\",\n \"order.at-the-sorting-hub\": \"in_transit\",\n \"order.in-transit\": \"in_transit\",\n \"order.received-at-last-mile-hub\": \"in_transit\",\n \"order.assigned-for-delivery\": \"out_for_delivery\",\n \"order.delivered\": \"delivered\",\n \"order.partial-delivery\": \"delivered\",\n \"order.delivery-failed\": \"failed\",\n \"order.on-hold\": \"on_hold\",\n \"order.returning\": \"returned\",\n \"order.return\": \"returned\",\n \"order.returned\": \"returned\",\n \"order.exchanged\": \"delivered\",\n \"order.paid\": \"delivered\",\n \"order.paid-return\": \"returned\",\n};\n\n/** Map a Pathao event name to a canonical status, or `undefined` if unknown. */\nexport function normalizePathaoStatus(event: string): DeliveryStatus | undefined {\n return PATHAO_STATUS_MAP[event];\n}\n\n/** A parsed, normalized Pathao webhook event. */\nexport interface PathaoWebhookEvent {\n event: string;\n status: DeliveryStatus;\n consignmentId?: string;\n merchantOrderId?: string;\n raw: Record<string, unknown>;\n}\n\n/**\n * Parse a Pathao webhook body (raw JSON string or already-parsed object) into a\n * normalized `PathaoWebhookEvent`. Throws `CourierError` on invalid JSON, a\n * missing `event`, or an unrecognized event name.\n */\nexport function parsePathaoWebhook(body: string | Record<string, unknown>): PathaoWebhookEvent {\n let obj: Record<string, unknown>;\n if (typeof body === \"string\") {\n try {\n obj = JSON.parse(body) as Record<string, unknown>;\n } catch {\n throw new CourierError(\"Pathao webhook body is not valid JSON.\", { code: \"invalid_body\" });\n }\n } else {\n obj = body;\n }\n\n const event = obj.event;\n if (typeof event !== \"string\" || !event) {\n throw new CourierError(\"Pathao webhook has no 'event' field.\", { code: \"invalid_body\" });\n }\n\n const status = normalizePathaoStatus(event);\n if (!status) {\n throw new CourierError(`Unknown Pathao webhook event: ${event}`, { code: \"unknown_event\" });\n }\n\n const consignmentId = obj.consignment_id;\n const merchantOrderId = obj.merchant_order_id;\n\n return {\n event,\n status,\n consignmentId: typeof consignmentId === \"string\" ? consignmentId : undefined,\n merchantOrderId: typeof merchantOrderId === \"string\" ? merchantOrderId : undefined,\n raw: obj,\n };\n}\n\n/**\n * Verify a Pathao webhook's shared secret. Pathao's integration sends a secret\n * header (e.g. `X-PATHAO-Signature`) the merchant configures; this does a\n * timing-safe string compare of the received header against the expected value.\n *\n * A `null`/`undefined`/empty header always fails. On success the merchant\n * endpoint is also expected to echo `PATHAO_WEBHOOK_ACK_HEADER` with the secret\n * and respond `202 Accepted`.\n */\nexport function verifyPathaoWebhook(opts: {\n headerSecret: string | null | undefined;\n expectedSecret: string;\n}): boolean {\n if (!opts.headerSecret) return false;\n return timingSafeEqual(opts.headerSecret, opts.expectedSecret);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/courier",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Courier / last-mile delivery toolkit — canonical delivery state machine, Pathao (Nepal) adapter, and inbound webhook verification + status normalization. Zero-dependency, isomorphic (Node, edge, browser).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"courier",
|
|
31
|
+
"logistics",
|
|
32
|
+
"delivery",
|
|
33
|
+
"pathao",
|
|
34
|
+
"nepal",
|
|
35
|
+
"last-mile",
|
|
36
|
+
"webhook",
|
|
37
|
+
"shipping",
|
|
38
|
+
"tracking",
|
|
39
|
+
"state-machine",
|
|
40
|
+
"isomorphic",
|
|
41
|
+
"typescript"
|
|
42
|
+
],
|
|
43
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
44
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
45
|
+
"homepage": "https://developer.lacspace.com/packages/courier",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
49
|
+
"directory": "courier"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/lacspace/npm-packages/issues"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|