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 +21 -0
- package/README.md +48 -0
- package/bin/mavunta.mjs +166 -0
- package/package.json +43 -0
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.
|
package/bin/mavunta.mjs
ADDED
|
@@ -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
|
+
}
|