@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 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
+ [![npm version](https://img.shields.io/npm/v/@lacspace/courier?color=%2316a34a&label=npm)](https://www.npmjs.com/package/@lacspace/courier)
8
+ [![install size](https://packagephobia.com/badge?p=@lacspace/courier)](https://packagephobia.com/result?p=@lacspace/courier)
9
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/courier?label=minzip)](https://bundlephobia.com/package/@lacspace/courier)
10
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/courier)
11
+ [![license](https://img.shields.io/npm/l/@lacspace/courier?color=green)](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"]}
@@ -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 };
@@ -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
+ }