@sakeetech/medusa-payment-viva 0.2.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/LICENSE +21 -0
- package/README.md +816 -0
- package/dist/api/index.d.ts +15 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +22 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/middlewares.d.ts +27 -0
- package/dist/api/middlewares.d.ts.map +1 -0
- package/dist/api/middlewares.js +62 -0
- package/dist/api/middlewares.js.map +1 -0
- package/dist/api/viva/admin/_admin-auth.d.ts +26 -0
- package/dist/api/viva/admin/_admin-auth.d.ts.map +1 -0
- package/dist/api/viva/admin/_admin-auth.js +49 -0
- package/dist/api/viva/admin/_admin-auth.js.map +1 -0
- package/dist/api/viva/admin/_mode-gate.d.ts +28 -0
- package/dist/api/viva/admin/_mode-gate.d.ts.map +1 -0
- package/dist/api/viva/admin/_mode-gate.js +45 -0
- package/dist/api/viva/admin/_mode-gate.js.map +1 -0
- package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.d.ts +21 -0
- package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.d.ts.map +1 -0
- package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.js +93 -0
- package/dist/api/viva/admin/connected-accounts/[id]/reconcile/route.js.map +1 -0
- package/dist/api/viva/admin/connected-accounts/[id]/route.d.ts +18 -0
- package/dist/api/viva/admin/connected-accounts/[id]/route.d.ts.map +1 -0
- package/dist/api/viva/admin/connected-accounts/[id]/route.js +59 -0
- package/dist/api/viva/admin/connected-accounts/[id]/route.js.map +1 -0
- package/dist/api/viva/admin/connected-accounts/[id]/sources/route.d.ts +34 -0
- package/dist/api/viva/admin/connected-accounts/[id]/sources/route.d.ts.map +1 -0
- package/dist/api/viva/admin/connected-accounts/[id]/sources/route.js +234 -0
- package/dist/api/viva/admin/connected-accounts/[id]/sources/route.js.map +1 -0
- package/dist/api/viva/admin/connected-accounts/route.d.ts +19 -0
- package/dist/api/viva/admin/connected-accounts/route.d.ts.map +1 -0
- package/dist/api/viva/admin/connected-accounts/route.js +78 -0
- package/dist/api/viva/admin/connected-accounts/route.js.map +1 -0
- package/dist/api/viva/internal/auth-status/route.d.ts +19 -0
- package/dist/api/viva/internal/auth-status/route.d.ts.map +1 -0
- package/dist/api/viva/internal/auth-status/route.js +91 -0
- package/dist/api/viva/internal/auth-status/route.js.map +1 -0
- package/dist/api/viva/internal/metrics/route.d.ts +13 -0
- package/dist/api/viva/internal/metrics/route.d.ts.map +1 -0
- package/dist/api/viva/internal/metrics/route.js +48 -0
- package/dist/api/viva/internal/metrics/route.js.map +1 -0
- package/dist/api/viva/webhook/health/route.d.ts +16 -0
- package/dist/api/viva/webhook/health/route.d.ts.map +1 -0
- package/dist/api/viva/webhook/health/route.js +27 -0
- package/dist/api/viva/webhook/health/route.js.map +1 -0
- package/dist/api/viva/webhook/route.d.ts +57 -0
- package/dist/api/viva/webhook/route.d.ts.map +1 -0
- package/dist/api/viva/webhook/route.js +269 -0
- package/dist/api/viva/webhook/route.js.map +1 -0
- package/dist/cli/bin.d.ts +12 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +78 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/plan.d.ts +51 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +128 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/register-webhooks.d.ts +54 -0
- package/dist/cli/register-webhooks.d.ts.map +1 -0
- package/dist/cli/register-webhooks.js +366 -0
- package/dist/cli/register-webhooks.js.map +1 -0
- package/dist/cli/types.d.ts +62 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +12 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/config.d.ts +158 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +236 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders/viva-oauth2-strategy.d.ts +26 -0
- package/dist/loaders/viva-oauth2-strategy.d.ts.map +1 -0
- package/dist/loaders/viva-oauth2-strategy.js +58 -0
- package/dist/loaders/viva-oauth2-strategy.js.map +1 -0
- package/dist/migrations/Migration_20260425000001_init_viva_payments.d.ts +19 -0
- package/dist/migrations/Migration_20260425000001_init_viva_payments.d.ts.map +1 -0
- package/dist/migrations/Migration_20260425000001_init_viva_payments.js +136 -0
- package/dist/migrations/Migration_20260425000001_init_viva_payments.js.map +1 -0
- package/dist/migrations/Migration_20260425000002_allow_null_order_code.d.ts +31 -0
- package/dist/migrations/Migration_20260425000002_allow_null_order_code.d.ts.map +1 -0
- package/dist/migrations/Migration_20260425000002_allow_null_order_code.js +71 -0
- package/dist/migrations/Migration_20260425000002_allow_null_order_code.js.map +1 -0
- package/dist/migrations/Migration_20260425000003_webhook_retry_count.d.ts +18 -0
- package/dist/migrations/Migration_20260425000003_webhook_retry_count.d.ts.map +1 -0
- package/dist/migrations/Migration_20260425000003_webhook_retry_count.js +42 -0
- package/dist/migrations/Migration_20260425000003_webhook_retry_count.js.map +1 -0
- package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.d.ts +29 -0
- package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.d.ts.map +1 -0
- package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.js +74 -0
- package/dist/migrations/Migration_20260425000004_webhook_error_and_nullable_merchant.js.map +1 -0
- package/dist/models/index.d.ts +7 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +10 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/viva-tenant-merchant.d.ts +11 -0
- package/dist/models/viva-tenant-merchant.d.ts.map +1 -0
- package/dist/models/viva-tenant-merchant.js +54 -0
- package/dist/models/viva-tenant-merchant.js.map +1 -0
- package/dist/models/viva-transaction.d.ts +34 -0
- package/dist/models/viva-transaction.d.ts.map +1 -0
- package/dist/models/viva-transaction.js +104 -0
- package/dist/models/viva-transaction.js.map +1 -0
- package/dist/models/viva-webhook-event.d.ts +32 -0
- package/dist/models/viva-webhook-event.d.ts.map +1 -0
- package/dist/models/viva-webhook-event.js +88 -0
- package/dist/models/viva-webhook-event.js.map +1 -0
- package/dist/observability/config.d.ts +34 -0
- package/dist/observability/config.d.ts.map +1 -0
- package/dist/observability/config.js +57 -0
- package/dist/observability/config.js.map +1 -0
- package/dist/observability/index.d.ts +8 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +15 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/prom-metrics.d.ts +41 -0
- package/dist/observability/prom-metrics.d.ts.map +1 -0
- package/dist/observability/prom-metrics.js +219 -0
- package/dist/observability/prom-metrics.js.map +1 -0
- package/dist/providers/payment-provider.d.ts +19 -0
- package/dist/providers/payment-provider.d.ts.map +1 -0
- package/dist/providers/payment-provider.js +24 -0
- package/dist/providers/payment-provider.js.map +1 -0
- package/dist/resolvers/auth-strategy-factory.d.ts +42 -0
- package/dist/resolvers/auth-strategy-factory.d.ts.map +1 -0
- package/dist/resolvers/auth-strategy-factory.js +60 -0
- package/dist/resolvers/auth-strategy-factory.js.map +1 -0
- package/dist/resolvers/tenant-resolver.d.ts +104 -0
- package/dist/resolvers/tenant-resolver.d.ts.map +1 -0
- package/dist/resolvers/tenant-resolver.js +118 -0
- package/dist/resolvers/tenant-resolver.js.map +1 -0
- package/dist/service.d.ts +200 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +1003 -0
- package/dist/service.js.map +1 -0
- package/dist/subscribers/index.d.ts +5 -0
- package/dist/subscribers/index.d.ts.map +1 -0
- package/dist/subscribers/index.js +10 -0
- package/dist/subscribers/index.js.map +1 -0
- package/dist/subscribers/viva-webhook-event.d.ts +38 -0
- package/dist/subscribers/viva-webhook-event.d.ts.map +1 -0
- package/dist/subscribers/viva-webhook-event.js +133 -0
- package/dist/subscribers/viva-webhook-event.js.map +1 -0
- package/dist/workflows/cleanup-old-webhook-events.d.ts +39 -0
- package/dist/workflows/cleanup-old-webhook-events.d.ts.map +1 -0
- package/dist/workflows/cleanup-old-webhook-events.js +68 -0
- package/dist/workflows/cleanup-old-webhook-events.js.map +1 -0
- package/dist/workflows/index.d.ts +14 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +19 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/per-tenant-semaphore.d.ts +47 -0
- package/dist/workflows/per-tenant-semaphore.d.ts.map +1 -0
- package/dist/workflows/per-tenant-semaphore.js +89 -0
- package/dist/workflows/per-tenant-semaphore.js.map +1 -0
- package/dist/workflows/process-webhook-event.d.ts +80 -0
- package/dist/workflows/process-webhook-event.d.ts.map +1 -0
- package/dist/workflows/process-webhook-event.js +280 -0
- package/dist/workflows/process-webhook-event.js.map +1 -0
- package/dist/workflows/reprocess-unresolved-tenants.d.ts +58 -0
- package/dist/workflows/reprocess-unresolved-tenants.d.ts.map +1 -0
- package/dist/workflows/reprocess-unresolved-tenants.js +121 -0
- package/dist/workflows/reprocess-unresolved-tenants.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* route.ts — Viva Wallet webhook endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Two handlers:
|
|
5
|
+
* GET /viva/webhook — challenge-response for URL registration.
|
|
6
|
+
* POST /viva/webhook — event ingest: IP check, INSERT + emit.
|
|
7
|
+
*
|
|
8
|
+
* Security model (plan P7):
|
|
9
|
+
* (a) IP allowlist — 403 on mismatch. Source IP is extracted with
|
|
10
|
+
* trustedProxyDepth-bounded X-Forwarded-For walking
|
|
11
|
+
* (CSO Finding #2). Configure via
|
|
12
|
+
* VIVA_TRUSTED_PROXY_DEPTH (default 0 = socket only).
|
|
13
|
+
* (b) DB dedup — INSERT ... ON CONFLICT (message_id) DO NOTHING.
|
|
14
|
+
* (c) A2 gate — only emit 'viva.webhook.received' when RETURNING non-empty.
|
|
15
|
+
*
|
|
16
|
+
* HMAC verification is intentionally not performed here. Event 7936 (the
|
|
17
|
+
* only HMAC-signed Viva event) is not subscribed; if it is ever added,
|
|
18
|
+
* route a separate VIVA_WEBHOOK_HMAC_SECRET, NOT the public
|
|
19
|
+
* VIVA_WEBHOOK_VERIFICATION_KEY. See CSO Finding #3.
|
|
20
|
+
*
|
|
21
|
+
* Metrics emitted (P16):
|
|
22
|
+
* viva_webhook_received_total{event_type_id, result}
|
|
23
|
+
* viva_webhook_processing_lag_seconds
|
|
24
|
+
* viva_tenant_resolution_failures_total
|
|
25
|
+
* viva_webhook_ordercode_mismatch_total
|
|
26
|
+
*
|
|
27
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:270 (IP allowlist)
|
|
28
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:254 (security model P7)
|
|
29
|
+
* @see docs/TODO-CSO.md (Findings 1, 2, 3)
|
|
30
|
+
*/
|
|
31
|
+
import type { MedusaRequest, MedusaResponse } from '@medusajs/framework/http';
|
|
32
|
+
/**
|
|
33
|
+
* Challenge-response for webhook URL registration.
|
|
34
|
+
* Returns `{"Key": "<webhook_verification_key>"}` per Viva protocol.
|
|
35
|
+
* No auth gate; only exercised at deployment time.
|
|
36
|
+
*
|
|
37
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:284 (challenge response)
|
|
38
|
+
*/
|
|
39
|
+
export declare const GET: (_req: MedusaRequest, res: MedusaResponse) => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* POST event ingest handler.
|
|
42
|
+
*
|
|
43
|
+
* Steps:
|
|
44
|
+
* 1. IP allowlist check (env-configured). 403 on rejection.
|
|
45
|
+
* 2. Read rawBody captured by vivaWebhookRawBodyMiddleware.
|
|
46
|
+
* 3. Parse JSON.
|
|
47
|
+
* 4. If EventTypeId 7936: verify HMAC. 401 on mismatch.
|
|
48
|
+
* 5. Resolve tenant via viva_tenant_merchant.
|
|
49
|
+
* 6. INSERT INTO viva_webhook_event ... ON CONFLICT (message_id) DO NOTHING RETURNING viva_webhook_event_id.
|
|
50
|
+
* 7. A2: only emit 'viva.webhook.received' when RETURNING produced a row.
|
|
51
|
+
* 8. Always respond 200.
|
|
52
|
+
*
|
|
53
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:286 (POST flow)
|
|
54
|
+
* @see references/viva-docs/md/wh-transaction-payment-created.txt:158 (event envelope)
|
|
55
|
+
*/
|
|
56
|
+
export declare const POST: (req: MedusaRequest, res: MedusaResponse) => Promise<void>;
|
|
57
|
+
//# sourceMappingURL=route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../src/api/viva/webhook/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAiB9E;;;;;;GAMG;AACH,eAAO,MAAM,GAAG,GACd,MAAM,aAAa,EACnB,KAAK,cAAc,KAClB,OAAO,CAAC,IAAI,CAQd,CAAC;AAMF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,IAAI,GACf,KAAK,aAAa,EAClB,KAAK,cAAc,KAClB,OAAO,CAAC,IAAI,CAwMd,CAAC"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* route.ts — Viva Wallet webhook endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Two handlers:
|
|
6
|
+
* GET /viva/webhook — challenge-response for URL registration.
|
|
7
|
+
* POST /viva/webhook — event ingest: IP check, INSERT + emit.
|
|
8
|
+
*
|
|
9
|
+
* Security model (plan P7):
|
|
10
|
+
* (a) IP allowlist — 403 on mismatch. Source IP is extracted with
|
|
11
|
+
* trustedProxyDepth-bounded X-Forwarded-For walking
|
|
12
|
+
* (CSO Finding #2). Configure via
|
|
13
|
+
* VIVA_TRUSTED_PROXY_DEPTH (default 0 = socket only).
|
|
14
|
+
* (b) DB dedup — INSERT ... ON CONFLICT (message_id) DO NOTHING.
|
|
15
|
+
* (c) A2 gate — only emit 'viva.webhook.received' when RETURNING non-empty.
|
|
16
|
+
*
|
|
17
|
+
* HMAC verification is intentionally not performed here. Event 7936 (the
|
|
18
|
+
* only HMAC-signed Viva event) is not subscribed; if it is ever added,
|
|
19
|
+
* route a separate VIVA_WEBHOOK_HMAC_SECRET, NOT the public
|
|
20
|
+
* VIVA_WEBHOOK_VERIFICATION_KEY. See CSO Finding #3.
|
|
21
|
+
*
|
|
22
|
+
* Metrics emitted (P16):
|
|
23
|
+
* viva_webhook_received_total{event_type_id, result}
|
|
24
|
+
* viva_webhook_processing_lag_seconds
|
|
25
|
+
* viva_tenant_resolution_failures_total
|
|
26
|
+
* viva_webhook_ordercode_mismatch_total
|
|
27
|
+
*
|
|
28
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:270 (IP allowlist)
|
|
29
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:254 (security model P7)
|
|
30
|
+
* @see docs/TODO-CSO.md (Findings 1, 2, 3)
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.POST = exports.GET = void 0;
|
|
37
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
38
|
+
const pg_1 = __importDefault(require("pg"));
|
|
39
|
+
const webhooks_1 = require("@sakeetech/viva-payments-core/webhooks");
|
|
40
|
+
const index_js_1 = require("../../../observability/index.js");
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// GET — challenge-response
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* Challenge-response for webhook URL registration.
|
|
46
|
+
* Returns `{"Key": "<webhook_verification_key>"}` per Viva protocol.
|
|
47
|
+
* No auth gate; only exercised at deployment time.
|
|
48
|
+
*
|
|
49
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:284 (challenge response)
|
|
50
|
+
*/
|
|
51
|
+
const GET = async (_req, res) => {
|
|
52
|
+
const key = process.env['VIVA_WEBHOOK_VERIFICATION_KEY'] ?? '';
|
|
53
|
+
if (!key) {
|
|
54
|
+
res.status(500).json({ error: 'VIVA_WEBHOOK_VERIFICATION_KEY is not configured' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
58
|
+
res.status(200).json({ Key: key });
|
|
59
|
+
};
|
|
60
|
+
exports.GET = GET;
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// POST — event ingest
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/**
|
|
65
|
+
* POST event ingest handler.
|
|
66
|
+
*
|
|
67
|
+
* Steps:
|
|
68
|
+
* 1. IP allowlist check (env-configured). 403 on rejection.
|
|
69
|
+
* 2. Read rawBody captured by vivaWebhookRawBodyMiddleware.
|
|
70
|
+
* 3. Parse JSON.
|
|
71
|
+
* 4. If EventTypeId 7936: verify HMAC. 401 on mismatch.
|
|
72
|
+
* 5. Resolve tenant via viva_tenant_merchant.
|
|
73
|
+
* 6. INSERT INTO viva_webhook_event ... ON CONFLICT (message_id) DO NOTHING RETURNING viva_webhook_event_id.
|
|
74
|
+
* 7. A2: only emit 'viva.webhook.received' when RETURNING produced a row.
|
|
75
|
+
* 8. Always respond 200.
|
|
76
|
+
*
|
|
77
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:286 (POST flow)
|
|
78
|
+
* @see references/viva-docs/md/wh-transaction-payment-created.txt:158 (event envelope)
|
|
79
|
+
*/
|
|
80
|
+
const POST = async (req, res) => {
|
|
81
|
+
const environment = (process.env['VIVA_ENVIRONMENT'] ?? 'demo');
|
|
82
|
+
const extraAllowlist = parseExtraAllowlist(process.env['VIVA_WEBHOOK_IP_ALLOWLIST']);
|
|
83
|
+
const trustedProxyDepth = parseTrustedProxyDepth(process.env['VIVA_TRUSTED_PROXY_DEPTH']);
|
|
84
|
+
const metrics = (0, index_js_1.getSharedMetrics)();
|
|
85
|
+
// ---- Step 1: IP allowlist (CSO Finding #2) ----
|
|
86
|
+
// VIVA_TRUSTED_PROXY_DEPTH controls how many trailing X-Forwarded-For hops we
|
|
87
|
+
// trust. Default 0 = socket only (safe for direct exposure); set to 1 for a
|
|
88
|
+
// single reverse proxy, 2 for CDN+LB. Walking from the rightmost end means
|
|
89
|
+
// attacker-injected leftmost values are ignored.
|
|
90
|
+
const clientIp = (0, webhooks_1.extractClientIp)(req, trustedProxyDepth);
|
|
91
|
+
if (!(0, webhooks_1.isAllowedSourceIp)(clientIp, environment, extraAllowlist)) {
|
|
92
|
+
// Log metric for IP rejection but only 403 in non-dev environments.
|
|
93
|
+
// In dev/test with IP_ALLOWLIST_BYPASS=true, allow through.
|
|
94
|
+
if (process.env['VIVA_WEBHOOK_IP_ALLOWLIST_BYPASS'] !== 'true') {
|
|
95
|
+
req.scope?.resolve?.('logger')?.warn?.(`[viva] Rejected webhook from disallowed IP ${clientIp} (env=${environment})`);
|
|
96
|
+
metrics.counter('viva_webhook_received_total', { event_type_id: 'unknown', result: 'ip_rejected' });
|
|
97
|
+
res.status(403).json({ error: 'Forbidden' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ---- Step 2: Raw body ----
|
|
102
|
+
const rawBody = req.rawBody;
|
|
103
|
+
if (!rawBody || rawBody.length === 0) {
|
|
104
|
+
res.status(200).end();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// ---- Step 3: Parse JSON ----
|
|
108
|
+
let envelope;
|
|
109
|
+
try {
|
|
110
|
+
envelope = JSON.parse(rawBody.toString('utf-8'));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Malformed JSON — return 200 per plan (Viva retries are not helpful here)
|
|
114
|
+
const logger = req.scope?.resolve?.('logger');
|
|
115
|
+
logger?.error?.('[viva] Webhook POST: failed to parse JSON body');
|
|
116
|
+
res.status(200).end();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const eventTypeId = envelope.EventTypeId;
|
|
120
|
+
const messageId = envelope.MessageId;
|
|
121
|
+
// ---- Compute processing lag (P16) ----
|
|
122
|
+
// envelope.Created is an ISO8601 timestamp when present
|
|
123
|
+
const createdTs = envelope['Created'];
|
|
124
|
+
if (typeof createdTs === 'string') {
|
|
125
|
+
const createdMs = new Date(createdTs).getTime();
|
|
126
|
+
if (!isNaN(createdMs)) {
|
|
127
|
+
const lagSeconds = (Date.now() - createdMs) / 1000;
|
|
128
|
+
metrics.histogram('viva_webhook_processing_lag_seconds', lagSeconds);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ---- Step 4: HMAC verification ----
|
|
132
|
+
// Plugin does NOT subscribe to event 7936 (Sale Transactions) — the only
|
|
133
|
+
// Viva event that ships an HMAC signature. The handler used to verify 7936
|
|
134
|
+
// signatures here, but it reused VIVA_WEBHOOK_VERIFICATION_KEY (a public,
|
|
135
|
+
// browser-fetchable value) as the HMAC secret, which is incorrect by
|
|
136
|
+
// Viva's protocol design (CSO Finding #3, dormant).
|
|
137
|
+
//
|
|
138
|
+
// If 7936 is ever added to the subscribed event list, route a SEPARATE
|
|
139
|
+
// VIVA_WEBHOOK_HMAC_SECRET env var, NOT VIVA_WEBHOOK_VERIFICATION_KEY.
|
|
140
|
+
// The `verifyHmacSignature` helper remains in
|
|
141
|
+
// @sakeetech/viva-payments-core/webhooks for that future wiring.
|
|
142
|
+
// @see docs/TODO-CSO.md "Finding 3"
|
|
143
|
+
// @see references/viva-docs/md/wh-sale-transactions.txt:174
|
|
144
|
+
// ---- Step 5: Resolve tenant ----
|
|
145
|
+
const connString = process.env['DATABASE_URL'] ??
|
|
146
|
+
`postgresql://${process.env['USER'] ?? 'postgres'}@localhost:5432/postgres`;
|
|
147
|
+
const pool = new pg_1.default.Pool({ connectionString: connString, max: 1 });
|
|
148
|
+
const logger = req.scope?.resolve?.('logger');
|
|
149
|
+
let tenantId = null;
|
|
150
|
+
let vivaMerchantId = null;
|
|
151
|
+
let connectedAccountId = null;
|
|
152
|
+
let transactionId = null;
|
|
153
|
+
try {
|
|
154
|
+
const eventData = envelope.EventData;
|
|
155
|
+
if ((0, webhooks_1.isTransactionEvent)(eventTypeId)) {
|
|
156
|
+
const txData = eventData;
|
|
157
|
+
vivaMerchantId = txData.MerchantId ?? null;
|
|
158
|
+
transactionId = txData.TransactionId ?? null;
|
|
159
|
+
connectedAccountId = txData.ConnectedAccountId ?? null;
|
|
160
|
+
if (vivaMerchantId) {
|
|
161
|
+
const client = await pool.connect();
|
|
162
|
+
try {
|
|
163
|
+
const row = await client.query(`SELECT tenant_id FROM viva_tenant_merchant WHERE viva_merchant_id = $1 LIMIT 1`, [vivaMerchantId]);
|
|
164
|
+
tenantId = row.rows[0]?.tenant_id ?? null;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
client.release();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if ((0, webhooks_1.isOnboardingEvent)(eventTypeId)) {
|
|
172
|
+
const onbData = eventData;
|
|
173
|
+
connectedAccountId = onbData.ConnectedAccountId ?? null;
|
|
174
|
+
if (connectedAccountId) {
|
|
175
|
+
const client = await pool.connect();
|
|
176
|
+
try {
|
|
177
|
+
const row = await client.query(`SELECT tenant_id FROM viva_tenant_merchant WHERE connected_account_id = $1 LIMIT 1`, [connectedAccountId]);
|
|
178
|
+
tenantId = row.rows[0]?.tenant_id ?? null;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
client.release();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!tenantId) {
|
|
186
|
+
// A6: log metric but continue — INSERT with tenant_id=NULL
|
|
187
|
+
logger?.warn?.(`[viva] Tenant resolution failed for event ${eventTypeId} message ${messageId} ` +
|
|
188
|
+
`vivaMerchantId=${vivaMerchantId ?? 'null'} connectedAccountId=${connectedAccountId ?? 'null'}. ` +
|
|
189
|
+
`Metric: viva_tenant_resolution_failures_total`);
|
|
190
|
+
metrics.counter('viva_tenant_resolution_failures_total', { event_type_id: String(eventTypeId) });
|
|
191
|
+
}
|
|
192
|
+
// ---- Step 6: INSERT ... ON CONFLICT DO NOTHING RETURNING ----
|
|
193
|
+
const client = await pool.connect();
|
|
194
|
+
let insertedEventId = null;
|
|
195
|
+
try {
|
|
196
|
+
const result = await client.query(`INSERT INTO viva_webhook_event
|
|
197
|
+
(transaction_id, event_type_id, message_id, viva_merchant_id, connected_account_id, raw_payload)
|
|
198
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
199
|
+
ON CONFLICT (message_id) DO NOTHING
|
|
200
|
+
RETURNING viva_webhook_event_id`, [
|
|
201
|
+
transactionId,
|
|
202
|
+
eventTypeId,
|
|
203
|
+
messageId,
|
|
204
|
+
vivaMerchantId,
|
|
205
|
+
connectedAccountId,
|
|
206
|
+
JSON.stringify(envelope),
|
|
207
|
+
]);
|
|
208
|
+
insertedEventId = result.rows[0]?.viva_webhook_event_id ?? null;
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
client.release();
|
|
212
|
+
}
|
|
213
|
+
// ---- Step 7: A2 dispatch gate — only emit when RETURNING returned a row ----
|
|
214
|
+
if (insertedEventId) {
|
|
215
|
+
metrics.counter('viva_webhook_received_total', {
|
|
216
|
+
event_type_id: String(eventTypeId),
|
|
217
|
+
result: tenantId ? 'accepted' : 'tenant_unresolved',
|
|
218
|
+
});
|
|
219
|
+
const eventBus = req.scope?.resolve?.(utils_1.Modules.EVENT_BUS);
|
|
220
|
+
if (eventBus) {
|
|
221
|
+
await eventBus.emit({
|
|
222
|
+
name: 'viva.webhook.received',
|
|
223
|
+
data: {
|
|
224
|
+
eventId: insertedEventId,
|
|
225
|
+
tenantId: tenantId,
|
|
226
|
+
eventTypeId,
|
|
227
|
+
transactionId,
|
|
228
|
+
vivaMerchantId,
|
|
229
|
+
connectedAccountId,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Duplicate — ON CONFLICT hit
|
|
236
|
+
metrics.counter('viva_webhook_received_total', {
|
|
237
|
+
event_type_id: String(eventTypeId),
|
|
238
|
+
result: 'duplicate',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
logger?.error?.(`[viva] Webhook POST error for message ${messageId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
244
|
+
// Still return 200 so Viva doesn't retry unnecessarily
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await pool.end().catch(() => undefined);
|
|
248
|
+
}
|
|
249
|
+
// ---- Step 8: Always 200 ----
|
|
250
|
+
res.status(200).end();
|
|
251
|
+
};
|
|
252
|
+
exports.POST = POST;
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Helpers
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
function parseExtraAllowlist(raw) {
|
|
257
|
+
if (!raw)
|
|
258
|
+
return [];
|
|
259
|
+
return raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
260
|
+
}
|
|
261
|
+
function parseTrustedProxyDepth(raw) {
|
|
262
|
+
if (!raw)
|
|
263
|
+
return 0;
|
|
264
|
+
const n = Number.parseInt(raw, 10);
|
|
265
|
+
if (Number.isNaN(n) || n < 0)
|
|
266
|
+
return 0;
|
|
267
|
+
return n;
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.js","sourceRoot":"","sources":["../../../../src/api/viva/webhook/route.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;;;;;;AAGH,qDAAoD;AAEpD,4CAAoB;AACpB,qEAKgD;AAEhD,8DAAmE;AAEnE,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E;;;;;;GAMG;AACI,MAAM,GAAG,GAAG,KAAK,EACtB,IAAmB,EACnB,GAAmB,EACJ,EAAE;IACjB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,IAAI,EAAE,CAAC;IAC/D,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iDAAiD,EAAE,CAAC,CAAC;QACnF,OAAO;IACT,CAAC;IACD,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;AACrC,CAAC,CAAC;AAXW,QAAA,GAAG,OAWd;AAEF,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;;;;;;;;;;GAeG;AACI,MAAM,IAAI,GAAG,KAAK,EACvB,GAAkB,EAClB,GAAmB,EACJ,EAAE;IACjB,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,MAAM,CAA0B,CAAC;IACzF,MAAM,cAAc,GAAG,mBAAmB,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,CAAC;IACrF,MAAM,iBAAiB,GAAG,sBAAsB,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAC;IAC1F,MAAM,OAAO,GAAG,IAAA,2BAAgB,GAAE,CAAC;IAEnC,kDAAkD;IAClD,8EAA8E;IAC9E,4EAA4E;IAC5E,2EAA2E;IAC3E,iDAAiD;IACjD,MAAM,QAAQ,GAAG,IAAA,0BAAmB,EAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAC7D,IAAI,CAAC,IAAA,4BAAiB,EAAC,QAAQ,EAAE,WAAW,EAAE,cAAc,CAAC,EAAE,CAAC;QAC9D,oEAAoE;QACpE,4DAA4D;QAC5D,IAAI,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,KAAK,MAAM,EAAE,CAAC;YAC/D,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CACpC,8CAA8C,QAAQ,SAAS,WAAW,GAAG,CAC9E,CAAC;YACF,OAAO,CAAC,OAAO,CAAC,6BAA6B,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;YACpG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;YAC7C,OAAO;QACT,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,MAAM,OAAO,GAAW,GAAG,CAAC,OAAiB,CAAC;IAC9C,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QACtB,OAAO;IACT,CAAC;IAED,+BAA+B;IAC/B,IAAI,QAA6B,CAAC;IAClC,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAwB,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAkD,CAAC;QAC/F,MAAM,EAAE,KAAK,EAAE,CAAC,gDAAgD,CAAC,CAAC;QAClE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QACtB,OAAO;IACT,CAAC;IAED,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,CAAC;IACzC,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;IAErC,yCAAyC;IACzC,wDAAwD;IACxD,MAAM,SAAS,GAAI,QAA+C,CAAC,SAAS,CAAC,CAAC;IAC9E,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;QAChD,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;YACtB,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;YACnD,OAAO,CAAC,SAAS,CAAC,qCAAqC,EAAE,UAAU,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,sCAAsC;IACtC,yEAAyE;IACzE,2EAA2E;IAC3E,0EAA0E;IAC1E,qEAAqE;IACrE,oDAAoD;IACpD,EAAE;IACF,uEAAuE;IACvE,uEAAuE;IACvE,8CAA8C;IAC9C,iEAAiE;IACjE,oCAAoC;IACpC,4DAA4D;IAE5D,mCAAmC;IACnC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;QAC5C,gBAAgB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,UAAU,0BAA0B,CAAC;IAE9E,MAAM,IAAI,GAAG,IAAI,YAAE,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,QAAQ,CAI/B,CAAC;IAEd,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,cAAc,GAAkB,IAAI,CAAC;IACzC,IAAI,kBAAkB,GAAkB,IAAI,CAAC;IAC7C,IAAI,aAAa,GAAkB,IAAI,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,QAAQ,CAAC,SAA+C,CAAC;QAE3E,IAAI,IAAA,6BAAkB,EAAC,WAAW,CAAC,EAAE,CAAC;YACpC,MAAM,MAAM,GAAG,SAA2G,CAAC;YAC3H,cAAc,GAAG,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC;YAC3C,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,IAAI,CAAC;YAC7C,kBAAkB,GAAG,MAAM,CAAC,kBAAkB,IAAI,IAAI,CAAC;YAEvD,IAAI,cAAc,EAAE,CAAC;gBACnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,gFAAgF,EAChF,CAAC,cAAc,CAAC,CACjB,CAAC;oBACF,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,IAAI,CAAC;gBAC5C,CAAC;wBAAS,CAAC;oBACT,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,CAAC;YACH,CAAC;QACH,CAAC;aAAM,IAAI,IAAA,4BAAiB,EAAC,WAAW,CAAC,EAAE,CAAC;YAC1C,MAAM,OAAO,GAAG,SAAuD,CAAC;YACxE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,IAAI,CAAC;YAExD,IAAI,kBAAkB,EAAE,CAAC;gBACvB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,oFAAoF,EACpF,CAAC,kBAAkB,CAAC,CACrB,CAAC;oBACF,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,IAAI,CAAC;gBAC5C,CAAC;wBAAS,CAAC;oBACT,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,2DAA2D;YAC3D,MAAM,EAAE,IAAI,EAAE,CACZ,6CAA6C,WAAW,YAAY,SAAS,GAAG;gBAChF,kBAAkB,cAAc,IAAI,MAAM,uBAAuB,kBAAkB,IAAI,MAAM,IAAI;gBACjG,+CAA+C,CAChD,CAAC;YACF,OAAO,CAAC,OAAO,CAAC,uCAAuC,EAAE,EAAE,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC;QAED,gEAAgE;QAChE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACpC,IAAI,eAAe,GAAkB,IAAI,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B;;;;yCAIiC,EACjC;gBACE,aAAa;gBACb,WAAW;gBACX,SAAS;gBACT,cAAc;gBACd,kBAAkB;gBAClB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;aACzB,CACF,CAAC;YACF,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,qBAAqB,IAAI,IAAI,CAAC;QAClE,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;QAED,+EAA+E;QAC/E,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO,CAAC,OAAO,CAAC,6BAA6B,EAAE;gBAC7C,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC;gBAClC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,mBAAmB;aACpD,CAAC,CAAC;YAEH,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,eAAO,CAAC,SAAS,CAAkC,CAAC;YAC1F,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,QAAQ,CAAC,IAAI,CAAC;oBAClB,IAAI,EAAE,uBAAuB;oBAC7B,IAAI,EAAE;wBACJ,OAAO,EAAE,eAAe;wBACxB,QAAQ,EAAE,QAAQ;wBAClB,WAAW;wBACX,aAAa;wBACb,cAAc;wBACd,kBAAkB;qBACnB;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;aAAM,CAAC;YACN,8BAA8B;YAC9B,OAAO,CAAC,OAAO,CAAC,6BAA6B,EAAE;gBAC7C,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC;gBAClC,MAAM,EAAE,WAAW;aACpB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,KAAK,EAAE,CACb,yCAAyC,SAAS,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC1G,CAAC;QACF,uDAAuD;IACzD,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC1C,CAAC;IAED,+BAA+B;IAC/B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;AACxB,CAAC,CAAC;AA3MW,QAAA,IAAI,QA2Mf;AAEF,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,mBAAmB,CAAC,GAAuB;IAClD,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,sBAAsB,CAAC,GAAuB;IACrD,IAAI,CAAC,GAAG;QAAE,OAAO,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACvC,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin.ts — CLI shim for viva-register-webhooks.
|
|
4
|
+
*
|
|
5
|
+
* Parses CLI flags with node:util.parseArgs (no npm dependency) and
|
|
6
|
+
* delegates to run() from register-webhooks.ts.
|
|
7
|
+
*
|
|
8
|
+
* @see references/viva-docs/md/isv-partner-program.txt:196 (ISV webhook setup)
|
|
9
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:134 (10-URL limit)
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=bin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG"}
|
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* bin.ts — CLI shim for viva-register-webhooks.
|
|
5
|
+
*
|
|
6
|
+
* Parses CLI flags with node:util.parseArgs (no npm dependency) and
|
|
7
|
+
* delegates to run() from register-webhooks.ts.
|
|
8
|
+
*
|
|
9
|
+
* @see references/viva-docs/md/isv-partner-program.txt:196 (ISV webhook setup)
|
|
10
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:134 (10-URL limit)
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
const node_util_1 = require("node:util");
|
|
14
|
+
const register_webhooks_js_1 = require("./register-webhooks.js");
|
|
15
|
+
const { values } = (0, node_util_1.parseArgs)({
|
|
16
|
+
options: {
|
|
17
|
+
'dry-run': { type: 'boolean', default: false },
|
|
18
|
+
apply: { type: 'boolean', default: false },
|
|
19
|
+
'webhook-base-url': { type: 'string' },
|
|
20
|
+
'reconcile-drift': { type: 'boolean', default: false },
|
|
21
|
+
output: { type: 'string', default: 'human' },
|
|
22
|
+
help: { type: 'boolean', default: false, short: 'h' },
|
|
23
|
+
},
|
|
24
|
+
strict: true,
|
|
25
|
+
});
|
|
26
|
+
if (values.help) {
|
|
27
|
+
console.log(`
|
|
28
|
+
viva-register-webhooks — Set up V1 webhook URLs for the configured Viva account.
|
|
29
|
+
|
|
30
|
+
Mode is auto-detected from VIVA_MODE (or VIVA_RESELLER_* presence):
|
|
31
|
+
- ISV mode: idempotently POSTs to /isv/v1/webhooks for every event type.
|
|
32
|
+
- Merchant mode: prints the URLs to paste into Viva Self Care → Sales →
|
|
33
|
+
API Access → Webhooks and the verification key.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
viva-register-webhooks --dry-run [--output json]
|
|
37
|
+
viva-register-webhooks --apply [--reconcile-drift]
|
|
38
|
+
viva-register-webhooks --apply --webhook-base-url https://api.example.com
|
|
39
|
+
|
|
40
|
+
Environment:
|
|
41
|
+
VIVA_MODE merchant | isv (auto-detected if unset)
|
|
42
|
+
VIVA_ENVIRONMENT demo | production (default: demo)
|
|
43
|
+
VIVA_CLIENT_ID required (alias: VIVA_ISV_CLIENT_ID, deprecated)
|
|
44
|
+
VIVA_CLIENT_SECRET required (alias: VIVA_ISV_CLIENT_SECRET, deprecated)
|
|
45
|
+
VIVA_MERCHANT_ID required (Basic-auth username; used in both modes)
|
|
46
|
+
VIVA_API_KEY required (Basic-auth password; used in both modes)
|
|
47
|
+
VIVA_WEBHOOK_VERIFICATION_KEY required
|
|
48
|
+
VIVA_WEBHOOK_BASE_URL required; e.g., https://api.example.com
|
|
49
|
+
|
|
50
|
+
Notes:
|
|
51
|
+
--reconcile-drift is non-applicable in merchant mode (manual setup only).
|
|
52
|
+
|
|
53
|
+
Exit codes:
|
|
54
|
+
0 clean / applied successfully / merchant manual setup printed
|
|
55
|
+
1 plan has actions but --apply was not passed (CI gate use case — ISV mode)
|
|
56
|
+
2 apply failed (HTTP/Viva error)
|
|
57
|
+
3 fatal precondition (config invalid, near-limit abort)
|
|
58
|
+
`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
if (!values['dry-run'] && !values.apply) {
|
|
62
|
+
console.error('Specify --dry-run or --apply (or --help).');
|
|
63
|
+
process.exit(3);
|
|
64
|
+
}
|
|
65
|
+
const runOpts = {
|
|
66
|
+
dryRun: !!values['dry-run'],
|
|
67
|
+
apply: !!values.apply,
|
|
68
|
+
reconcileDrift: !!values['reconcile-drift'],
|
|
69
|
+
output: values.output ?? 'human',
|
|
70
|
+
};
|
|
71
|
+
if (values['webhook-base-url'] !== undefined) {
|
|
72
|
+
runOpts.webhookBaseUrl = values['webhook-base-url'];
|
|
73
|
+
}
|
|
74
|
+
void (async () => {
|
|
75
|
+
const exitCode = await (0, register_webhooks_js_1.run)(runOpts);
|
|
76
|
+
process.exit(exitCode);
|
|
77
|
+
})();
|
|
78
|
+
//# sourceMappingURL=bin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";;AACA;;;;;;;;GAQG;;AAEH,yCAAsC;AACtC,iEAA6C;AAE7C,MAAM,EAAE,MAAM,EAAE,GAAG,IAAA,qBAAS,EAAC;IAC3B,OAAO,EAAE;QACP,SAAS,EAAW,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;QACvD,KAAK,EAAe,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;QACvD,kBAAkB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACtC,iBAAiB,EAAG,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;QACvD,MAAM,EAAc,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE;QACxD,IAAI,EAAgB,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE;KACpE;IACD,MAAM,EAAE,IAAI;CACb,CAAC,CAAC;AAEH,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+Bb,CAAC,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACxC,OAAO,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC3D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,OAAO,GAA8B;IACzC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK;IACrB,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAC3C,MAAM,EAAG,MAAM,CAAC,MAA2B,IAAI,OAAO;CACvD,CAAC;AAEF,IAAI,MAAM,CAAC,kBAAkB,CAAC,KAAK,SAAS,EAAE,CAAC;IAC7C,OAAO,CAAC,cAAc,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;AACtD,CAAC;AAED,KAAK,CAAC,KAAK,IAAI,EAAE;IACf,MAAM,QAAQ,GAAG,MAAM,IAAA,0BAAG,EAAC,OAAO,CAAC,CAAC;IACpC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACzB,CAAC,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI barrel — re-exports the public surface of the register-webhooks CLI.
|
|
3
|
+
*
|
|
4
|
+
* @see references/viva-docs/md/isv-partner-program.txt:196
|
|
5
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:134
|
|
6
|
+
*/
|
|
7
|
+
export { run } from './register-webhooks.js';
|
|
8
|
+
export type { RunOptions } from './register-webhooks.js';
|
|
9
|
+
export { computePlan } from './plan.js';
|
|
10
|
+
export type { ComputePlanInput } from './plan.js';
|
|
11
|
+
export type { DesiredWebhook, WebhookPlanAction, PlanResult, } from './types.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI barrel — re-exports the public surface of the register-webhooks CLI.
|
|
4
|
+
*
|
|
5
|
+
* @see references/viva-docs/md/isv-partner-program.txt:196
|
|
6
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:134
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.computePlan = exports.run = void 0;
|
|
10
|
+
var register_webhooks_js_1 = require("./register-webhooks.js");
|
|
11
|
+
Object.defineProperty(exports, "run", { enumerable: true, get: function () { return register_webhooks_js_1.run; } });
|
|
12
|
+
var plan_js_1 = require("./plan.js");
|
|
13
|
+
Object.defineProperty(exports, "computePlan", { enumerable: true, get: function () { return plan_js_1.computePlan; } });
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,+DAA6C;AAApC,2GAAA,GAAG,OAAA;AAEZ,qCAAwC;AAA/B,sGAAA,WAAW,OAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan.ts — pure, IO-free webhook plan computation.
|
|
3
|
+
*
|
|
4
|
+
* Computes the diff between desired webhook state (from config) and current
|
|
5
|
+
* registrations (from Viva API), returning a deterministic action list.
|
|
6
|
+
*
|
|
7
|
+
* Per Viva docs, max 10 webhook URLs per event type.
|
|
8
|
+
*
|
|
9
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:134 (10-URL limit)
|
|
10
|
+
* @see references/viva-docs/md/isv-partner-program.txt:196 (ISV webhook setup flow)
|
|
11
|
+
*/
|
|
12
|
+
import type { DesiredWebhook, PlanResult, WebhookRegistration } from './types.js';
|
|
13
|
+
export interface ComputePlanInput {
|
|
14
|
+
desired: readonly DesiredWebhook[];
|
|
15
|
+
current: readonly WebhookRegistration[];
|
|
16
|
+
/**
|
|
17
|
+
* Per Viva: 10 URLs per event type.
|
|
18
|
+
* @see references/viva-docs/md/webhooks-for-payments.txt:134
|
|
19
|
+
*/
|
|
20
|
+
perEventTypeLimit?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Warn when registered URLs reach this fraction of the limit.
|
|
23
|
+
* Default 0.8 → warn at 8.
|
|
24
|
+
*/
|
|
25
|
+
nearLimitFraction?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to deactivate URLs that match our hostname pattern but aren't in
|
|
28
|
+
* `desired`. Disabled by default (safer for shared ISV accounts).
|
|
29
|
+
*/
|
|
30
|
+
reconcileDrift?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Hostname pattern that identifies "our" registrations for drift reconciliation.
|
|
33
|
+
* Required when reconcileDrift is true to avoid touching foreign webhooks.
|
|
34
|
+
*/
|
|
35
|
+
ownedHostnamePattern?: RegExp;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Computes the desired-vs-current diff and returns an ordered action list.
|
|
39
|
+
*
|
|
40
|
+
* Action ordering for human readability:
|
|
41
|
+
* 1. REGISTER (sorted by eventTypeId)
|
|
42
|
+
* 2. SKIP_ALREADY_REGISTERED (sorted by eventTypeId)
|
|
43
|
+
* 3. DEACTIVATE_DRIFT (sorted by eventTypeId)
|
|
44
|
+
* 4. WARN_LIMIT_NEAR (sorted by eventTypeId)
|
|
45
|
+
* 5. ABORT_LIMIT_HIT (sorted by eventTypeId)
|
|
46
|
+
*
|
|
47
|
+
* This is deterministic: same logical inputs always produce the same output
|
|
48
|
+
* regardless of input ordering.
|
|
49
|
+
*/
|
|
50
|
+
export declare function computePlan(input: ComputePlanInput): PlanResult;
|
|
51
|
+
//# sourceMappingURL=plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/cli/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,cAAc,EAEd,UAAU,EACV,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACxC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,UAAU,CAyH/D"}
|