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 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
- Or from source:
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 };
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@mayaross/auth",
3
+ "version": "1.0.0",
4
+ "description": "SDK for mayar-cli to authenticate via mayar auth service",
5
+ "main": "index.js",
6
+ "license": "ISC",
7
+ "engines": {
8
+ "node": ">=18"
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,17 +1,25 @@
1
1
  {
2
2
  "name": "mayar",
3
- "version": "0.1.8",
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 = process.env.MAYAR_API_URL || 'https://api.mayar.id';
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 (default: https://api.mayar.id)
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('Endpoint: MAYAR_API_URL env or https://api.mayar.id (default)')}
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 };
@@ -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
- module.exports = { load, save, clear, file, dir };
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 };