mayar 0.1.8 → 0.2.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/README.md +23 -1
- package/node_modules/@mayaross/auth/README.md +204 -0
- package/node_modules/@mayaross/auth/index.js +146 -0
- package/node_modules/@mayaross/auth/package.json +10 -0
- package/package.json +10 -2
- package/src/api.js +2 -1
- package/src/cli.js +21 -3
- package/src/commands/customer.js +24 -2
- package/src/commands/login.js +74 -0
- package/src/commands/review.js +42 -0
- package/src/commands/transaction.js +12 -1
- package/src/config.js +18 -1
package/README.md
CHANGED
|
@@ -4,13 +4,28 @@ Command-line interface for the [Mayar](https://docs.mayar.id) API. Zero runtime
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
**One-liner (no npm required):**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
curl -fsSL https://raw.githubusercontent.com/mayarid/mayar-cli/main/install.sh | sh
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Downloads the latest tarball from the npm registry, extracts to `~/.local/share/mayar`, and symlinks the binary to `~/.local/bin/mayar`. Requires only `node>=18`, `curl`, and `tar`. Override paths or pin a version:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
MAYAR_VERSION=0.1.9 MAYAR_BIN_DIR=/usr/local/bin \
|
|
17
|
+
curl -fsSL https://raw.githubusercontent.com/mayarid/mayar-cli/main/install.sh | sh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Via npm:**
|
|
21
|
+
|
|
7
22
|
```bash
|
|
8
23
|
npm install -g mayar
|
|
9
24
|
# or run without installing:
|
|
10
25
|
npx mayar --help
|
|
11
26
|
```
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
**From source:**
|
|
14
29
|
|
|
15
30
|
```bash
|
|
16
31
|
git clone https://github.com/mayarid/mayar-cli.git
|
|
@@ -93,10 +108,17 @@ Single payment requests
|
|
|
93
108
|
Customers
|
|
94
109
|
customer list [--page ...] GET /hl/v1/customer
|
|
95
110
|
customer create --data <json|@file> POST /hl/v1/customer/create
|
|
111
|
+
customer search <email> GET /hl/v1/customer/detail?email=
|
|
112
|
+
customer update <from> <to> POST /hl/v1/customer/update
|
|
113
|
+
customer magic-link <email> POST /hl/v1/customer/login/portal
|
|
96
114
|
|
|
97
115
|
Transactions
|
|
98
116
|
tx list [--page ...] GET /hl/v1/transactions (paid)
|
|
99
117
|
tx unpaid [--page ...] GET /hl/v1/transactions/unpaid
|
|
118
|
+
tx daily GET /hl/v1/transactions/daily
|
|
119
|
+
|
|
120
|
+
Reviews
|
|
121
|
+
review list [--page ...] GET /hl/v1/reviews
|
|
100
122
|
|
|
101
123
|
Dynamic QR
|
|
102
124
|
qrcode <amount> POST /hl/v1/qrcode/create
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# @mayaross/auth
|
|
2
|
+
|
|
3
|
+
SDK untuk autentikasi `mayar-cli` via mayar auth service menggunakan Google OAuth + SSE.
|
|
4
|
+
|
|
5
|
+
## Instalasi
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mayaross/auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Tidak ada dependency eksternal — hanya menggunakan built-in Node.js (`https`, `http`, `child_process`). Membutuhkan Node.js >= 18.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Flow
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
mayar login
|
|
19
|
+
│
|
|
20
|
+
├─ 1. Minta appid + URL ke server → GET /token/cli/initiate
|
|
21
|
+
│
|
|
22
|
+
├─ 2. Buka browser ke auth URL → /oauth2/google?state=appid,<uuid>
|
|
23
|
+
│
|
|
24
|
+
├─ 3. Subscribe SSE → GET /token/cli/stream?appid=<uuid>
|
|
25
|
+
│ (tunggu token, max 5 menit)
|
|
26
|
+
│
|
|
27
|
+
├─ 4. User login Google di browser
|
|
28
|
+
│
|
|
29
|
+
└─ 5. Server push token via SSE → CLI terima, simpan credentials
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Penggunaan
|
|
35
|
+
|
|
36
|
+
### Login satu langkah
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const { MayarAuth } = require('@mayaross/auth');
|
|
40
|
+
|
|
41
|
+
const auth = new MayarAuth('https://auth.mayar.id');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const token = await auth.login({
|
|
45
|
+
onUrl: ({ url }) => {
|
|
46
|
+
console.log(`Membuka browser untuk login...`);
|
|
47
|
+
console.log(`Jika browser tidak terbuka, buka URL ini:\n${url}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// token = "authToken,refreshToken"
|
|
52
|
+
console.log('Login berhasil!');
|
|
53
|
+
// simpan token ke ~/.mayar/credentials atau keychain
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('Login gagal:', e.message);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Manual step-by-step
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
const { MayarAuth } = require('@mayaross/auth');
|
|
63
|
+
|
|
64
|
+
const auth = new MayarAuth('https://auth.mayar.id');
|
|
65
|
+
|
|
66
|
+
// 1. Dapatkan appid dan URL login
|
|
67
|
+
const { appid, url } = await auth.initiate();
|
|
68
|
+
console.log('Buka URL ini di browser:', url);
|
|
69
|
+
|
|
70
|
+
// 2. (opsional) buka browser secara manual
|
|
71
|
+
auth.openBrowser(url);
|
|
72
|
+
|
|
73
|
+
// 3. Tunggu token via SSE
|
|
74
|
+
const token = await auth.listenForToken(appid);
|
|
75
|
+
console.log('Token diterima:', token);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
### `new MayarAuth(baseUrl, opts?)`
|
|
83
|
+
|
|
84
|
+
| Parameter | Type | Default | Keterangan |
|
|
85
|
+
|-----------|------|---------|------------|
|
|
86
|
+
| `baseUrl` | `string` | `'https://auth.mayar.id'` | Base URL auth server |
|
|
87
|
+
| `opts.timeoutMs` | `number` | `300000` (5 menit) | Timeout tunggu login |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### `auth.login(opts?)`
|
|
92
|
+
|
|
93
|
+
Full login flow: initiate → buka browser → tunggu token via SSE.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
login(opts?: {
|
|
97
|
+
openBrowser?: boolean, // default: true. Set false untuk print URL saja
|
|
98
|
+
onUrl?: ({ appid, url }) => void // callback sebelum browser dibuka
|
|
99
|
+
}): Promise<string> // "authToken,refreshToken"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### `auth.initiate()`
|
|
105
|
+
|
|
106
|
+
Minta `appid` dan `url` dari server.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
initiate(): Promise<{ appid: string, url: string }>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### `auth.listenForToken(appid)`
|
|
115
|
+
|
|
116
|
+
Konek ke SSE stream dan tunggu token. Resolve saat token diterima, reject saat timeout.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
listenForToken(appid: string): Promise<string> // "authToken,refreshToken"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### `auth.openBrowser(url)`
|
|
125
|
+
|
|
126
|
+
Buka URL di browser default. Support macOS, Linux, Windows.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
openBrowser(url: string): void
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Format Token
|
|
135
|
+
|
|
136
|
+
Token yang dikembalikan berupa string dengan format:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
authToken,refreshToken
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- **authToken** — JWT RS256, berlaku 24 jam
|
|
143
|
+
- **refreshToken** — JWT RS256, berlaku 7 hari, digunakan untuk refresh otomatis via `/validate`
|
|
144
|
+
|
|
145
|
+
Untuk decode payload:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const [authToken] = token.split(',');
|
|
149
|
+
const payload = JSON.parse(
|
|
150
|
+
Buffer.from(authToken.split('.')[1], 'base64url').toString()
|
|
151
|
+
);
|
|
152
|
+
// { sub: email, role, name, exp, ... }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Contoh Integrasi di mayar-cli
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
// commands/login.js
|
|
161
|
+
const { MayarAuth } = require('@mayaross/auth');
|
|
162
|
+
const fs = require('fs');
|
|
163
|
+
const os = require('os');
|
|
164
|
+
const path = require('path');
|
|
165
|
+
|
|
166
|
+
const CREDENTIALS_PATH = path.join(os.homedir(), '.mayar', 'credentials.json');
|
|
167
|
+
|
|
168
|
+
async function loginCommand() {
|
|
169
|
+
const auth = new MayarAuth(process.env.MAYAR_AUTH_URL || 'https://auth.mayar.id');
|
|
170
|
+
|
|
171
|
+
console.log('Menghubungkan ke mayar...');
|
|
172
|
+
|
|
173
|
+
const token = await auth.login({
|
|
174
|
+
onUrl: ({ url }) => {
|
|
175
|
+
console.log('\nJika browser tidak terbuka otomatis, buka URL ini:');
|
|
176
|
+
console.log(url + '\n');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const [authToken] = token.split(',');
|
|
181
|
+
const { sub: email, name, exp } = JSON.parse(
|
|
182
|
+
Buffer.from(authToken.split('.')[1], 'base64url').toString()
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
fs.mkdirSync(path.dirname(CREDENTIALS_PATH), { recursive: true });
|
|
186
|
+
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify({ token, email, name }, null, 2));
|
|
187
|
+
|
|
188
|
+
console.log(`\nLogin berhasil sebagai ${name} (${email})`);
|
|
189
|
+
console.log(`Token berlaku hingga ${new Date(exp * 1000).toLocaleString()}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { loginCommand };
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Server Endpoints
|
|
198
|
+
|
|
199
|
+
SDK ini mengonsumsi dua endpoint dari auth server:
|
|
200
|
+
|
|
201
|
+
| Endpoint | Method | Keterangan |
|
|
202
|
+
|----------|--------|------------|
|
|
203
|
+
| `/token/cli/initiate` | GET | Generate `appid` + auth URL |
|
|
204
|
+
| `/token/cli/stream?appid=<uuid>` | GET (SSE) | Stream token setelah user login |
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mayarid/auth-cli
|
|
3
|
+
* SDK for mayar-cli to authenticate via mayar auth service.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* const { MayarAuth } = require('@mayarid/auth-cli');
|
|
7
|
+
* const auth = new MayarAuth('https://auth.mayar.id');
|
|
8
|
+
* const token = await auth.login(); // opens browser, waits for SSE token
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
|
|
15
|
+
class MayarAuth {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} baseUrl - Auth server base URL, e.g. 'https://auth.mayar.id'
|
|
18
|
+
* @param {object} [opts]
|
|
19
|
+
* @param {number} [opts.timeoutMs=300000] - How long to wait for login (default 5 min)
|
|
20
|
+
*/
|
|
21
|
+
constructor(baseUrl = 'https://auth.mayar.id', opts = {}) {
|
|
22
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
23
|
+
this.timeoutMs = opts.timeoutMs ?? 5 * 60 * 1000;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Step 1: Ask the server for a fresh appid + auth URL.
|
|
28
|
+
* @returns {Promise<{ appid: string, url: string }>}
|
|
29
|
+
*/
|
|
30
|
+
async initiate() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const endpoint = new URL(`${this.baseUrl}/token/cli/initiate`);
|
|
33
|
+
const lib = endpoint.protocol === 'https:' ? https : http;
|
|
34
|
+
|
|
35
|
+
lib.get(endpoint.href, (res) => {
|
|
36
|
+
let body = '';
|
|
37
|
+
res.on('data', (chunk) => (body += chunk));
|
|
38
|
+
res.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
resolve(JSON.parse(body));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
reject(new Error(`Invalid response from server: ${body}`));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}).on('error', reject);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Step 2: Open system browser at the given URL.
|
|
51
|
+
* @param {string} url
|
|
52
|
+
*/
|
|
53
|
+
openBrowser(url) {
|
|
54
|
+
const platform = process.platform;
|
|
55
|
+
const cmd = platform === 'darwin' ? 'open'
|
|
56
|
+
: platform === 'win32' ? 'start'
|
|
57
|
+
: 'xdg-open';
|
|
58
|
+
|
|
59
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Step 3: Subscribe to SSE stream and wait for the token event.
|
|
64
|
+
* Resolves with the token pair string "authToken,refreshToken".
|
|
65
|
+
* @param {string} appid
|
|
66
|
+
* @returns {Promise<string>}
|
|
67
|
+
*/
|
|
68
|
+
listenForToken(appid) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const endpoint = new URL(`${this.baseUrl}/token/cli/stream?appid=${encodeURIComponent(appid)}`);
|
|
71
|
+
const lib = endpoint.protocol === 'https:' ? https : http;
|
|
72
|
+
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
req.destroy();
|
|
75
|
+
reject(new Error('Login timed out. Please try again.'));
|
|
76
|
+
}, this.timeoutMs);
|
|
77
|
+
|
|
78
|
+
const req = lib.get(endpoint.href, (res) => {
|
|
79
|
+
if (res.statusCode !== 200) {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
reject(new Error(`SSE stream returned ${res.statusCode}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let buf = '';
|
|
86
|
+
let eventName = '';
|
|
87
|
+
|
|
88
|
+
res.setEncoding('utf8');
|
|
89
|
+
res.on('data', (chunk) => {
|
|
90
|
+
buf += chunk;
|
|
91
|
+
const lines = buf.split('\n');
|
|
92
|
+
buf = lines.pop(); // keep incomplete last line
|
|
93
|
+
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (line.startsWith('event:')) {
|
|
96
|
+
eventName = line.replace('event:', '').trim();
|
|
97
|
+
} else if (line.startsWith('data:')) {
|
|
98
|
+
const raw = line.replace('data:', '').trim();
|
|
99
|
+
if (eventName === 'token') {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
req.destroy();
|
|
102
|
+
try {
|
|
103
|
+
const { token } = JSON.parse(raw);
|
|
104
|
+
resolve(token);
|
|
105
|
+
} catch {
|
|
106
|
+
reject(new Error('Failed to parse token from server'));
|
|
107
|
+
}
|
|
108
|
+
} else if (eventName === 'timeout') {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
req.destroy();
|
|
111
|
+
reject(new Error('Login timed out on server side. Please try again.'));
|
|
112
|
+
}
|
|
113
|
+
eventName = '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
res.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
119
|
+
res.on('end', () => { clearTimeout(timer); reject(new Error('SSE stream closed unexpectedly')); });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
req.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Full login flow: initiate → open browser → listen for SSE token.
|
|
128
|
+
* @param {object} [opts]
|
|
129
|
+
* @param {boolean} [opts.openBrowser=true] - Set false to print URL instead of opening browser
|
|
130
|
+
* @param {function} [opts.onUrl] - Called with { appid, url } before waiting for token
|
|
131
|
+
* @returns {Promise<string>} token pair "authToken,refreshToken"
|
|
132
|
+
*/
|
|
133
|
+
async login({ openBrowser = true, onUrl } = {}) {
|
|
134
|
+
const { appid, url } = await this.initiate();
|
|
135
|
+
|
|
136
|
+
if (typeof onUrl === 'function') onUrl({ appid, url });
|
|
137
|
+
|
|
138
|
+
if (openBrowser) {
|
|
139
|
+
this.openBrowser(url);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return this.listenForToken(appid);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { MayarAuth };
|
package/package.json
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mayar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Zero-dependency Node.js CLI for the Mayar API (production endpoint).",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mayar": "bin/mayar.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "src/cli.js",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node bin/mayar.js"
|
|
10
|
+
"start": "node bin/mayar.js",
|
|
11
|
+
"test": "node --test",
|
|
12
|
+
"test:coverage": "node --test --experimental-test-coverage"
|
|
11
13
|
},
|
|
12
14
|
"engines": {
|
|
13
15
|
"node": ">=18"
|
|
14
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@mayaross/auth": "^1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"bundleDependencies": [
|
|
21
|
+
"@mayaross/auth"
|
|
22
|
+
],
|
|
15
23
|
"keywords": [
|
|
16
24
|
"mayar",
|
|
17
25
|
"mayar-api",
|
package/src/api.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
const { URL } = require('url');
|
|
3
|
+
const config = require('./config');
|
|
3
4
|
|
|
4
|
-
const BASE_URL =
|
|
5
|
+
const BASE_URL = config.apiBaseUrl();
|
|
5
6
|
const VERSION = require('../package.json').version;
|
|
6
7
|
|
|
7
8
|
function request(method, pathname, { apiKey, body, query } = {}) {
|
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ ${ui.bold('Usage:')}
|
|
|
10
10
|
|
|
11
11
|
${ui.bold('Setup:')}
|
|
12
12
|
init Run first-time setup (or re-configure API key)
|
|
13
|
+
login [--no-browser] Sign in via browser (Google OAuth) and save auth token
|
|
13
14
|
api-key <key> Save API key non-interactively
|
|
14
15
|
config show Show config path and masked API key
|
|
15
16
|
config reset Remove the saved API key
|
|
@@ -43,10 +44,17 @@ ${ui.bold('Single payment requests:')}
|
|
|
43
44
|
${ui.bold('Customers:')}
|
|
44
45
|
customer list [--page N --pageSize N]
|
|
45
46
|
customer create --data <json|@file.json>
|
|
47
|
+
customer search <email> Look up a customer by email
|
|
48
|
+
customer update <fromEmail> <toEmail>
|
|
49
|
+
customer magic-link <email> Email a portal login link to the customer
|
|
46
50
|
|
|
47
51
|
${ui.bold('Transactions:')}
|
|
48
52
|
tx list [--page N --pageSize N] Paid transactions
|
|
49
53
|
tx unpaid [--page N --pageSize N] Unpaid transactions
|
|
54
|
+
tx daily Today's totals (volume + count)
|
|
55
|
+
|
|
56
|
+
${ui.bold('Reviews:')}
|
|
57
|
+
review list [--page N --pageSize N]
|
|
50
58
|
|
|
51
59
|
${ui.bold('Dynamic QR:')}
|
|
52
60
|
qrcode <amount>
|
|
@@ -66,10 +74,13 @@ ${ui.bold('Global flags:')}
|
|
|
66
74
|
|
|
67
75
|
${ui.bold('Environment:')}
|
|
68
76
|
MAYAR_API_KEY Used when --api-key is not given and no config is saved
|
|
69
|
-
MAYAR_API_URL Override API base URL
|
|
77
|
+
MAYAR_API_URL Override API base URL
|
|
78
|
+
MAYAR_AUTH_URL Override auth server base URL (used by 'login')
|
|
79
|
+
NODE_ENV=development Target the sandbox (api/auth .mayar.club) instead of production
|
|
70
80
|
|
|
71
81
|
${ui.dim('Resolution order: --api-key flag > MAYAR_API_KEY env > saved config.')}
|
|
72
|
-
${ui.dim('
|
|
82
|
+
${ui.dim('API endpoint: MAYAR_API_URL, else NODE_ENV=development → api.mayar.club, else api.mayar.id')}
|
|
83
|
+
${ui.dim('Auth endpoint: MAYAR_AUTH_URL, else NODE_ENV=development → auth.mayar.club, else auth.mayar.id')}
|
|
73
84
|
${ui.dim('Config: ~/.config/mayar/config.json (chmod 600)')}
|
|
74
85
|
`;
|
|
75
86
|
|
|
@@ -81,6 +92,7 @@ function parseFlags(argv) {
|
|
|
81
92
|
if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
|
|
82
93
|
if (a === '--json') flags.json = true;
|
|
83
94
|
else if (a === '--force') flags.force = true;
|
|
95
|
+
else if (a === '--no-browser') flags['no-browser'] = true;
|
|
84
96
|
else if (a === '--api-key') flags.apiKey = argv[++i];
|
|
85
97
|
else if (a.startsWith('--api-key=')) flags.apiKey = a.slice('--api-key='.length);
|
|
86
98
|
else if (a === '--page') flags.page = argv[++i];
|
|
@@ -133,6 +145,10 @@ async function run(argv) {
|
|
|
133
145
|
const init = require('./commands/init');
|
|
134
146
|
return await init.run({ flags });
|
|
135
147
|
}
|
|
148
|
+
if (cmd === 'login') {
|
|
149
|
+
const login = require('./commands/login');
|
|
150
|
+
return await login.run({ flags });
|
|
151
|
+
}
|
|
136
152
|
if (cmd === 'api-key' || cmd === 'apikey') {
|
|
137
153
|
const apikey = require('./commands/apikey');
|
|
138
154
|
return await apikey.run({ positional: [sub, ...rest].filter((x) => x !== undefined) });
|
|
@@ -170,6 +186,8 @@ async function run(argv) {
|
|
|
170
186
|
qr: './commands/qrcode',
|
|
171
187
|
qrcode: './commands/qrcode',
|
|
172
188
|
webhook: './commands/webhook',
|
|
189
|
+
review: './commands/review',
|
|
190
|
+
reviews: './commands/review',
|
|
173
191
|
};
|
|
174
192
|
const handler = handlers[cmd];
|
|
175
193
|
if (!handler) {
|
|
@@ -187,4 +205,4 @@ async function run(argv) {
|
|
|
187
205
|
}
|
|
188
206
|
}
|
|
189
207
|
|
|
190
|
-
module.exports = { run };
|
|
208
|
+
module.exports = { run, parseFlags };
|
package/src/commands/customer.js
CHANGED
|
@@ -2,10 +2,10 @@ const api = require('../api');
|
|
|
2
2
|
const ui = require('../ui');
|
|
3
3
|
const { checkResp, readData } = require('../util');
|
|
4
4
|
|
|
5
|
-
const USAGE = 'Usage: mayar customer <list|create>';
|
|
5
|
+
const USAGE = 'Usage: mayar customer <list|create|search|update|magic-link>';
|
|
6
6
|
|
|
7
7
|
async function run({ apiKey, flags, positional }) {
|
|
8
|
-
const [sub] = positional;
|
|
8
|
+
const [sub, ...rest] = positional;
|
|
9
9
|
switch (sub) {
|
|
10
10
|
case 'list': {
|
|
11
11
|
const res = await api.request('GET', '/hl/v1/customer', {
|
|
@@ -26,6 +26,28 @@ async function run({ apiKey, flags, positional }) {
|
|
|
26
26
|
const res = await api.request('POST', '/hl/v1/customer/create', { apiKey, body });
|
|
27
27
|
checkResp(res); ui.jsonOut(res.body); return;
|
|
28
28
|
}
|
|
29
|
+
case 'search': {
|
|
30
|
+
if (!rest[0]) throw new Error('Usage: mayar customer search <email>');
|
|
31
|
+
const res = await api.request('GET', '/hl/v1/customer/detail', {
|
|
32
|
+
apiKey, query: { email: rest[0] },
|
|
33
|
+
});
|
|
34
|
+
checkResp(res); ui.jsonOut(res.body); return;
|
|
35
|
+
}
|
|
36
|
+
case 'update': {
|
|
37
|
+
if (rest.length < 2) throw new Error('Usage: mayar customer update <fromEmail> <toEmail>');
|
|
38
|
+
const res = await api.request('POST', '/hl/v1/customer/update', {
|
|
39
|
+
apiKey, body: { fromEmail: rest[0], toEmail: rest[1] },
|
|
40
|
+
});
|
|
41
|
+
checkResp(res); ui.jsonOut(res.body); return;
|
|
42
|
+
}
|
|
43
|
+
case 'magic-link':
|
|
44
|
+
case 'magiclink': {
|
|
45
|
+
if (!rest[0]) throw new Error('Usage: mayar customer magic-link <email>');
|
|
46
|
+
const res = await api.request('POST', '/hl/v1/customer/login/portal', {
|
|
47
|
+
apiKey, body: { email: rest[0] },
|
|
48
|
+
});
|
|
49
|
+
checkResp(res); ui.jsonOut(res.body); return;
|
|
50
|
+
}
|
|
29
51
|
default:
|
|
30
52
|
throw new Error(USAGE);
|
|
31
53
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { MayarAuth } = require('@mayaross/auth');
|
|
2
|
+
const config = require('../config');
|
|
3
|
+
const ui = require('../ui');
|
|
4
|
+
|
|
5
|
+
// Decode the payload of a JWT without verifying its signature.
|
|
6
|
+
function decodeJwt(token) {
|
|
7
|
+
const parts = String(token || '').split('.');
|
|
8
|
+
if (parts.length !== 3) return null;
|
|
9
|
+
try {
|
|
10
|
+
const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
11
|
+
const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));
|
|
12
|
+
return JSON.parse(Buffer.from(padded + pad, 'base64').toString('utf8'));
|
|
13
|
+
} catch (_) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function run({ flags }) {
|
|
19
|
+
const authUrl = config.authBaseUrl();
|
|
20
|
+
const auth = new MayarAuth(authUrl);
|
|
21
|
+
|
|
22
|
+
if (!flags.json) {
|
|
23
|
+
ui.printBanner();
|
|
24
|
+
process.stdout.write(`${ui.bold('Sign in to Mayar')}\n`);
|
|
25
|
+
process.stdout.write(`${ui.dim('Auth server:')} ${authUrl}\n\n`);
|
|
26
|
+
process.stdout.write('Opening your browser to complete Google sign-in…\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const token = await auth.login({
|
|
30
|
+
openBrowser: !flags['no-browser'],
|
|
31
|
+
onUrl: ({ url }) => {
|
|
32
|
+
if (flags.json) return;
|
|
33
|
+
if (flags['no-browser']) {
|
|
34
|
+
process.stdout.write('\nOpen this URL in your browser to sign in:\n');
|
|
35
|
+
} else {
|
|
36
|
+
process.stdout.write('\nIf your browser did not open automatically, visit:\n');
|
|
37
|
+
}
|
|
38
|
+
process.stdout.write(ui.cyan(url) + '\n\n');
|
|
39
|
+
process.stdout.write(ui.dim('Waiting for you to finish signing in…') + '\n');
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// The SDK returns "authToken,refreshToken".
|
|
44
|
+
const [authToken, refreshToken] = String(token).split(',');
|
|
45
|
+
const claims = decodeJwt(authToken) || {};
|
|
46
|
+
const email = claims.sub || claims.email || null;
|
|
47
|
+
const name = claims.name || null;
|
|
48
|
+
const expiresAt = claims.exp ? new Date(claims.exp * 1000).toISOString() : null;
|
|
49
|
+
|
|
50
|
+
const existing = config.load() || {};
|
|
51
|
+
config.save({
|
|
52
|
+
...existing,
|
|
53
|
+
auth: {
|
|
54
|
+
authToken,
|
|
55
|
+
refreshToken: refreshToken || null,
|
|
56
|
+
email,
|
|
57
|
+
name,
|
|
58
|
+
expiresAt,
|
|
59
|
+
authUrl,
|
|
60
|
+
savedAt: new Date().toISOString(),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (flags.json) {
|
|
65
|
+
ui.jsonOut({ ok: true, email, name, expiresAt, authUrl });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.stdout.write('\n' + ui.green(`✓ Signed in${name ? ` as ${name}` : ''}${email ? ` (${email})` : ''}`) + '\n');
|
|
70
|
+
if (expiresAt) process.stdout.write(`${ui.bold('Token expires:')} ${expiresAt}\n`);
|
|
71
|
+
process.stdout.write(ui.dim(`Credentials saved to ${config.file}`) + '\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { run };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const api = require('../api');
|
|
2
|
+
const ui = require('../ui');
|
|
3
|
+
const { checkResp } = require('../util');
|
|
4
|
+
|
|
5
|
+
const USAGE = 'Usage: mayar review <list>';
|
|
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
|
+
|
|
13
|
+
async function run({ apiKey, flags, positional }) {
|
|
14
|
+
const [sub] = positional;
|
|
15
|
+
switch (sub) {
|
|
16
|
+
case undefined:
|
|
17
|
+
case 'list': {
|
|
18
|
+
const res = await api.request('GET', '/hl/v1/reviews', {
|
|
19
|
+
apiKey, query: { page: flags.page, pageSize: flags.pageSize },
|
|
20
|
+
});
|
|
21
|
+
checkResp(res);
|
|
22
|
+
if (flags.json) return ui.jsonOut(res.body);
|
|
23
|
+
const data = (res.body && res.body.data) || [];
|
|
24
|
+
const rows = data.map((r) => ({
|
|
25
|
+
id: r.id,
|
|
26
|
+
rating: r.rating,
|
|
27
|
+
customer: (r.customer && (r.customer.name || r.customer.email)) || '',
|
|
28
|
+
message: r.message || '',
|
|
29
|
+
status: r.status || '',
|
|
30
|
+
createdAt: fmtDate(r.createdAt),
|
|
31
|
+
}));
|
|
32
|
+
ui.table(rows, ['id', 'rating', 'customer', 'message', 'status', 'createdAt']);
|
|
33
|
+
const m = res.body || {};
|
|
34
|
+
process.stdout.write(ui.dim(`page ${m.page ?? '?'} / ${m.pageCount ?? '?'} · total ${m.total ?? data.length}`) + '\n');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(USAGE);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { run };
|
|
@@ -2,7 +2,7 @@ const api = require('../api');
|
|
|
2
2
|
const ui = require('../ui');
|
|
3
3
|
const { checkResp } = require('../util');
|
|
4
4
|
|
|
5
|
-
const USAGE = 'Usage: mayar tx <list|unpaid>';
|
|
5
|
+
const USAGE = 'Usage: mayar tx <list|unpaid|daily>';
|
|
6
6
|
|
|
7
7
|
function fmtDate(v) {
|
|
8
8
|
if (v == null) return '';
|
|
@@ -45,6 +45,17 @@ async function run({ apiKey, flags, positional }) {
|
|
|
45
45
|
if (flags.json) return ui.jsonOut(res.body);
|
|
46
46
|
renderTx(res.body); return;
|
|
47
47
|
}
|
|
48
|
+
case 'daily': {
|
|
49
|
+
const res = await api.request('GET', '/hl/v1/transactions/daily', { apiKey });
|
|
50
|
+
checkResp(res);
|
|
51
|
+
if (flags.json) return ui.jsonOut(res.body);
|
|
52
|
+
const d = (res.body && res.body.data) || {};
|
|
53
|
+
if (d.date) process.stdout.write(`${ui.bold('Date:')} ${d.date}\n`);
|
|
54
|
+
if (d.tpvCount != null) process.stdout.write(`${ui.bold('Total volume:')} ${Number(d.tpvCount).toLocaleString('id-ID')}\n`);
|
|
55
|
+
if (d.trxCount != null) process.stdout.write(`${ui.bold('Transactions:')} ${d.trxCount}\n`);
|
|
56
|
+
if (!d.date && !d.tpvCount && !d.trxCount) ui.jsonOut(res.body);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
48
59
|
default:
|
|
49
60
|
throw new Error(USAGE);
|
|
50
61
|
}
|
package/src/config.js
CHANGED
|
@@ -40,4 +40,21 @@ function clear() {
|
|
|
40
40
|
try { fs.unlinkSync(file); return true; } catch (_) { return false; }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// Environment resolution -----------------------------------------------------
|
|
44
|
+
// NODE_ENV=development targets the *.mayar.club sandbox; anything else targets
|
|
45
|
+
// production *.mayar.id. Explicit env overrides always win.
|
|
46
|
+
function isDev() {
|
|
47
|
+
return process.env.NODE_ENV === 'development';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function apiBaseUrl() {
|
|
51
|
+
if (process.env.MAYAR_API_URL) return process.env.MAYAR_API_URL;
|
|
52
|
+
return isDev() ? 'https://api.mayar.club' : 'https://api.mayar.id';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function authBaseUrl() {
|
|
56
|
+
if (process.env.MAYAR_AUTH_URL) return process.env.MAYAR_AUTH_URL;
|
|
57
|
+
return isDev() ? 'https://auth.mayar.club' : 'https://auth.mayar.id';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { load, save, clear, file, dir, isDev, apiBaseUrl, authBaseUrl };
|