@lacspace/courier 1.0.0 → 1.0.2
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/README.md +4 -4
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,9 +127,9 @@ All crypto uses Web Crypto (`globalThis.crypto.subtle`) — never hand-rolled. `
|
|
|
127
127
|
|
|
128
128
|
## Licensing
|
|
129
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.
|
|
130
|
+
This package is **free** under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
|
|
131
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)**.
|
|
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://developer.lacspace.com/licenses)**.
|
|
133
133
|
|
|
134
134
|
<!-- LACSPACE-DEV-PLATFORM -->
|
|
135
135
|
|
|
@@ -137,7 +137,7 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
|
|
|
137
137
|
|
|
138
138
|
## The Lacspace Developer Platform
|
|
139
139
|
|
|
140
|
-
`@lacspace/courier` is part of **
|
|
140
|
+
`@lacspace/courier` is part of **80+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
141
141
|
|
|
142
142
|
- 🗂️ **All packages** — https://developer.lacspace.com/packages
|
|
143
143
|
- 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
|
|
@@ -145,4 +145,4 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
|
|
|
145
145
|
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
146
146
|
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
147
147
|
|
|
148
|
-
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
|
148
|
+
Free under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
package/dist/index.cjs
CHANGED
|
@@ -14,7 +14,8 @@ var DELIVERY_TRANSITIONS = {
|
|
|
14
14
|
failed: []
|
|
15
15
|
};
|
|
16
16
|
function canTransition(from, to) {
|
|
17
|
-
|
|
17
|
+
const nexts = DELIVERY_TRANSITIONS[from];
|
|
18
|
+
return nexts ? nexts.includes(to) : false;
|
|
18
19
|
}
|
|
19
20
|
function isTerminal(s) {
|
|
20
21
|
return DELIVERY_TRANSITIONS[s].length === 0;
|
|
@@ -73,6 +74,7 @@ function timingSafeEqual(a, b) {
|
|
|
73
74
|
return diff === 0;
|
|
74
75
|
}
|
|
75
76
|
async function verifyWebhookSignature(payload, signature, secret) {
|
|
77
|
+
if (typeof signature !== "string" || signature.length === 0) return false;
|
|
76
78
|
const expected = await hmacSha256Hex(secret, payload);
|
|
77
79
|
return timingSafeEqual(expected, signature.trim().toLowerCase());
|
|
78
80
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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.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"]}
|
|
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 // Guard against unknown/untrusted `from` values (e.g. a status derived from\n // an inbound webhook) so we never throw on `undefined.includes`.\n const nexts = DELIVERY_TRANSITIONS[from];\n return nexts ? nexts.includes(to) : false;\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 // A missing / non-string / empty signature is never valid — and must not\n // throw (the doc guarantees \"never throws on a bad signature\").\n if (typeof signature !== \"string\" || signature.length === 0) return false;\n const expected = await hmacSha256Hex(secret, payload);\n return timingSafeEqual(expected, signature.trim().toLowerCase());\n}\n\n/* ------------------------------------------------------------------ *\n * Pathao \"Aladdin\" Merchant API v1 adapter\n * ------------------------------------------------------------------ */\n\n/**\n * Configuration for the Pathao adapter.\n *\n * `baseUrl` defaults to production `https://api-hermes.pathao.com`. For the\n * Pathao sandbox use `https://courier-api-sandbox.pathao.com`.\n */\nexport interface PathaoConfig {\n baseUrl?: string;\n clientId: string;\n clientSecret: string;\n username: string;\n password: string;\n storeId: number;\n /** Inject a `fetch` implementation (defaults to global `fetch`). */\n fetch?: typeof fetch;\n}\n\n/** Production base URL for the Pathao Merchant API. */\nexport const PATHAO_PROD_BASE_URL = \"https://api-hermes.pathao.com\";\n/** Sandbox base URL for the Pathao Merchant API. */\nexport const PATHAO_SANDBOX_BASE_URL = \"https://courier-api-sandbox.pathao.com\";\n\nexport interface PathaoPriceInput {\n itemType?: number;\n deliveryType?: number;\n itemWeight: number;\n recipientCity: number;\n recipientZone: number;\n storeId?: number;\n}\n\n/** The extra Pathao-specific methods surfaced beyond the generic adapter. */\nexport interface PathaoAdapter extends CourierAdapter {\n issueToken(): Promise<string>;\n priceCalculation(input: PathaoPriceInput): Promise<Record<string, unknown>>;\n cities(): Promise<Record<string, unknown>>;\n zones(cityId: number): Promise<Record<string, unknown>>;\n areas(zoneId: number): Promise<Record<string, unknown>>;\n}\n\ninterface TokenState {\n accessToken: string;\n /** Absolute epoch ms at which the token should be considered expired. */\n expiresAt: number;\n}\n\n/** Refresh the token this many ms before it actually expires. */\nconst TOKEN_SKEW_MS = 60_000;\n\n/**\n * Create a Pathao Merchant API v1 adapter. Handles token issue + auto-refresh\n * and maps the generic `CreateOrderInput` onto Pathao's order body.\n */\nexport function createPathaoAdapter(config: PathaoConfig): PathaoAdapter {\n const baseUrl = (config.baseUrl ?? PATHAO_PROD_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch = config.fetch ?? globalThis.fetch;\n if (typeof doFetch !== \"function\") {\n throw new CourierError(\n \"@lacspace/courier: global fetch is unavailable; pass config.fetch.\",\n { code: \"no_fetch\" },\n );\n }\n\n let token: TokenState | null = null;\n\n async function readError(res: Response): Promise<string> {\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n try {\n body = await res.text();\n } catch {\n body = undefined;\n }\n }\n if (body && typeof body === \"object\") {\n const rec = body as Record<string, unknown>;\n const msg = rec.message ?? rec.error ?? rec.errors;\n if (msg) return typeof msg === \"string\" ? msg : JSON.stringify(msg);\n }\n if (typeof body === \"string\" && body) return body;\n return res.statusText || `HTTP ${res.status}`;\n }\n\n async function issueToken(): Promise<string> {\n const res = await doFetch(`${baseUrl}/aladdin/api/v1/issue-token`, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n grant_type: \"password\",\n username: config.username,\n password: config.password,\n }),\n });\n if (!res.ok) {\n throw new CourierError(`Pathao token request failed: ${await readError(res)}`, {\n code: \"token_failed\",\n status: res.status,\n });\n }\n const data = (await res.json()) as { access_token?: string; expires_in?: number };\n if (!data.access_token) {\n throw new CourierError(\"Pathao token response had no access_token.\", {\n code: \"token_failed\",\n status: res.status,\n });\n }\n const ttlMs = (typeof data.expires_in === \"number\" ? data.expires_in : 3600) * 1000;\n token = { accessToken: data.access_token, expiresAt: Date.now() + ttlMs };\n return token.accessToken;\n }\n\n async function getToken(): Promise<string> {\n if (token && Date.now() < token.expiresAt - TOKEN_SKEW_MS) return token.accessToken;\n return issueToken();\n }\n\n async function authed<R = Record<string, unknown>>(\n path: string,\n init: { method: \"GET\" | \"POST\"; body?: unknown },\n ): Promise<R> {\n const bearer = await getToken();\n const res = await doFetch(`${baseUrl}${path}`, {\n method: init.method,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${bearer}`,\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n if (!res.ok) {\n throw new CourierError(`Pathao ${init.method} ${path} failed: ${await readError(res)}`, {\n code: \"request_failed\",\n status: res.status,\n });\n }\n return (await res.json()) as R;\n }\n\n async function createOrder(input: CreateOrderInput): Promise<CourierShipment> {\n const body: Record<string, unknown> = {\n store_id: config.storeId,\n recipient_name: input.recipientName,\n recipient_phone: input.recipientPhone,\n recipient_address: input.recipientAddress,\n recipient_city: input.cityId,\n recipient_zone: input.zoneId,\n recipient_area: input.areaId,\n delivery_type: 48,\n item_type: 2,\n special_instruction: input.specialInstruction,\n item_quantity: input.itemQuantity,\n item_weight: input.itemWeight,\n amount_to_collect: input.amountToCollect,\n item_description: input.description,\n };\n if (input.merchantOrderId !== undefined) body.merchant_order_id = input.merchantOrderId;\n\n const data = await authed<{\n data?: { consignment_id?: string; order_status?: string; merchant_order_id?: string };\n }>(\"/aladdin/api/v1/orders\", { method: \"POST\", body });\n\n const consignmentId = data.data?.consignment_id;\n if (!consignmentId) {\n throw new CourierError(\"Pathao order response had no consignment_id.\", {\n code: \"request_failed\",\n });\n }\n return {\n trackingId: consignmentId,\n status: \"confirmed\",\n carrier: \"pathao\",\n raw: data,\n };\n }\n\n async function track(_trackingId: string): Promise<CourierShipment> {\n throw new CourierError(\n \"Pathao reports delivery status via webhooks, not a public track endpoint; use verifyPathaoWebhook + parsePathaoWebhook.\",\n { code: \"unsupported\" },\n );\n }\n\n return {\n name: \"pathao\",\n issueToken,\n createOrder,\n track,\n priceCalculation: (input: PathaoPriceInput) =>\n authed(\"/aladdin/api/v1/merchant/price-plan\", {\n method: \"POST\",\n body: {\n store_id: input.storeId ?? config.storeId,\n item_type: input.itemType ?? 2,\n delivery_type: input.deliveryType ?? 48,\n item_weight: input.itemWeight,\n recipient_city: input.recipientCity,\n recipient_zone: input.recipientZone,\n },\n }),\n cities: () => authed(\"/aladdin/api/v1/city-list\", { method: \"GET\" }),\n zones: (cityId: number) =>\n authed(`/aladdin/api/v1/cities/${cityId}/zone-list`, { method: \"GET\" }),\n areas: (zoneId: number) =>\n authed(`/aladdin/api/v1/zones/${zoneId}/area-list`, { method: \"GET\" }),\n };\n}\n\n/* ------------------------------------------------------------------ *\n * Pathao inbound webhooks\n * ------------------------------------------------------------------ */\n\n/**\n * Header Pathao requires the merchant endpoint to echo back (with HTTP 202)\n * to acknowledge a webhook, carrying the integration secret as its value.\n */\nexport const PATHAO_WEBHOOK_ACK_HEADER = \"X-Pathao-Merchant-Webhook-Integration-Secret\";\n\n/** Maps Pathao webhook event names to canonical delivery statuses. */\nexport const PATHAO_STATUS_MAP: Record<string, DeliveryStatus> = {\n \"order.created\": \"confirmed\",\n \"order.updated\": \"confirmed\",\n \"order.pickup-requested\": \"confirmed\",\n \"order.assigned-for-pickup\": \"confirmed\",\n \"order.picked\": \"picked_up\",\n \"order.pickup-failed\": \"failed\",\n \"order.at-the-sorting-hub\": \"in_transit\",\n \"order.in-transit\": \"in_transit\",\n \"order.received-at-last-mile-hub\": \"in_transit\",\n \"order.assigned-for-delivery\": \"out_for_delivery\",\n \"order.delivered\": \"delivered\",\n \"order.partial-delivery\": \"delivered\",\n \"order.delivery-failed\": \"failed\",\n \"order.on-hold\": \"on_hold\",\n \"order.returning\": \"returned\",\n \"order.return\": \"returned\",\n \"order.returned\": \"returned\",\n \"order.exchanged\": \"delivered\",\n \"order.paid\": \"delivered\",\n \"order.paid-return\": \"returned\",\n};\n\n/** Map a Pathao event name to a canonical status, or `undefined` if unknown. */\nexport function normalizePathaoStatus(event: string): DeliveryStatus | undefined {\n return PATHAO_STATUS_MAP[event];\n}\n\n/** A parsed, normalized Pathao webhook event. */\nexport interface PathaoWebhookEvent {\n event: string;\n status: DeliveryStatus;\n consignmentId?: string;\n merchantOrderId?: string;\n raw: Record<string, unknown>;\n}\n\n/**\n * Parse a Pathao webhook body (raw JSON string or already-parsed object) into a\n * normalized `PathaoWebhookEvent`. Throws `CourierError` on invalid JSON, a\n * missing `event`, or an unrecognized event name.\n */\nexport function parsePathaoWebhook(body: string | Record<string, unknown>): PathaoWebhookEvent {\n let obj: Record<string, unknown>;\n if (typeof body === \"string\") {\n try {\n obj = JSON.parse(body) as Record<string, unknown>;\n } catch {\n throw new CourierError(\"Pathao webhook body is not valid JSON.\", { code: \"invalid_body\" });\n }\n } else {\n obj = body;\n }\n\n const event = obj.event;\n if (typeof event !== \"string\" || !event) {\n throw new CourierError(\"Pathao webhook has no 'event' field.\", { code: \"invalid_body\" });\n }\n\n const status = normalizePathaoStatus(event);\n if (!status) {\n throw new CourierError(`Unknown Pathao webhook event: ${event}`, { code: \"unknown_event\" });\n }\n\n const consignmentId = obj.consignment_id;\n const merchantOrderId = obj.merchant_order_id;\n\n return {\n event,\n status,\n consignmentId: typeof consignmentId === \"string\" ? consignmentId : undefined,\n merchantOrderId: typeof merchantOrderId === \"string\" ? merchantOrderId : undefined,\n raw: obj,\n };\n}\n\n/**\n * Verify a Pathao webhook's shared secret. Pathao's integration sends a secret\n * header (e.g. `X-PATHAO-Signature`) the merchant configures; this does a\n * timing-safe string compare of the received header against the expected value.\n *\n * A `null`/`undefined`/empty header always fails. On success the merchant\n * endpoint is also expected to echo `PATHAO_WEBHOOK_ACK_HEADER` with the secret\n * and respond `202 Accepted`.\n */\nexport function verifyPathaoWebhook(opts: {\n headerSecret: string | null | undefined;\n expectedSecret: string;\n}): boolean {\n if (!opts.headerSecret) return false;\n return timingSafeEqual(opts.headerSecret, opts.expectedSecret);\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,8 @@ var DELIVERY_TRANSITIONS = {
|
|
|
12
12
|
failed: []
|
|
13
13
|
};
|
|
14
14
|
function canTransition(from, to) {
|
|
15
|
-
|
|
15
|
+
const nexts = DELIVERY_TRANSITIONS[from];
|
|
16
|
+
return nexts ? nexts.includes(to) : false;
|
|
16
17
|
}
|
|
17
18
|
function isTerminal(s) {
|
|
18
19
|
return DELIVERY_TRANSITIONS[s].length === 0;
|
|
@@ -71,6 +72,7 @@ function timingSafeEqual(a, b) {
|
|
|
71
72
|
return diff === 0;
|
|
72
73
|
}
|
|
73
74
|
async function verifyWebhookSignature(payload, signature, secret) {
|
|
75
|
+
if (typeof signature !== "string" || signature.length === 0) return false;
|
|
74
76
|
const expected = await hmacSha256Hex(secret, payload);
|
|
75
77
|
return timingSafeEqual(expected, signature.trim().toLowerCase());
|
|
76
78
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","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 // Guard against unknown/untrusted `from` values (e.g. a status derived from\n // an inbound webhook) so we never throw on `undefined.includes`.\n const nexts = DELIVERY_TRANSITIONS[from];\n return nexts ? nexts.includes(to) : false;\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 // A missing / non-string / empty signature is never valid — and must not\n // throw (the doc guarantees \"never throws on a bad signature\").\n if (typeof signature !== \"string\" || signature.length === 0) return false;\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacspace/courier",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|