@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.
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/dist/auth/http.d.ts +44 -0
- package/dist/auth/http.d.ts.map +1 -0
- package/dist/auth/http.js +80 -0
- package/dist/auth/http.js.map +1 -0
- package/dist/auth/index.d.ts +19 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +18 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth2-strategy.d.ts +117 -0
- package/dist/auth/oauth2-strategy.d.ts.map +1 -0
- package/dist/auth/oauth2-strategy.js +217 -0
- package/dist/auth/oauth2-strategy.js.map +1 -0
- package/dist/auth/reseller-strategy.d.ts +65 -0
- package/dist/auth/reseller-strategy.d.ts.map +1 -0
- package/dist/auth/reseller-strategy.js +68 -0
- package/dist/auth/reseller-strategy.js.map +1 -0
- package/dist/auth/single-flight.d.ts +81 -0
- package/dist/auth/single-flight.d.ts.map +1 -0
- package/dist/auth/single-flight.js +160 -0
- package/dist/auth/single-flight.js.map +1 -0
- package/dist/auth/token-cache.d.ts +50 -0
- package/dist/auth/token-cache.d.ts.map +1 -0
- package/dist/auth/token-cache.js +59 -0
- package/dist/auth/token-cache.js.map +1 -0
- package/dist/errors/api-error.d.ts +15 -0
- package/dist/errors/api-error.d.ts.map +1 -0
- package/dist/errors/api-error.js +18 -0
- package/dist/errors/api-error.js.map +1 -0
- package/dist/errors/auth-error.d.ts +14 -0
- package/dist/errors/auth-error.d.ts.map +1 -0
- package/dist/errors/auth-error.js +17 -0
- package/dist/errors/auth-error.js.map +1 -0
- package/dist/errors/base.d.ts +59 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/base.js +51 -0
- package/dist/errors/base.js.map +1 -0
- package/dist/errors/index.d.ts +18 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +16 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/mode-mismatch-error.d.ts +19 -0
- package/dist/errors/mode-mismatch-error.d.ts.map +1 -0
- package/dist/errors/mode-mismatch-error.js +22 -0
- package/dist/errors/mode-mismatch-error.js.map +1 -0
- package/dist/errors/rate-limit-error.d.ts +20 -0
- package/dist/errors/rate-limit-error.d.ts.map +1 -0
- package/dist/errors/rate-limit-error.js +20 -0
- package/dist/errors/rate-limit-error.js.map +1 -0
- package/dist/errors/validation-error.d.ts +14 -0
- package/dist/errors/validation-error.d.ts.map +1 -0
- package/dist/errors/validation-error.js +17 -0
- package/dist/errors/validation-error.js.map +1 -0
- package/dist/errors/webhook-error.d.ts +14 -0
- package/dist/errors/webhook-error.d.ts.map +1 -0
- package/dist/errors/webhook-error.js +17 -0
- package/dist/errors/webhook-error.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/isv/accounts.d.ts +38 -0
- package/dist/isv/accounts.d.ts.map +1 -0
- package/dist/isv/accounts.js +60 -0
- package/dist/isv/accounts.js.map +1 -0
- package/dist/isv/client.d.ts +187 -0
- package/dist/isv/client.d.ts.map +1 -0
- package/dist/isv/client.js +465 -0
- package/dist/isv/client.js.map +1 -0
- package/dist/isv/index.d.ts +52 -0
- package/dist/isv/index.d.ts.map +1 -0
- package/dist/isv/index.js +53 -0
- package/dist/isv/index.js.map +1 -0
- package/dist/isv/legacy-basic-client.d.ts +122 -0
- package/dist/isv/legacy-basic-client.d.ts.map +1 -0
- package/dist/isv/legacy-basic-client.js +281 -0
- package/dist/isv/legacy-basic-client.js.map +1 -0
- package/dist/isv/payments.d.ts +199 -0
- package/dist/isv/payments.d.ts.map +1 -0
- package/dist/isv/payments.js +385 -0
- package/dist/isv/payments.js.map +1 -0
- package/dist/isv/sources.d.ts +80 -0
- package/dist/isv/sources.d.ts.map +1 -0
- package/dist/isv/sources.js +112 -0
- package/dist/isv/sources.js.map +1 -0
- package/dist/isv/webhooks-api.d.ts +48 -0
- package/dist/isv/webhooks-api.d.ts.map +1 -0
- package/dist/isv/webhooks-api.js +66 -0
- package/dist/isv/webhooks-api.js.map +1 -0
- package/dist/legacy/client.d.ts +199 -0
- package/dist/legacy/client.d.ts.map +1 -0
- package/dist/legacy/client.js +351 -0
- package/dist/legacy/client.js.map +1 -0
- package/dist/legacy/index.d.ts +15 -0
- package/dist/legacy/index.d.ts.map +1 -0
- package/dist/legacy/index.js +14 -0
- package/dist/legacy/index.js.map +1 -0
- package/dist/observability/context.d.ts +30 -0
- package/dist/observability/context.d.ts.map +1 -0
- package/dist/observability/context.js +40 -0
- package/dist/observability/context.js.map +1 -0
- package/dist/observability/index.d.ts +15 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +11 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/logger.d.ts +81 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +127 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +37 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +40 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/redact.d.ts +21 -0
- package/dist/observability/redact.d.ts.map +1 -0
- package/dist/observability/redact.js +72 -0
- package/dist/observability/redact.js.map +1 -0
- package/dist/observability/tracer.d.ts +25 -0
- package/dist/observability/tracer.d.ts.map +1 -0
- package/dist/observability/tracer.js +18 -0
- package/dist/observability/tracer.js.map +1 -0
- package/dist/payments/client.d.ts +247 -0
- package/dist/payments/client.d.ts.map +1 -0
- package/dist/payments/client.js +488 -0
- package/dist/payments/client.js.map +1 -0
- package/dist/payments/index.d.ts +14 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +13 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/refunds/fast-refund-client.d.ts +128 -0
- package/dist/refunds/fast-refund-client.d.ts.map +1 -0
- package/dist/refunds/fast-refund-client.js +138 -0
- package/dist/refunds/fast-refund-client.js.map +1 -0
- package/dist/refunds/index.d.ts +19 -0
- package/dist/refunds/index.d.ts.map +1 -0
- package/dist/refunds/index.js +17 -0
- package/dist/refunds/index.js.map +1 -0
- package/dist/refunds/strategy.d.ts +78 -0
- package/dist/refunds/strategy.d.ts.map +1 -0
- package/dist/refunds/strategy.js +75 -0
- package/dist/refunds/strategy.js.map +1 -0
- package/dist/types/auth.d.ts +80 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +12 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/card-types.d.ts +48 -0
- package/dist/types/card-types.d.ts.map +1 -0
- package/dist/types/card-types.js +62 -0
- package/dist/types/card-types.js.map +1 -0
- package/dist/types/common.d.ts +160 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +70 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +21 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/isv-accounts.d.ts +109 -0
- package/dist/types/isv-accounts.d.ts.map +1 -0
- package/dist/types/isv-accounts.js +22 -0
- package/dist/types/isv-accounts.js.map +1 -0
- package/dist/types/isv-payments.d.ts +262 -0
- package/dist/types/isv-payments.d.ts.map +1 -0
- package/dist/types/isv-payments.js +19 -0
- package/dist/types/isv-payments.js.map +1 -0
- package/dist/types/status.d.ts +125 -0
- package/dist/types/status.d.ts.map +1 -0
- package/dist/types/status.js +19 -0
- package/dist/types/status.js.map +1 -0
- package/dist/types/webhook-events.d.ts +447 -0
- package/dist/types/webhook-events.d.ts.map +1 -0
- package/dist/types/webhook-events.js +76 -0
- package/dist/types/webhook-events.js.map +1 -0
- package/dist/webhooks/challenge-response.d.ts +28 -0
- package/dist/webhooks/challenge-response.d.ts.map +1 -0
- package/dist/webhooks/challenge-response.js +35 -0
- package/dist/webhooks/challenge-response.js.map +1 -0
- package/dist/webhooks/event-types.d.ts +44 -0
- package/dist/webhooks/event-types.d.ts.map +1 -0
- package/dist/webhooks/event-types.js +50 -0
- package/dist/webhooks/event-types.js.map +1 -0
- package/dist/webhooks/extract-client-ip.d.ts +40 -0
- package/dist/webhooks/extract-client-ip.d.ts.map +1 -0
- package/dist/webhooks/extract-client-ip.js +72 -0
- package/dist/webhooks/extract-client-ip.js.map +1 -0
- package/dist/webhooks/hmac-verify.d.ts +38 -0
- package/dist/webhooks/hmac-verify.d.ts.map +1 -0
- package/dist/webhooks/hmac-verify.js +92 -0
- package/dist/webhooks/hmac-verify.js.map +1 -0
- package/dist/webhooks/index.d.ts +19 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +19 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/ip-allowlist.d.ts +59 -0
- package/dist/webhooks/ip-allowlist.d.ts.map +1 -0
- package/dist/webhooks/ip-allowlist.js +147 -0
- package/dist/webhooks/ip-allowlist.js.map +1 -0
- package/dist/webhooks/status-lattice.d.ts +72 -0
- package/dist/webhooks/status-lattice.d.ts.map +1 -0
- package/dist/webhooks/status-lattice.js +208 -0
- package/dist/webhooks/status-lattice.js.map +1 -0
- 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"}
|