mavunta-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chainwaka Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @mavunta/cli
2
+
3
+ Command-line tools for **Mavunta Pay** — verify keys, fire sandbox webhook events, and forward live events to your local server for webhook testing.
4
+
5
+ ## Use without installing
6
+
7
+ ```bash
8
+ export MAVUNTA_SECRET_KEY=cwk_test_sk_…
9
+ npx @mavunta/cli verify
10
+ ```
11
+
12
+ Or install globally:
13
+
14
+ ```bash
15
+ npm install -g @mavunta/cli
16
+ mavunta --help
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ ```bash
22
+ # Verify your API key (no money moves)
23
+ mavunta verify
24
+
25
+ # Fire a sandbox webhook event
26
+ mavunta trigger payment_intent.paid
27
+
28
+ # Forward live sandbox events to your local server (the killer command for
29
+ # webhook development) — prints a signing secret and POSTs each new event
30
+ mavunta listen --forward-to http://localhost:3000/mavunta/webhook
31
+ mavunta listen --forward-to http://localhost:3000/webhook --events payment_intent.paid,refund.succeeded
32
+
33
+ # List recent events
34
+ mavunta events --limit 20
35
+ ```
36
+
37
+ ## Environment
38
+
39
+ | Variable | Purpose |
40
+ | --- | --- |
41
+ | `MAVUNTA_SECRET_KEY` | Your `cwk_test_…` / `cwk_live_…` key (required) |
42
+ | `MAVUNTA_BASE_URL` | Override the API base (default `https://api.mavunta.com/v1`) |
43
+
44
+ Webhooks forwarded by `listen` are signed exactly like production: `Mavunta-Signature` is HMAC-SHA256 (hex) over `` `${Mavunta-Timestamp}.${rawBody}` ``. Verify them with [`@mavunta/sdk`](https://www.npmjs.com/package/@mavunta/sdk)'s `webhooks.verify`.
45
+
46
+ ## License
47
+
48
+ MIT © Chainwaka Technologies. Not affiliated with CoinW or any similarly named exchange.
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ // mavunta — command-line tools for the Mavunta Pay API. Zero dependencies
3
+ // (Node 18+ global fetch + node:crypto). Its standout command is `listen`,
4
+ // which forwards live sandbox events to your local server for webhook testing.
5
+ import { createHmac, randomBytes } from 'node:crypto'
6
+
7
+ const VERSION = '1.0.0'
8
+ const BASE = (process.env.MAVUNTA_BASE_URL || 'https://api.mavunta.com/v1').replace(/\/$/, '')
9
+ const KEY = process.env.MAVUNTA_SECRET_KEY || process.env.MAVUNTA_API_KEY || ''
10
+
11
+ const HELP = `mavunta — Mavunta Pay CLI (v${VERSION})
12
+
13
+ Usage:
14
+ mavunta verify Verify your API key (no money moves)
15
+ mavunta trigger <event> Fire a sandbox webhook event
16
+ e.g. mavunta trigger payment_intent.paid
17
+ mavunta listen --forward-to <url> Forward live sandbox events to a local URL
18
+ [--events a,b] optional comma-separated type filter
19
+ mavunta events [--limit N] Show recent events
20
+ mavunta version Print the CLI version
21
+
22
+ Environment:
23
+ MAVUNTA_SECRET_KEY your cwk_test_… / cwk_live_… key (required)
24
+ MAVUNTA_BASE_URL override the API base (default https://api.mavunta.com/v1)`
25
+
26
+ function die(msg, code = 1) {
27
+ console.error(msg)
28
+ process.exit(code)
29
+ }
30
+
31
+ function parseFlags(args) {
32
+ const out = { _: [] }
33
+ for (let i = 0; i < args.length; i++) {
34
+ const a = args[i]
35
+ if (a.startsWith('--')) {
36
+ const key = a.slice(2)
37
+ const next = args[i + 1]
38
+ out[key] = next && !next.startsWith('--') ? args[++i] : 'true'
39
+ } else {
40
+ out._.push(a)
41
+ }
42
+ }
43
+ return out
44
+ }
45
+
46
+ function buildUrl(path, query) {
47
+ let url = BASE + path
48
+ if (query) {
49
+ const p = new URLSearchParams()
50
+ for (const [k, v] of Object.entries(query)) if (v != null) p.set(k, String(v))
51
+ const s = p.toString()
52
+ if (s) url += '?' + s
53
+ }
54
+ return url
55
+ }
56
+
57
+ // fatal=true: print and exit on error (one-shot commands). fatal=false: return
58
+ // null on error (the long-running listen loop must survive transient failures).
59
+ async function request(method, path, { body, query, fatal = true } = {}) {
60
+ if (!KEY) die('Set MAVUNTA_SECRET_KEY to a cwk_test_… or cwk_live_… key.')
61
+ try {
62
+ const res = await fetch(buildUrl(path, query), {
63
+ method,
64
+ headers: {
65
+ Authorization: `Bearer ${KEY}`,
66
+ 'Content-Type': 'application/json',
67
+ 'User-Agent': `mavunta-cli/${VERSION}`,
68
+ },
69
+ body: body != null ? JSON.stringify(body) : undefined,
70
+ })
71
+ const text = await res.text()
72
+ const json = text ? JSON.parse(text) : {}
73
+ if (!res.ok) {
74
+ if (!fatal) return null
75
+ die(`Error ${res.status}: ${json?.error?.code ?? ''} ${json?.error?.message ?? text}`.trim())
76
+ }
77
+ return json
78
+ } catch (err) {
79
+ if (!fatal) return null
80
+ die(`Request failed: ${err?.message ?? err}`)
81
+ }
82
+ }
83
+
84
+ async function cmdVerify() {
85
+ const r = await request('GET', '/auth/verify')
86
+ console.log(`${r.livemode ? 'LIVE' : 'sandbox'} key for merchant ${r.merchant_id}`)
87
+ console.log(`scopes: ${(r.scopes ?? []).join(', ')}`)
88
+ }
89
+
90
+ async function cmdTrigger(flags) {
91
+ const type = flags._[0]
92
+ if (!type) die('Usage: mavunta trigger <event> e.g. mavunta trigger payment_intent.paid')
93
+ await request('POST', '/sandbox/webhooks/trigger', { body: { type } })
94
+ console.log(`Triggered ${type}.`)
95
+ }
96
+
97
+ async function cmdEvents(flags) {
98
+ const r = await request('GET', '/events', { query: { limit: flags.limit ?? 20 } })
99
+ if (!r.data?.length) return console.log('No events yet.')
100
+ for (const e of r.data) console.log(`${e.created_at} ${e.id} ${e.type}`)
101
+ }
102
+
103
+ async function cmdListen(flags) {
104
+ const url = flags['forward-to']
105
+ if (!url) die('Usage: mavunta listen --forward-to http://localhost:3000/webhook')
106
+ const filter = flags.events ? new Set(String(flags.events).split(',').map((s) => s.trim())) : null
107
+ const secret = `whsec_cli_${randomBytes(16).toString('hex')}`
108
+ console.log(`Ready! Forwarding sandbox events to ${url}`)
109
+ console.log(`Your webhook signing secret is ${secret}`)
110
+ console.log('Verify with HMAC-SHA256 over `${Mavunta-Timestamp}.${rawBody}`.\n')
111
+
112
+ // Seed the cursor at the latest event so only NEW events are forwarded.
113
+ const seed = await request('GET', '/events', { query: { limit: 1 } })
114
+ let cursor = seed?.data?.[0]?.id ?? null
115
+
116
+ for (;;) {
117
+ const r = cursor
118
+ ? await request('GET', '/events', { query: { after: cursor, limit: 50 }, fatal: false })
119
+ : await request('GET', '/events', { query: { limit: 1 }, fatal: false })
120
+ if (r?.data?.length) {
121
+ if (cursor) {
122
+ for (const e of r.data) {
123
+ cursor = e.id
124
+ if (filter && !filter.has(e.type)) continue
125
+ const body = JSON.stringify(e)
126
+ const ts = Math.floor(Date.now() / 1000).toString()
127
+ const sig = createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex')
128
+ let status = 'ERR'
129
+ try {
130
+ const resp = await fetch(url, {
131
+ method: 'POST',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'Mavunta-Signature': sig,
135
+ 'Mavunta-Timestamp': ts,
136
+ 'Mavunta-Event-Id': e.id,
137
+ },
138
+ body,
139
+ })
140
+ status = String(resp.status)
141
+ } catch (err) {
142
+ status = err?.message ?? 'ERR'
143
+ }
144
+ console.log(`${e.type} -> ${url} [${status}]`)
145
+ }
146
+ } else {
147
+ cursor = r.data[0].id // establish the starting point, forward nothing
148
+ }
149
+ }
150
+ await new Promise((res) => setTimeout(res, 2000))
151
+ }
152
+ }
153
+
154
+ async function main() {
155
+ const [, , cmd, ...rest] = process.argv
156
+ const flags = parseFlags(rest)
157
+ if (!cmd || cmd === 'help' || flags.help) return console.log(HELP)
158
+ if (cmd === 'version' || flags.version) return console.log(VERSION)
159
+ if (cmd === 'verify') return cmdVerify()
160
+ if (cmd === 'trigger') return cmdTrigger(flags)
161
+ if (cmd === 'events') return cmdEvents(flags)
162
+ if (cmd === 'listen') return cmdListen(flags)
163
+ die(`Unknown command: ${cmd}\n\n${HELP}`)
164
+ }
165
+
166
+ main().catch((e) => die(String(e?.message ?? e)))
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "mavunta-cli",
3
+ "version": "1.0.0",
4
+ "description": "Mavunta Pay command-line tools: verify keys, trigger sandbox webhooks, and forward live events to your local server for webhook testing.",
5
+ "license": "MIT",
6
+ "author": "Chainwaka Technologies",
7
+ "homepage": "https://developers.mavunta.com",
8
+ "keywords": [
9
+ "mavunta",
10
+ "mavunta-pay",
11
+ "cli",
12
+ "webhooks",
13
+ "mpesa",
14
+ "kenya",
15
+ "payments"
16
+ ],
17
+ "type": "module",
18
+ "bin": {
19
+ "mavunta": "./bin/mavunta.mjs"
20
+ },
21
+ "files": [
22
+ "bin",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {},
33
+ "devDependencies": {
34
+ "rimraf": "^6.0.1",
35
+ "vitest": "^3.2.0"
36
+ },
37
+ "scripts": {
38
+ "clean": "rimraf dist",
39
+ "typecheck": "node --check bin/mavunta.mjs",
40
+ "build": "node --check bin/mavunta.mjs",
41
+ "test": "vitest run"
42
+ }
43
+ }