mayar 0.1.0 → 0.1.6
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/README.md +14 -5
- package/package.json +2 -2
- package/src/cli.js +15 -2
- package/src/commands/apikey.js +22 -0
- package/src/commands/transaction.js +12 -4
- package/src/commands/whoami.js +64 -0
- package/src/config.js +18 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npm link # exposes a `mayar` command on your PATH
|
|
|
20
20
|
|
|
21
21
|
## First run
|
|
22
22
|
|
|
23
|
-
The first time you invoke any command, the CLI prints an ASCII banner and asks for your production API key (input is masked). Paste it once; it's stored at `~/.mayar/config.json` (chmod 600) and reused on subsequent runs.
|
|
23
|
+
The first time you invoke any command, the CLI prints an ASCII banner and asks for your production API key (input is masked). Paste it once; it's stored at `~/.config/mayar/config.json` (chmod 600, follows the [XDG Base Directory](https://specification.freedesktop.org/basedir-spec/latest/) spec) and reused on subsequent runs.
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
mayar balance
|
|
@@ -29,7 +29,7 @@ mayar balance
|
|
|
29
29
|
# Welcome to Mayar CLI.
|
|
30
30
|
# No API key found. Get yours from https://web.mayar.id → Integration → API Key.
|
|
31
31
|
# Paste your production API key: ************
|
|
32
|
-
# ✓ Saved to /Users/you/.mayar/config.json
|
|
32
|
+
# ✓ Saved to /Users/you/.config/mayar/config.json
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
You can also run `mayar init` explicitly to (re-)configure the key, or pass `--api-key <key>` on any invocation to override.
|
|
@@ -38,11 +38,13 @@ You can also run `mayar init` explicitly to (re-)configure the key, or pass `--a
|
|
|
38
38
|
|
|
39
39
|
```
|
|
40
40
|
Setup
|
|
41
|
-
init Run first-time setup
|
|
41
|
+
init Run first-time setup (interactive, masked input)
|
|
42
|
+
api-key <key> Save API key non-interactively (e.g. for scripts)
|
|
42
43
|
config show Show masked saved key
|
|
43
44
|
config reset Remove the saved key
|
|
44
45
|
|
|
45
46
|
Account
|
|
47
|
+
whoami Show merchant identity (decoded from JWT key) + verify
|
|
46
48
|
balance GET /hl/v1/balance
|
|
47
49
|
|
|
48
50
|
Invoices
|
|
@@ -85,13 +87,18 @@ Webhooks
|
|
|
85
87
|
|
|
86
88
|
Global flags
|
|
87
89
|
--json Output raw JSON instead of pretty tables
|
|
88
|
-
--api-key <key>
|
|
90
|
+
--api-key <key> Use this API key for the run (also accepts --api-key=KEY)
|
|
89
91
|
--page N Pagination page (default 1)
|
|
90
92
|
--pageSize N Pagination page size (default 10)
|
|
91
93
|
-h, --help Show help
|
|
92
94
|
-v, --version Show version
|
|
95
|
+
|
|
96
|
+
Environment
|
|
97
|
+
MAYAR_API_KEY Used when --api-key is not given and no config is saved
|
|
93
98
|
```
|
|
94
99
|
|
|
100
|
+
Resolution order: `--api-key` flag → `MAYAR_API_KEY` env → saved config.
|
|
101
|
+
|
|
95
102
|
## Examples
|
|
96
103
|
|
|
97
104
|
```bash
|
|
@@ -141,9 +148,11 @@ mayar invoice list --json | jq '.data[] | {id, status}'
|
|
|
141
148
|
|
|
142
149
|
| Key | Value |
|
|
143
150
|
| -------- | ------------------------------------------ |
|
|
144
|
-
| Path | `~/.mayar/config.json` (chmod 600)
|
|
151
|
+
| Path | `$XDG_CONFIG_HOME/mayar/config.json`, defaulting to `~/.config/mayar/config.json` (chmod 600) |
|
|
145
152
|
| Endpoint | `https://api.mayar.id` (production, fixed) |
|
|
146
153
|
|
|
154
|
+
Legacy installs that wrote to `~/.mayar/config.json` are migrated automatically on first run.
|
|
155
|
+
|
|
147
156
|
To rotate keys: `mayar config reset && mayar init`.
|
|
148
157
|
|
|
149
158
|
## Notes
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mayar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Zero-dependency Node.js CLI for the Mayar API (production endpoint).",
|
|
5
5
|
"bin": {
|
|
6
|
-
"mayar": "
|
|
6
|
+
"mayar": "bin/mayar.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "src/cli.js",
|
|
9
9
|
"scripts": {
|
package/src/cli.js
CHANGED
|
@@ -10,10 +10,12 @@ ${ui.bold('Usage:')}
|
|
|
10
10
|
|
|
11
11
|
${ui.bold('Setup:')}
|
|
12
12
|
init Run first-time setup (or re-configure API key)
|
|
13
|
+
api-key <key> Save API key non-interactively
|
|
13
14
|
config show Show config path and masked API key
|
|
14
15
|
config reset Remove the saved API key
|
|
15
16
|
|
|
16
17
|
${ui.bold('Account:')}
|
|
18
|
+
whoami Show identity behind the saved API key
|
|
17
19
|
balance Get account balance
|
|
18
20
|
|
|
19
21
|
${ui.bold('Invoices:')}
|
|
@@ -56,14 +58,18 @@ ${ui.bold('Webhooks:')}
|
|
|
56
58
|
|
|
57
59
|
${ui.bold('Global flags:')}
|
|
58
60
|
--json Output raw JSON instead of formatted tables
|
|
59
|
-
--api-key <key>
|
|
61
|
+
--api-key <key> Use this API key for the run (overrides env + saved config)
|
|
60
62
|
--page N Pagination page (default 1)
|
|
61
63
|
--pageSize N Pagination page size (default 10)
|
|
62
64
|
-h, --help Show help
|
|
63
65
|
-v, --version Show version
|
|
64
66
|
|
|
67
|
+
${ui.bold('Environment:')}
|
|
68
|
+
MAYAR_API_KEY Used when --api-key is not given and no config is saved
|
|
69
|
+
|
|
70
|
+
${ui.dim('Resolution order: --api-key flag > MAYAR_API_KEY env > saved config.')}
|
|
65
71
|
${ui.dim('Endpoint: https://api.mayar.id (production only)')}
|
|
66
|
-
${ui.dim('Config: ~/.mayar/config.json (chmod 600)')}
|
|
72
|
+
${ui.dim('Config: ~/.config/mayar/config.json (chmod 600)')}
|
|
67
73
|
`;
|
|
68
74
|
|
|
69
75
|
function parseFlags(argv) {
|
|
@@ -75,6 +81,7 @@ function parseFlags(argv) {
|
|
|
75
81
|
if (a === '--json') flags.json = true;
|
|
76
82
|
else if (a === '--force') flags.force = true;
|
|
77
83
|
else if (a === '--api-key') flags.apiKey = argv[++i];
|
|
84
|
+
else if (a.startsWith('--api-key=')) flags.apiKey = a.slice('--api-key='.length);
|
|
78
85
|
else if (a === '--page') flags.page = argv[++i];
|
|
79
86
|
else if (a === '--pageSize' || a === '--page-size') flags.pageSize = argv[++i];
|
|
80
87
|
else if (a === '--data') flags.data = argv[++i];
|
|
@@ -93,6 +100,7 @@ function parseFlags(argv) {
|
|
|
93
100
|
|
|
94
101
|
async function ensureKey(flags) {
|
|
95
102
|
if (flags.apiKey) return flags.apiKey;
|
|
103
|
+
if (process.env.MAYAR_API_KEY) return process.env.MAYAR_API_KEY;
|
|
96
104
|
const cfg = config.load();
|
|
97
105
|
if (cfg && cfg.apiKey) return cfg.apiKey;
|
|
98
106
|
// First-run flow
|
|
@@ -124,6 +132,10 @@ async function run(argv) {
|
|
|
124
132
|
const init = require('./commands/init');
|
|
125
133
|
return await init.run({ flags });
|
|
126
134
|
}
|
|
135
|
+
if (cmd === 'api-key' || cmd === 'apikey') {
|
|
136
|
+
const apikey = require('./commands/apikey');
|
|
137
|
+
return await apikey.run({ positional: [sub, ...rest].filter((x) => x !== undefined) });
|
|
138
|
+
}
|
|
127
139
|
if (cmd === 'config') {
|
|
128
140
|
if (sub === 'show') {
|
|
129
141
|
const cfg = config.load();
|
|
@@ -145,6 +157,7 @@ async function run(argv) {
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
const handlers = {
|
|
160
|
+
whoami: './commands/whoami',
|
|
148
161
|
balance: './commands/balance',
|
|
149
162
|
invoice: './commands/invoice',
|
|
150
163
|
product: './commands/product',
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const config = require('../config');
|
|
2
|
+
const ui = require('../ui');
|
|
3
|
+
|
|
4
|
+
async function run({ positional }) {
|
|
5
|
+
const [key] = positional;
|
|
6
|
+
if (!key || !key.trim()) {
|
|
7
|
+
throw new Error('Usage: mayar api-key <your-api-key>');
|
|
8
|
+
}
|
|
9
|
+
const trimmed = key.trim();
|
|
10
|
+
const existing = config.load() || {};
|
|
11
|
+
config.save({
|
|
12
|
+
...existing,
|
|
13
|
+
apiKey: trimmed,
|
|
14
|
+
endpoint: 'production',
|
|
15
|
+
savedAt: new Date().toISOString(),
|
|
16
|
+
});
|
|
17
|
+
const masked = trimmed.slice(0, 6) + '…' + trimmed.slice(-4);
|
|
18
|
+
process.stdout.write(ui.green(`✓ API key saved (${masked})`) + '\n');
|
|
19
|
+
process.stdout.write(ui.dim(config.file) + '\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { run };
|
|
@@ -4,14 +4,22 @@ const { checkResp } = require('../util');
|
|
|
4
4
|
|
|
5
5
|
const USAGE = 'Usage: mayar tx <list|unpaid>';
|
|
6
6
|
|
|
7
|
+
function fmtDate(v) {
|
|
8
|
+
if (v == null) return '';
|
|
9
|
+
if (typeof v === 'number') return new Date(v).toISOString();
|
|
10
|
+
return String(v);
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
function renderTx(body) {
|
|
8
14
|
const data = (body && body.data) || [];
|
|
9
15
|
const rows = data.map((t) => ({
|
|
10
16
|
id: t.id,
|
|
11
|
-
customer: (t.customer && (t.customer.name || t.customer.email))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
customer: (t.customer && (t.customer.name || t.customer.email))
|
|
18
|
+
|| (t.paymentLink && t.paymentLink.name)
|
|
19
|
+
|| t.customerName || '',
|
|
20
|
+
amount: t.credit ?? t.amount ?? '',
|
|
21
|
+
status: t.status || '',
|
|
22
|
+
createdAt: fmtDate(t.createdAt),
|
|
15
23
|
}));
|
|
16
24
|
ui.table(rows, ['id', 'customer', 'amount', 'status', 'createdAt']);
|
|
17
25
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const api = require('../api');
|
|
2
|
+
const ui = require('../ui');
|
|
3
|
+
|
|
4
|
+
function decodeJwt(token) {
|
|
5
|
+
const parts = token.split('.');
|
|
6
|
+
if (parts.length !== 3) return null;
|
|
7
|
+
try {
|
|
8
|
+
const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
9
|
+
const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));
|
|
10
|
+
const json = Buffer.from(padded + pad, 'base64').toString('utf8');
|
|
11
|
+
return JSON.parse(json);
|
|
12
|
+
} catch (_) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fmtTs(s) {
|
|
18
|
+
if (!s) return '';
|
|
19
|
+
if (typeof s === 'number') return new Date(s * 1000).toISOString();
|
|
20
|
+
return String(s);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function run({ apiKey, flags }) {
|
|
24
|
+
const masked = apiKey.slice(0, 6) + '…' + apiKey.slice(-4);
|
|
25
|
+
const decoded = decodeJwt(apiKey);
|
|
26
|
+
const res = await api.request('GET', '/hl/v1/balance', { apiKey });
|
|
27
|
+
const valid = res.status >= 200 && res.status < 300;
|
|
28
|
+
|
|
29
|
+
if (flags.json) {
|
|
30
|
+
ui.jsonOut({
|
|
31
|
+
apiKey: masked,
|
|
32
|
+
valid,
|
|
33
|
+
status: res.status,
|
|
34
|
+
decoded,
|
|
35
|
+
balance: valid ? (res.body && res.body.data) : null,
|
|
36
|
+
error: valid ? null : (res.body && (res.body.messages || res.body.message)) || null,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
process.stdout.write(`${ui.bold('API key:')} ${masked}\n`);
|
|
42
|
+
process.stdout.write(`${ui.bold('Endpoint:')} ${api.BASE_URL}\n`);
|
|
43
|
+
if (decoded) {
|
|
44
|
+
const name = decoded.name || decoded.merchantName || decoded.fullName;
|
|
45
|
+
const id = decoded.accountId || decoded.userId || decoded.sub || decoded.id;
|
|
46
|
+
const email = decoded.email || decoded.merchantEmail;
|
|
47
|
+
if (name) process.stdout.write(`${ui.bold('Name:')} ${name}\n`);
|
|
48
|
+
if (email) process.stdout.write(`${ui.bold('Email:')} ${email}\n`);
|
|
49
|
+
if (id) process.stdout.write(`${ui.bold('Account ID:')} ${id}\n`);
|
|
50
|
+
if (decoded.exp) process.stdout.write(`${ui.bold('Expires:')} ${fmtTs(decoded.exp)}\n`);
|
|
51
|
+
} else {
|
|
52
|
+
process.stdout.write(ui.dim('(API key is not a decodable JWT — skipping local profile)') + '\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (valid) {
|
|
56
|
+
process.stdout.write(ui.green('✓ Key is valid (verified via /hl/v1/balance)') + '\n');
|
|
57
|
+
} else {
|
|
58
|
+
const msg = (res.body && (res.body.messages || res.body.message)) || res.raw || '';
|
|
59
|
+
process.stdout.write(ui.red(`✗ Key rejected (HTTP ${res.status}) — ${msg}`) + '\n');
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { run };
|
package/src/config.js
CHANGED
|
@@ -2,10 +2,27 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
6
|
+
const dir = path.join(xdgConfigHome, 'mayar');
|
|
6
7
|
const file = path.join(dir, 'config.json');
|
|
7
8
|
|
|
9
|
+
const legacyDir = path.join(os.homedir(), '.mayar');
|
|
10
|
+
const legacyFile = path.join(legacyDir, 'config.json');
|
|
11
|
+
|
|
12
|
+
function migrateLegacy() {
|
|
13
|
+
if (fs.existsSync(file)) return;
|
|
14
|
+
if (!fs.existsSync(legacyFile)) return;
|
|
15
|
+
try {
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
17
|
+
fs.copyFileSync(legacyFile, file);
|
|
18
|
+
try { fs.chmodSync(file, 0o600); } catch (_) {}
|
|
19
|
+
fs.unlinkSync(legacyFile);
|
|
20
|
+
try { fs.rmdirSync(legacyDir); } catch (_) {}
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
function load() {
|
|
25
|
+
migrateLegacy();
|
|
9
26
|
try {
|
|
10
27
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
11
28
|
} catch (_) {
|