@sakeetech/viva-payments-core 0.2.1

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.
Files changed (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -0
  3. package/dist/auth/http.d.ts +44 -0
  4. package/dist/auth/http.d.ts.map +1 -0
  5. package/dist/auth/http.js +80 -0
  6. package/dist/auth/http.js.map +1 -0
  7. package/dist/auth/index.d.ts +19 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/index.js +18 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/oauth2-strategy.d.ts +117 -0
  12. package/dist/auth/oauth2-strategy.d.ts.map +1 -0
  13. package/dist/auth/oauth2-strategy.js +217 -0
  14. package/dist/auth/oauth2-strategy.js.map +1 -0
  15. package/dist/auth/reseller-strategy.d.ts +65 -0
  16. package/dist/auth/reseller-strategy.d.ts.map +1 -0
  17. package/dist/auth/reseller-strategy.js +68 -0
  18. package/dist/auth/reseller-strategy.js.map +1 -0
  19. package/dist/auth/single-flight.d.ts +81 -0
  20. package/dist/auth/single-flight.d.ts.map +1 -0
  21. package/dist/auth/single-flight.js +160 -0
  22. package/dist/auth/single-flight.js.map +1 -0
  23. package/dist/auth/token-cache.d.ts +50 -0
  24. package/dist/auth/token-cache.d.ts.map +1 -0
  25. package/dist/auth/token-cache.js +59 -0
  26. package/dist/auth/token-cache.js.map +1 -0
  27. package/dist/errors/api-error.d.ts +15 -0
  28. package/dist/errors/api-error.d.ts.map +1 -0
  29. package/dist/errors/api-error.js +18 -0
  30. package/dist/errors/api-error.js.map +1 -0
  31. package/dist/errors/auth-error.d.ts +14 -0
  32. package/dist/errors/auth-error.d.ts.map +1 -0
  33. package/dist/errors/auth-error.js +17 -0
  34. package/dist/errors/auth-error.js.map +1 -0
  35. package/dist/errors/base.d.ts +59 -0
  36. package/dist/errors/base.d.ts.map +1 -0
  37. package/dist/errors/base.js +51 -0
  38. package/dist/errors/base.js.map +1 -0
  39. package/dist/errors/index.d.ts +18 -0
  40. package/dist/errors/index.d.ts.map +1 -0
  41. package/dist/errors/index.js +16 -0
  42. package/dist/errors/index.js.map +1 -0
  43. package/dist/errors/mode-mismatch-error.d.ts +19 -0
  44. package/dist/errors/mode-mismatch-error.d.ts.map +1 -0
  45. package/dist/errors/mode-mismatch-error.js +22 -0
  46. package/dist/errors/mode-mismatch-error.js.map +1 -0
  47. package/dist/errors/rate-limit-error.d.ts +20 -0
  48. package/dist/errors/rate-limit-error.d.ts.map +1 -0
  49. package/dist/errors/rate-limit-error.js +20 -0
  50. package/dist/errors/rate-limit-error.js.map +1 -0
  51. package/dist/errors/validation-error.d.ts +14 -0
  52. package/dist/errors/validation-error.d.ts.map +1 -0
  53. package/dist/errors/validation-error.js +17 -0
  54. package/dist/errors/validation-error.js.map +1 -0
  55. package/dist/errors/webhook-error.d.ts +14 -0
  56. package/dist/errors/webhook-error.d.ts.map +1 -0
  57. package/dist/errors/webhook-error.js +17 -0
  58. package/dist/errors/webhook-error.js.map +1 -0
  59. package/dist/index.d.ts +15 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +15 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/isv/accounts.d.ts +38 -0
  64. package/dist/isv/accounts.d.ts.map +1 -0
  65. package/dist/isv/accounts.js +60 -0
  66. package/dist/isv/accounts.js.map +1 -0
  67. package/dist/isv/client.d.ts +187 -0
  68. package/dist/isv/client.d.ts.map +1 -0
  69. package/dist/isv/client.js +465 -0
  70. package/dist/isv/client.js.map +1 -0
  71. package/dist/isv/index.d.ts +52 -0
  72. package/dist/isv/index.d.ts.map +1 -0
  73. package/dist/isv/index.js +53 -0
  74. package/dist/isv/index.js.map +1 -0
  75. package/dist/isv/legacy-basic-client.d.ts +122 -0
  76. package/dist/isv/legacy-basic-client.d.ts.map +1 -0
  77. package/dist/isv/legacy-basic-client.js +281 -0
  78. package/dist/isv/legacy-basic-client.js.map +1 -0
  79. package/dist/isv/payments.d.ts +199 -0
  80. package/dist/isv/payments.d.ts.map +1 -0
  81. package/dist/isv/payments.js +385 -0
  82. package/dist/isv/payments.js.map +1 -0
  83. package/dist/isv/sources.d.ts +80 -0
  84. package/dist/isv/sources.d.ts.map +1 -0
  85. package/dist/isv/sources.js +112 -0
  86. package/dist/isv/sources.js.map +1 -0
  87. package/dist/isv/webhooks-api.d.ts +48 -0
  88. package/dist/isv/webhooks-api.d.ts.map +1 -0
  89. package/dist/isv/webhooks-api.js +66 -0
  90. package/dist/isv/webhooks-api.js.map +1 -0
  91. package/dist/legacy/client.d.ts +199 -0
  92. package/dist/legacy/client.d.ts.map +1 -0
  93. package/dist/legacy/client.js +351 -0
  94. package/dist/legacy/client.js.map +1 -0
  95. package/dist/legacy/index.d.ts +15 -0
  96. package/dist/legacy/index.d.ts.map +1 -0
  97. package/dist/legacy/index.js +14 -0
  98. package/dist/legacy/index.js.map +1 -0
  99. package/dist/observability/context.d.ts +30 -0
  100. package/dist/observability/context.d.ts.map +1 -0
  101. package/dist/observability/context.js +40 -0
  102. package/dist/observability/context.js.map +1 -0
  103. package/dist/observability/index.d.ts +15 -0
  104. package/dist/observability/index.d.ts.map +1 -0
  105. package/dist/observability/index.js +11 -0
  106. package/dist/observability/index.js.map +1 -0
  107. package/dist/observability/logger.d.ts +81 -0
  108. package/dist/observability/logger.d.ts.map +1 -0
  109. package/dist/observability/logger.js +127 -0
  110. package/dist/observability/logger.js.map +1 -0
  111. package/dist/observability/metrics.d.ts +37 -0
  112. package/dist/observability/metrics.d.ts.map +1 -0
  113. package/dist/observability/metrics.js +40 -0
  114. package/dist/observability/metrics.js.map +1 -0
  115. package/dist/observability/redact.d.ts +21 -0
  116. package/dist/observability/redact.d.ts.map +1 -0
  117. package/dist/observability/redact.js +72 -0
  118. package/dist/observability/redact.js.map +1 -0
  119. package/dist/observability/tracer.d.ts +25 -0
  120. package/dist/observability/tracer.d.ts.map +1 -0
  121. package/dist/observability/tracer.js +18 -0
  122. package/dist/observability/tracer.js.map +1 -0
  123. package/dist/payments/client.d.ts +247 -0
  124. package/dist/payments/client.d.ts.map +1 -0
  125. package/dist/payments/client.js +488 -0
  126. package/dist/payments/client.js.map +1 -0
  127. package/dist/payments/index.d.ts +14 -0
  128. package/dist/payments/index.d.ts.map +1 -0
  129. package/dist/payments/index.js +13 -0
  130. package/dist/payments/index.js.map +1 -0
  131. package/dist/refunds/fast-refund-client.d.ts +128 -0
  132. package/dist/refunds/fast-refund-client.d.ts.map +1 -0
  133. package/dist/refunds/fast-refund-client.js +138 -0
  134. package/dist/refunds/fast-refund-client.js.map +1 -0
  135. package/dist/refunds/index.d.ts +19 -0
  136. package/dist/refunds/index.d.ts.map +1 -0
  137. package/dist/refunds/index.js +17 -0
  138. package/dist/refunds/index.js.map +1 -0
  139. package/dist/refunds/strategy.d.ts +78 -0
  140. package/dist/refunds/strategy.d.ts.map +1 -0
  141. package/dist/refunds/strategy.js +75 -0
  142. package/dist/refunds/strategy.js.map +1 -0
  143. package/dist/types/auth.d.ts +80 -0
  144. package/dist/types/auth.d.ts.map +1 -0
  145. package/dist/types/auth.js +12 -0
  146. package/dist/types/auth.js.map +1 -0
  147. package/dist/types/card-types.d.ts +48 -0
  148. package/dist/types/card-types.d.ts.map +1 -0
  149. package/dist/types/card-types.js +62 -0
  150. package/dist/types/card-types.js.map +1 -0
  151. package/dist/types/common.d.ts +160 -0
  152. package/dist/types/common.d.ts.map +1 -0
  153. package/dist/types/common.js +70 -0
  154. package/dist/types/common.js.map +1 -0
  155. package/dist/types/index.d.ts +21 -0
  156. package/dist/types/index.d.ts.map +1 -0
  157. package/dist/types/index.js +21 -0
  158. package/dist/types/index.js.map +1 -0
  159. package/dist/types/isv-accounts.d.ts +109 -0
  160. package/dist/types/isv-accounts.d.ts.map +1 -0
  161. package/dist/types/isv-accounts.js +22 -0
  162. package/dist/types/isv-accounts.js.map +1 -0
  163. package/dist/types/isv-payments.d.ts +262 -0
  164. package/dist/types/isv-payments.d.ts.map +1 -0
  165. package/dist/types/isv-payments.js +19 -0
  166. package/dist/types/isv-payments.js.map +1 -0
  167. package/dist/types/status.d.ts +125 -0
  168. package/dist/types/status.d.ts.map +1 -0
  169. package/dist/types/status.js +19 -0
  170. package/dist/types/status.js.map +1 -0
  171. package/dist/types/webhook-events.d.ts +447 -0
  172. package/dist/types/webhook-events.d.ts.map +1 -0
  173. package/dist/types/webhook-events.js +76 -0
  174. package/dist/types/webhook-events.js.map +1 -0
  175. package/dist/webhooks/challenge-response.d.ts +28 -0
  176. package/dist/webhooks/challenge-response.d.ts.map +1 -0
  177. package/dist/webhooks/challenge-response.js +35 -0
  178. package/dist/webhooks/challenge-response.js.map +1 -0
  179. package/dist/webhooks/event-types.d.ts +44 -0
  180. package/dist/webhooks/event-types.d.ts.map +1 -0
  181. package/dist/webhooks/event-types.js +50 -0
  182. package/dist/webhooks/event-types.js.map +1 -0
  183. package/dist/webhooks/extract-client-ip.d.ts +40 -0
  184. package/dist/webhooks/extract-client-ip.d.ts.map +1 -0
  185. package/dist/webhooks/extract-client-ip.js +72 -0
  186. package/dist/webhooks/extract-client-ip.js.map +1 -0
  187. package/dist/webhooks/hmac-verify.d.ts +38 -0
  188. package/dist/webhooks/hmac-verify.d.ts.map +1 -0
  189. package/dist/webhooks/hmac-verify.js +92 -0
  190. package/dist/webhooks/hmac-verify.js.map +1 -0
  191. package/dist/webhooks/index.d.ts +19 -0
  192. package/dist/webhooks/index.d.ts.map +1 -0
  193. package/dist/webhooks/index.js +19 -0
  194. package/dist/webhooks/index.js.map +1 -0
  195. package/dist/webhooks/ip-allowlist.d.ts +59 -0
  196. package/dist/webhooks/ip-allowlist.d.ts.map +1 -0
  197. package/dist/webhooks/ip-allowlist.js +147 -0
  198. package/dist/webhooks/ip-allowlist.js.map +1 -0
  199. package/dist/webhooks/status-lattice.d.ts +72 -0
  200. package/dist/webhooks/status-lattice.d.ts.map +1 -0
  201. package/dist/webhooks/status-lattice.js +208 -0
  202. package/dist/webhooks/status-lattice.js.map +1 -0
  203. package/package.json +85 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Runtime helpers for Viva webhook event type IDs.
3
+ *
4
+ * Re-exports EVENT_TYPES and VivaEventTypeId from the types package, and
5
+ * provides runtime type-guard helpers for routing webhook payloads.
6
+ *
7
+ * @see references/viva-docs/md/webhooks-for-payments.txt:141
8
+ */
9
+ export { EVENT_TYPES } from '../types/webhook-events.js';
10
+ /**
11
+ * Returns `true` for event types that carry a transaction payload and require
12
+ * MerchantId-based tenant resolution.
13
+ *
14
+ * v1-scope transaction events:
15
+ * 1796 — Transaction Payment Created
16
+ * 1797 — Transaction Reversal Created
17
+ * 1798 — Transaction Failed
18
+ * 4865 — Order Updated (cancellation)
19
+ * 7936 — Sale Transactions (HMAC-signed, deferred from v1 core but type-guard kept)
20
+ *
21
+ * @see references/viva-docs/md/webhooks-for-payments.txt:141
22
+ */
23
+ export function isTransactionEvent(id) {
24
+ return id === 1796 || id === 1797 || id === 1798 || id === 4865 || id === 7936;
25
+ }
26
+ /**
27
+ * Returns `true` for event types that carry an onboarding payload and require
28
+ * ConnectedAccountId-based tenant resolution.
29
+ *
30
+ * v1-scope onboarding events:
31
+ * 8193 — Account Connected
32
+ * 8194 — Account Verification Status Changed
33
+ *
34
+ * @see references/viva-docs/md/webhooks-for-payments.txt:224
35
+ */
36
+ export function isOnboardingEvent(id) {
37
+ return id === 8193 || id === 8194;
38
+ }
39
+ /**
40
+ * All v1-scope event IDs as a frozen array.
41
+ *
42
+ * Used by `viva:register-webhooks` CLI to enumerate the required registrations
43
+ * and by the route handler to reject unknown event types.
44
+ *
45
+ * @see references/viva-docs/md/webhooks-for-payments.txt:141
46
+ */
47
+ export const V1_EVENT_TYPE_IDS = Object.freeze([
48
+ 1796, 1797, 1798, 4865, 8193, 8194,
49
+ ]);
50
+ //# sourceMappingURL=event-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-types.js","sourceRoot":"","sources":["../../src/webhooks/event-types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAwB,MAAM,4BAA4B,CAAC;AAE/E;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU;IAC3C,OAAO,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC;AACjF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAU;IAC1C,OAAO,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC;AACpC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAyD,MAAM,CAAC,MAAM,CAAC;IACnG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;CAC1B,CAAC,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * extract-client-ip.ts — Source IP extraction with explicit proxy-depth trust.
3
+ *
4
+ * CSO Finding #2 (HIGH). Unconditionally trusting `X-Forwarded-For` lets any
5
+ * client append a value and bypass the in-app IP allowlist. The remedy is to
6
+ * walk the X-F-F chain from the *rightmost* end, counting back exactly
7
+ * `trustedProxyDepth` hops. Anything earlier in the chain was set by upstream
8
+ * proxies the operator does not control.
9
+ *
10
+ * Usage:
11
+ * trustedProxyDepth = 0 → ignore X-F-F entirely; use req.socket.remoteAddress
12
+ * trustedProxyDepth = 1 → one trusted reverse proxy (typical)
13
+ * trustedProxyDepth = 2 → CDN + LB (e.g. Cloudflare → ALB)
14
+ *
15
+ * When the chain is shorter than `trustedProxyDepth`, the request is treated
16
+ * as misconfigured or malicious: we fall back to the socket address rather
17
+ * than picking an attacker-controllable value.
18
+ *
19
+ * @see docs/TODO-CSO.md "Finding 2"
20
+ */
21
+ import type { IncomingMessage } from 'node:http';
22
+ /**
23
+ * Minimal request shape — accepts Node's IncomingMessage and any framework
24
+ * request that exposes headers + socket (Medusa, Express, NestJS, etc.).
25
+ */
26
+ export interface ClientIpRequest {
27
+ readonly headers: IncomingMessage['headers'];
28
+ readonly socket?: {
29
+ readonly remoteAddress?: string | undefined;
30
+ } | undefined;
31
+ }
32
+ /**
33
+ * Extract the trusted client IP from a request.
34
+ *
35
+ * Returns an empty string if no source can be determined (caller decides
36
+ * whether to reject or fall back to a default). Strips IPv6 brackets if
37
+ * present (e.g. `[2001:db8::1]:443` → `2001:db8::1`).
38
+ */
39
+ export declare function extractClientIp(req: ClientIpRequest, trustedProxyDepth: number): string;
40
+ //# sourceMappingURL=extract-client-ip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-client-ip.d.ts","sourceRoot":"","sources":["../../src/webhooks/extract-client-ip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAAG,SAAS,CAAC;CAC/E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,eAAe,EACpB,iBAAiB,EAAE,MAAM,GACxB,MAAM,CAoBR"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * extract-client-ip.ts — Source IP extraction with explicit proxy-depth trust.
3
+ *
4
+ * CSO Finding #2 (HIGH). Unconditionally trusting `X-Forwarded-For` lets any
5
+ * client append a value and bypass the in-app IP allowlist. The remedy is to
6
+ * walk the X-F-F chain from the *rightmost* end, counting back exactly
7
+ * `trustedProxyDepth` hops. Anything earlier in the chain was set by upstream
8
+ * proxies the operator does not control.
9
+ *
10
+ * Usage:
11
+ * trustedProxyDepth = 0 → ignore X-F-F entirely; use req.socket.remoteAddress
12
+ * trustedProxyDepth = 1 → one trusted reverse proxy (typical)
13
+ * trustedProxyDepth = 2 → CDN + LB (e.g. Cloudflare → ALB)
14
+ *
15
+ * When the chain is shorter than `trustedProxyDepth`, the request is treated
16
+ * as misconfigured or malicious: we fall back to the socket address rather
17
+ * than picking an attacker-controllable value.
18
+ *
19
+ * @see docs/TODO-CSO.md "Finding 2"
20
+ */
21
+ /**
22
+ * Extract the trusted client IP from a request.
23
+ *
24
+ * Returns an empty string if no source can be determined (caller decides
25
+ * whether to reject or fall back to a default). Strips IPv6 brackets if
26
+ * present (e.g. `[2001:db8::1]:443` → `2001:db8::1`).
27
+ */
28
+ export function extractClientIp(req, trustedProxyDepth) {
29
+ const depth = Number.isInteger(trustedProxyDepth) && trustedProxyDepth >= 0
30
+ ? trustedProxyDepth
31
+ : 0;
32
+ if (depth === 0) {
33
+ return normalizeIp(req.socket?.remoteAddress ?? '');
34
+ }
35
+ const xff = req.headers['x-forwarded-for'];
36
+ const chain = flattenXff(xff);
37
+ if (chain.length < depth) {
38
+ // Chain shorter than configured trust depth — operator misconfig or an
39
+ // attacker omitting expected hops. Fall back to socket; do NOT pick the
40
+ // leftmost (attacker-controllable) value.
41
+ return normalizeIp(req.socket?.remoteAddress ?? '');
42
+ }
43
+ return normalizeIp(chain[chain.length - depth] ?? '');
44
+ }
45
+ function flattenXff(xff) {
46
+ if (!xff)
47
+ return [];
48
+ // Node may surface repeated headers as either a comma-joined string or an
49
+ // array of strings; normalize both into one comma-separated source.
50
+ const joined = Array.isArray(xff) ? xff.join(',') : xff;
51
+ return joined
52
+ .split(',')
53
+ .map((s) => s.trim())
54
+ .filter((s) => s.length > 0);
55
+ }
56
+ function normalizeIp(raw) {
57
+ const trimmed = raw.trim();
58
+ if (!trimmed)
59
+ return '';
60
+ // IPv6-mapped IPv4 (::ffff:1.2.3.4) → 1.2.3.4
61
+ if (trimmed.toLowerCase().startsWith('::ffff:')) {
62
+ return trimmed.slice(7);
63
+ }
64
+ // Bracketed IPv6 with optional :port — [2001:db8::1]:443 → 2001:db8::1
65
+ if (trimmed.startsWith('[')) {
66
+ const close = trimmed.indexOf(']');
67
+ if (close > 0)
68
+ return trimmed.slice(1, close);
69
+ }
70
+ return trimmed;
71
+ }
72
+ //# sourceMappingURL=extract-client-ip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-client-ip.js","sourceRoot":"","sources":["../../src/webhooks/extract-client-ip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAaH;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAoB,EACpB,iBAAyB;IAEzB,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,IAAI,CAAC;QACzE,CAAC,CAAC,iBAAiB;QACnB,CAAC,CAAC,CAAC,CAAC;IAEN,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,OAAO,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAE9B,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,EAAE,CAAC;QACzB,uEAAuE;QACvE,wEAAwE;QACxE,0CAA0C;QAC1C,OAAO,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,UAAU,CACjB,GAAkC;IAElC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,0EAA0E;IAC1E,oEAAoE;IACpE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACxD,OAAO,MAAM;SACV,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,8CAA8C;IAC9C,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAChD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IACD,uEAAuE;IACvE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * HMAC-SHA256 signature verification for the Sale Transactions webhook (event 7936).
3
+ *
4
+ * PER PLAN A8: HMAC verification applies ONLY to event 7936 (Sale Transactions).
5
+ * Viva does NOT document an HMAC header for any other v1 event type. For all
6
+ * other events, security relies on:
7
+ * (a) IP allowlist — see ip-allowlist.ts
8
+ * (b) Challenge-response URL verification — see challenge-response.ts
9
+ * (c) DB-level deduplication via opaque MessageId (ON CONFLICT DO NOTHING)
10
+ *
11
+ * Header format (from Viva documentation):
12
+ * `Viva-Signature-256`: HMAC hex digest of the request body,
13
+ * generated using SHA-256 and the webhook secret as the HMAC key.
14
+ *
15
+ * @see references/viva-docs/md/wh-sale-transactions.txt:149
16
+ */
17
+ /**
18
+ * Verify the `Viva-Signature-256` header for the Sale Transactions webhook
19
+ * (event 7936 only).
20
+ *
21
+ * Uses `crypto.timingSafeEqual` for constant-time comparison. Length mismatches
22
+ * are handled without calling `timingSafeEqual` (which would throw on unequal
23
+ * lengths) by comparing against a zeroed buffer of the same expected length.
24
+ *
25
+ * @param rawBody The exact raw request body bytes received over the wire.
26
+ * Pass a `Buffer` or `Uint8Array` for binary-safe comparison;
27
+ * a `string` is accepted and UTF-8-encoded internally.
28
+ * @param signature Value of the `Viva-Signature-256` HTTP header.
29
+ * Must be a lowercase hex digest (64 hex chars for SHA-256).
30
+ * @param secret HMAC secret from Viva configuration.
31
+ *
32
+ * @throws VivaWebhookError when the signature is missing, malformed, or does
33
+ * not match the computed digest.
34
+ *
35
+ * @see references/viva-docs/md/wh-sale-transactions.txt:149
36
+ */
37
+ export declare function verifyHmacSignature(rawBody: Uint8Array | string, signature: string | undefined | null, secret: string): void;
38
+ //# sourceMappingURL=hmac-verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmac-verify.d.ts","sourceRoot":"","sources":["../../src/webhooks/hmac-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,UAAU,GAAG,MAAM,EAC5B,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACpC,MAAM,EAAE,MAAM,GACb,IAAI,CA0DN"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * HMAC-SHA256 signature verification for the Sale Transactions webhook (event 7936).
3
+ *
4
+ * PER PLAN A8: HMAC verification applies ONLY to event 7936 (Sale Transactions).
5
+ * Viva does NOT document an HMAC header for any other v1 event type. For all
6
+ * other events, security relies on:
7
+ * (a) IP allowlist — see ip-allowlist.ts
8
+ * (b) Challenge-response URL verification — see challenge-response.ts
9
+ * (c) DB-level deduplication via opaque MessageId (ON CONFLICT DO NOTHING)
10
+ *
11
+ * Header format (from Viva documentation):
12
+ * `Viva-Signature-256`: HMAC hex digest of the request body,
13
+ * generated using SHA-256 and the webhook secret as the HMAC key.
14
+ *
15
+ * @see references/viva-docs/md/wh-sale-transactions.txt:149
16
+ */
17
+ import { createHmac, timingSafeEqual } from 'node:crypto';
18
+ import { VivaWebhookError } from '../errors/index.js';
19
+ /**
20
+ * Verify the `Viva-Signature-256` header for the Sale Transactions webhook
21
+ * (event 7936 only).
22
+ *
23
+ * Uses `crypto.timingSafeEqual` for constant-time comparison. Length mismatches
24
+ * are handled without calling `timingSafeEqual` (which would throw on unequal
25
+ * lengths) by comparing against a zeroed buffer of the same expected length.
26
+ *
27
+ * @param rawBody The exact raw request body bytes received over the wire.
28
+ * Pass a `Buffer` or `Uint8Array` for binary-safe comparison;
29
+ * a `string` is accepted and UTF-8-encoded internally.
30
+ * @param signature Value of the `Viva-Signature-256` HTTP header.
31
+ * Must be a lowercase hex digest (64 hex chars for SHA-256).
32
+ * @param secret HMAC secret from Viva configuration.
33
+ *
34
+ * @throws VivaWebhookError when the signature is missing, malformed, or does
35
+ * not match the computed digest.
36
+ *
37
+ * @see references/viva-docs/md/wh-sale-transactions.txt:149
38
+ */
39
+ export function verifyHmacSignature(rawBody, signature, secret) {
40
+ // --- 1. Guard: signature header must be present and non-empty ---
41
+ if (signature == null || signature.length === 0) {
42
+ throw new VivaWebhookError({
43
+ message: 'Missing Viva-Signature-256 header. This header is required for event 7936 (Sale Transactions).',
44
+ });
45
+ }
46
+ // --- 2. Decode candidate hex string to bytes ---
47
+ // The header value is a hex digest per wh-sale-transactions.txt:149.
48
+ // A SHA-256 hex digest is always exactly 64 hex characters (32 bytes).
49
+ const expectedByteLength = 32; // SHA-256 output is 32 bytes
50
+ const hexRegex = /^[0-9a-f]+$/i;
51
+ if (!hexRegex.test(signature)) {
52
+ throw new VivaWebhookError({
53
+ message: `Malformed Viva-Signature-256 header: not a valid hex string. Received: "${signature.slice(0, 20)}..."`,
54
+ });
55
+ }
56
+ let candidateBuffer;
57
+ try {
58
+ candidateBuffer = Buffer.from(signature, 'hex');
59
+ }
60
+ catch {
61
+ throw new VivaWebhookError({
62
+ message: 'Malformed Viva-Signature-256 header: hex decode failed.',
63
+ });
64
+ }
65
+ // --- 3. Compute expected HMAC-SHA256 ---
66
+ const hmac = createHmac('sha256', secret);
67
+ hmac.update(rawBody);
68
+ const expectedBuffer = hmac.digest(); // returns a Buffer (32 bytes)
69
+ // --- 4. Constant-time comparison ---
70
+ // timingSafeEqual requires both buffers to have the same length.
71
+ // If candidateBuffer.length !== expectedByteLength, we still do a
72
+ // constant-time comparison against a zeroed buffer to avoid timing leaks,
73
+ // then unconditionally throw.
74
+ if (candidateBuffer.length !== expectedByteLength) {
75
+ // Compare candidate against a zero buffer of the expected length.
76
+ // This runs in constant time relative to expectedByteLength.
77
+ const zeros = Buffer.alloc(expectedByteLength, 0);
78
+ // We deliberately ignore the result here — length mismatch is always a fail.
79
+ void timingSafeEqual(zeros, expectedBuffer);
80
+ throw new VivaWebhookError({
81
+ message: `Viva-Signature-256 header length mismatch: expected ${expectedByteLength} bytes (${expectedByteLength * 2} hex chars), got ${candidateBuffer.length} bytes.`,
82
+ });
83
+ }
84
+ // Both buffers are 32 bytes — safe to call timingSafeEqual.
85
+ const valid = timingSafeEqual(expectedBuffer, candidateBuffer);
86
+ if (!valid) {
87
+ throw new VivaWebhookError({
88
+ message: 'Viva-Signature-256 signature mismatch. Request body may have been tampered with.',
89
+ });
90
+ }
91
+ }
92
+ //# sourceMappingURL=hmac-verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmac-verify.js","sourceRoot":"","sources":["../../src/webhooks/hmac-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAA4B,EAC5B,SAAoC,EACpC,MAAc;IAEd,mEAAmE;IACnE,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,gBAAgB,CAAC;YACzB,OAAO,EAAE,gGAAgG;SAC1G,CAAC,CAAC;IACL,CAAC;IAED,kDAAkD;IAClD,qEAAqE;IACrE,uEAAuE;IACvE,MAAM,kBAAkB,GAAG,EAAE,CAAC,CAAC,6BAA6B;IAC5D,MAAM,QAAQ,GAAG,cAAc,CAAC;IAEhC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,gBAAgB,CAAC;YACzB,OAAO,EAAE,2EAA2E,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM;SACjH,CAAC,CAAC;IACL,CAAC;IAED,IAAI,eAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,gBAAgB,CAAC;YACzB,OAAO,EAAE,yDAAyD;SACnE,CAAC,CAAC;IACL,CAAC;IAED,0CAA0C;IAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACrB,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,8BAA8B;IAEpE,sCAAsC;IACtC,iEAAiE;IACjE,kEAAkE;IAClE,0EAA0E;IAC1E,8BAA8B;IAC9B,IAAI,eAAe,CAAC,MAAM,KAAK,kBAAkB,EAAE,CAAC;QAClD,kEAAkE;QAClE,6DAA6D;QAC7D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC;QAClD,6EAA6E;QAC7E,KAAK,eAAe,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;QAC5C,MAAM,IAAI,gBAAgB,CAAC;YACzB,OAAO,EAAE,uDAAuD,kBAAkB,WAAW,kBAAkB,GAAG,CAAC,oBAAoB,eAAe,CAAC,MAAM,SAAS;SACvK,CAAC,CAAC;IACL,CAAC;IAED,4DAA4D;IAC5D,MAAM,KAAK,GAAG,eAAe,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAE/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,gBAAgB,CAAC;YACzB,OAAO,EAAE,kFAAkF;SAC5F,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * viva-payments-core/webhooks — barrel export.
3
+ *
4
+ * Provides three security layers for the Viva webhook endpoint:
5
+ * (a) IP allowlist — isAllowedSourceIp()
6
+ * (b) Challenge-response — buildChallengeResponse()
7
+ * (c) HMAC signature — verifyHmacSignature() (event 7936 only, per plan A8)
8
+ *
9
+ * Plus runtime helpers for event types and the monotonic status lattice.
10
+ *
11
+ * @see references/viva-docs/md/webhooks-for-payments.txt:254
12
+ */
13
+ export { buildChallengeResponse } from './challenge-response.js';
14
+ export { VIVA_DEMO_IPS, VIVA_PROD_IPS, isAllowedSourceIp, } from './ip-allowlist.js';
15
+ export { extractClientIp, type ClientIpRequest, } from './extract-client-ip.js';
16
+ export { verifyHmacSignature } from './hmac-verify.js';
17
+ export { EVENT_TYPES, type VivaEventTypeId, isTransactionEvent, isOnboardingEvent, V1_EVENT_TYPE_IDS, } from './event-types.js';
18
+ export { mapStatusLetter, validateStatusTransition, applyStatusTransition, type StatusTransitionResult, } from './status-lattice.js';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webhooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EACL,aAAa,EACb,aAAa,EACb,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,eAAe,EACf,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,KAAK,sBAAsB,GAC5B,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * viva-payments-core/webhooks — barrel export.
3
+ *
4
+ * Provides three security layers for the Viva webhook endpoint:
5
+ * (a) IP allowlist — isAllowedSourceIp()
6
+ * (b) Challenge-response — buildChallengeResponse()
7
+ * (c) HMAC signature — verifyHmacSignature() (event 7936 only, per plan A8)
8
+ *
9
+ * Plus runtime helpers for event types and the monotonic status lattice.
10
+ *
11
+ * @see references/viva-docs/md/webhooks-for-payments.txt:254
12
+ */
13
+ export { buildChallengeResponse } from './challenge-response.js';
14
+ export { VIVA_DEMO_IPS, VIVA_PROD_IPS, isAllowedSourceIp, } from './ip-allowlist.js';
15
+ export { extractClientIp, } from './extract-client-ip.js';
16
+ export { verifyHmacSignature } from './hmac-verify.js';
17
+ export { EVENT_TYPES, isTransactionEvent, isOnboardingEvent, V1_EVENT_TYPE_IDS, } from './event-types.js';
18
+ export { mapStatusLetter, validateStatusTransition, applyStatusTransition, } from './status-lattice.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webhooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EACL,aAAa,EACb,aAAa,EACb,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,eAAe,GAEhB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EACL,WAAW,EAEX,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,qBAAqB,GAEtB,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Viva Wallet webhook source IP allowlist.
3
+ *
4
+ * Viva documents the following source IP addresses/ranges for webhook POST
5
+ * requests. Only requests from these IPs should be processed; others are
6
+ * rejected with 403 by the route handler.
7
+ *
8
+ * Production IPs (literal + CIDR):
9
+ * 51.138.37.238
10
+ * 13.80.70.181
11
+ * 13.80.71.223
12
+ * 13.79.28.70
13
+ * 40.127.253.112/28
14
+ * 51.105.129.192/28
15
+ * 20.54.89.16
16
+ * 4.223.76.50
17
+ * 51.12.157.0/28
18
+ *
19
+ * Demo IPs (literal):
20
+ * 20.50.240.57
21
+ * 40.74.20.78
22
+ * 195.167.87.181
23
+ * 195.167.87.180
24
+ * 20.13.195.185
25
+ * 135.225.16.50
26
+ *
27
+ * @see references/viva-docs/md/webhooks-for-payments.txt:270
28
+ */
29
+ /**
30
+ * Viva demo-environment webhook source IPs.
31
+ * All are literal IPv4 addresses (no CIDR ranges in the demo list).
32
+ *
33
+ * @see references/viva-docs/md/webhooks-for-payments.txt:289
34
+ */
35
+ export declare const VIVA_DEMO_IPS: readonly string[];
36
+ /**
37
+ * Viva production-environment webhook source IPs (mix of literals and CIDRs).
38
+ *
39
+ * @see references/viva-docs/md/webhooks-for-payments.txt:276
40
+ */
41
+ export declare const VIVA_PROD_IPS: readonly string[];
42
+ /**
43
+ * Returns `true` if `ip` is in Viva's published source list for `env`,
44
+ * or if it appears in the optional `extraAllowlist`.
45
+ *
46
+ * Does NOT throw — the route handler decides whether to 403.
47
+ *
48
+ * IPv4 literals and CIDR ranges are matched via `node:net.BlockList`.
49
+ * IPv6 is normalised (lowercase + bracket-strip) before checking.
50
+ *
51
+ * @param ip The source IP of the incoming request.
52
+ * @param env Target environment: `'demo'` or `'production'`.
53
+ * @param extraAllowlist Optional operator-configured IPs (e.g. reverse proxies
54
+ * that sit upstream of Viva before reaching this server).
55
+ *
56
+ * @see references/viva-docs/md/webhooks-for-payments.txt:270
57
+ */
58
+ export declare function isAllowedSourceIp(ip: string, env: 'demo' | 'production', extraAllowlist?: readonly string[]): boolean;
59
+ //# sourceMappingURL=ip-allowlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ip-allowlist.d.ts","sourceRoot":"","sources":["../../src/webhooks/ip-allowlist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAQH;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAOjC,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAUjC,CAAC;AA2DX;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,MAAM,GAAG,YAAY,EAC1B,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GACjC,OAAO,CAmBT"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Viva Wallet webhook source IP allowlist.
3
+ *
4
+ * Viva documents the following source IP addresses/ranges for webhook POST
5
+ * requests. Only requests from these IPs should be processed; others are
6
+ * rejected with 403 by the route handler.
7
+ *
8
+ * Production IPs (literal + CIDR):
9
+ * 51.138.37.238
10
+ * 13.80.70.181
11
+ * 13.80.71.223
12
+ * 13.79.28.70
13
+ * 40.127.253.112/28
14
+ * 51.105.129.192/28
15
+ * 20.54.89.16
16
+ * 4.223.76.50
17
+ * 51.12.157.0/28
18
+ *
19
+ * Demo IPs (literal):
20
+ * 20.50.240.57
21
+ * 40.74.20.78
22
+ * 195.167.87.181
23
+ * 195.167.87.180
24
+ * 20.13.195.185
25
+ * 135.225.16.50
26
+ *
27
+ * @see references/viva-docs/md/webhooks-for-payments.txt:270
28
+ */
29
+ import { BlockList } from 'node:net';
30
+ // ---------------------------------------------------------------------------
31
+ // Published IP lists
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * Viva demo-environment webhook source IPs.
35
+ * All are literal IPv4 addresses (no CIDR ranges in the demo list).
36
+ *
37
+ * @see references/viva-docs/md/webhooks-for-payments.txt:289
38
+ */
39
+ export const VIVA_DEMO_IPS = [
40
+ '20.50.240.57',
41
+ '40.74.20.78',
42
+ '195.167.87.181',
43
+ '195.167.87.180',
44
+ '20.13.195.185',
45
+ '135.225.16.50',
46
+ ];
47
+ /**
48
+ * Viva production-environment webhook source IPs (mix of literals and CIDRs).
49
+ *
50
+ * @see references/viva-docs/md/webhooks-for-payments.txt:276
51
+ */
52
+ export const VIVA_PROD_IPS = [
53
+ '51.138.37.238',
54
+ '13.80.70.181',
55
+ '13.80.71.223',
56
+ '13.79.28.70',
57
+ '40.127.253.112/28',
58
+ '51.105.129.192/28',
59
+ '20.54.89.16',
60
+ '4.223.76.50',
61
+ '51.12.157.0/28',
62
+ ];
63
+ // ---------------------------------------------------------------------------
64
+ // BlockList builder
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Parse a single IP string or CIDR string and add it to a BlockList.
68
+ * Supports:
69
+ * - Literal IPv4: "1.2.3.4"
70
+ * - IPv4 CIDR: "1.2.3.0/24"
71
+ * - Literal IPv6: "::1"
72
+ */
73
+ function addToBlockList(bl, entry) {
74
+ const slash = entry.indexOf('/');
75
+ if (slash !== -1) {
76
+ const addr = entry.slice(0, slash);
77
+ const prefix = parseInt(entry.slice(slash + 1), 10);
78
+ // node:net BlockList.addSubnet(addr, prefix, type)
79
+ bl.addSubnet(addr, prefix, addr.includes(':') ? 'ipv6' : 'ipv4');
80
+ }
81
+ else {
82
+ bl.addAddress(entry, entry.includes(':') ? 'ipv6' : 'ipv4');
83
+ }
84
+ }
85
+ /** Build a BlockList from an array of IP/CIDR strings. */
86
+ function buildBlockList(ips) {
87
+ const bl = new BlockList();
88
+ for (const entry of ips) {
89
+ addToBlockList(bl, entry);
90
+ }
91
+ return bl;
92
+ }
93
+ // Cached BlockLists built once per process.
94
+ const _demoList = buildBlockList(VIVA_DEMO_IPS);
95
+ const _prodList = buildBlockList(VIVA_PROD_IPS);
96
+ // ---------------------------------------------------------------------------
97
+ // Normaliser
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * Normalise an IP string for comparison:
101
+ * - Lowercase
102
+ * - Strip surrounding brackets (e.g. `[::1]` → `::1`)
103
+ */
104
+ function normaliseIp(ip) {
105
+ let s = ip.trim().toLowerCase();
106
+ if (s.startsWith('[') && s.endsWith(']')) {
107
+ s = s.slice(1, -1);
108
+ }
109
+ return s;
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Public API
113
+ // ---------------------------------------------------------------------------
114
+ /**
115
+ * Returns `true` if `ip` is in Viva's published source list for `env`,
116
+ * or if it appears in the optional `extraAllowlist`.
117
+ *
118
+ * Does NOT throw — the route handler decides whether to 403.
119
+ *
120
+ * IPv4 literals and CIDR ranges are matched via `node:net.BlockList`.
121
+ * IPv6 is normalised (lowercase + bracket-strip) before checking.
122
+ *
123
+ * @param ip The source IP of the incoming request.
124
+ * @param env Target environment: `'demo'` or `'production'`.
125
+ * @param extraAllowlist Optional operator-configured IPs (e.g. reverse proxies
126
+ * that sit upstream of Viva before reaching this server).
127
+ *
128
+ * @see references/viva-docs/md/webhooks-for-payments.txt:270
129
+ */
130
+ export function isAllowedSourceIp(ip, env, extraAllowlist) {
131
+ const normalised = normaliseIp(ip);
132
+ const type = normalised.includes(':') ? 'ipv6' : 'ipv4';
133
+ // Check the env-specific block list.
134
+ const list = env === 'demo' ? _demoList : _prodList;
135
+ if (list.check(normalised, type)) {
136
+ return true;
137
+ }
138
+ // Check extra allowlist if provided.
139
+ if (extraAllowlist && extraAllowlist.length > 0) {
140
+ const extraList = buildBlockList(extraAllowlist);
141
+ if (extraList.check(normalised, type)) {
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+ //# sourceMappingURL=ip-allowlist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ip-allowlist.js","sourceRoot":"","sources":["../../src/webhooks/ip-allowlist.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Viva transaction status mapping and monotonic status lattice.
3
+ *
4
+ * Implements plan P17 (status letter → plugin enum) and plan A9
5
+ * (authorized → cancelled allowed — void-before-capture transition).
6
+ *
7
+ * Status lattice (per P17 + A9):
8
+ * initiated → authorized | captured | failed
9
+ * authorized → captured | cancelled | failed | disputed ← A9 added cancelled
10
+ * captured → refunded | disputed
11
+ * refunded → disputed
12
+ * failed, cancelled, disputed → TERMINAL (no further transitions)
13
+ *
14
+ * Terminal states never transition except to themselves (idempotent re-apply).
15
+ *
16
+ * @see references/viva-docs/md/wh-sale-transactions.txt:210
17
+ * @see references/viva-docs/md/wh-transaction-payment-created.txt:398
18
+ */
19
+ import type { VivaStatusLetter, VivaTransactionStatus, VivaClaimSubstate } from '../types/status.js';
20
+ /**
21
+ * Pure mapping from Viva's `StatusId` letter to the plugin's canonical status.
22
+ *
23
+ * M-family letters (M, MA, MI, ML, MS, MW) all map to `'disputed'` with the
24
+ * specific letter preserved as `claimSubstate`.
25
+ *
26
+ * @see references/viva-docs/md/wh-sale-transactions.txt:210
27
+ * @see references/viva-docs/md/wh-transaction-payment-created.txt:398
28
+ */
29
+ export declare function mapStatusLetter(letter: VivaStatusLetter): {
30
+ status: VivaTransactionStatus;
31
+ claimSubstate: VivaClaimSubstate | null;
32
+ };
33
+ export type StatusTransitionResult = {
34
+ ok: true;
35
+ next: VivaTransactionStatus;
36
+ } | {
37
+ ok: false;
38
+ reason: 'TERMINAL' | 'BACKWARD' | 'ILLEGAL';
39
+ current: VivaTransactionStatus;
40
+ attempted: VivaTransactionStatus;
41
+ };
42
+ /**
43
+ * Pure validator. Does NOT mutate state. Returns a typed result describing
44
+ * whether the `current → next` transition is allowed by the lattice.
45
+ *
46
+ * Returns:
47
+ * - `{ ok: true, next }` if the transition is allowed OR if `next === current`
48
+ * (idempotent re-apply).
49
+ * - `{ ok: false, reason: 'TERMINAL' }` if `current` is terminal and
50
+ * `next !== current`.
51
+ * - `{ ok: false, reason: 'BACKWARD' }` if `next` is an ancestor of `current`
52
+ * in the DAG.
53
+ * - `{ ok: false, reason: 'ILLEGAL' }` for any other invalid pair.
54
+ *
55
+ * Callers (the Medusa subscriber) use this before issuing an UPDATE to
56
+ * `viva_transaction.status`.
57
+ *
58
+ * @see Plan P17 (status lattice), Plan A9 (authorized → cancelled)
59
+ * @see references/viva-docs/md/wh-transaction-payment-created.txt:398
60
+ */
61
+ export declare function validateStatusTransition(current: VivaTransactionStatus, next: VivaTransactionStatus): StatusTransitionResult;
62
+ /**
63
+ * Convenience wrapper: looks up `mapStatusLetter` then calls
64
+ * `validateStatusTransition`. Returns the full result so callers can decide
65
+ * whether to apply or log.
66
+ *
67
+ * Not part of the core interface spec, provided for S8 convenience.
68
+ */
69
+ export declare function applyStatusTransition(current: VivaTransactionStatus, incomingLetter: VivaStatusLetter): StatusTransitionResult & {
70
+ claimSubstate: VivaClaimSubstate | null;
71
+ };
72
+ //# sourceMappingURL=status-lattice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-lattice.d.ts","sourceRoot":"","sources":["../../src/webhooks/status-lattice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAUrG;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG;IACzD,MAAM,EAAE,qBAAqB,CAAC;IAC9B,aAAa,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACzC,CA2BA;AAqHD,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,qBAAqB,CAAA;CAAE,GACzC;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,OAAO,EAAE,qBAAqB,CAAC;IAC/B,SAAS,EAAE,qBAAqB,CAAC;CAClC,CAAC;AAMN;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,IAAI,EAAE,qBAAqB,GAC1B,sBAAsB,CA0BxB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,qBAAqB,EAC9B,cAAc,EAAE,gBAAgB,GAC/B,sBAAsB,GAAG;IAAE,aAAa,EAAE,iBAAiB,GAAG,IAAI,CAAA;CAAE,CAItE"}