mayar 0.1.1 → 0.1.7

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 CHANGED
@@ -13,14 +13,14 @@ npx mayar --help
13
13
  Or from source:
14
14
 
15
15
  ```bash
16
- git clone https://github.com/moerdowo/mayar-cli.git
16
+ git clone https://github.com/mayarid/mayar-cli.git
17
17
  cd mayar-cli
18
18
  npm link # exposes a `mayar` command on your PATH
19
19
  ```
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> Override saved key for this run
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,6 +1,6 @@
1
1
  {
2
2
  "name": "mayar",
3
- "version": "0.1.1",
3
+ "version": "0.1.7",
4
4
  "description": "Zero-dependency Node.js CLI for the Mayar API (production endpoint).",
5
5
  "bin": {
6
6
  "mayar": "bin/mayar.js"
@@ -23,13 +23,13 @@
23
23
  ],
24
24
  "author": "Frianto Moerdowo <frianto@duck.com>",
25
25
  "license": "MIT",
26
- "homepage": "https://github.com/moerdowo/mayar-cli#readme",
26
+ "homepage": "https://github.com/mayarid/mayar-cli#readme",
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "git+https://github.com/moerdowo/mayar-cli.git"
29
+ "url": "git+https://github.com/mayarid/mayar-cli.git"
30
30
  },
31
31
  "bugs": {
32
- "url": "https://github.com/moerdowo/mayar-cli/issues"
32
+ "url": "https://github.com/mayarid/mayar-cli/issues"
33
33
  },
34
34
  "files": [
35
35
  "bin",
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> Override saved API key for this run
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 };
@@ -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 dir = path.join(os.homedir(), '.mayar');
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 (_) {