mayar 0.1.9 → 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
@@ -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.9",
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
@@ -73,10 +74,13 @@ ${ui.bold('Global flags:')}
73
74
 
74
75
  ${ui.bold('Environment:')}
75
76
  MAYAR_API_KEY Used when --api-key is not given and no config is saved
76
- 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
77
80
 
78
81
  ${ui.dim('Resolution order: --api-key flag > MAYAR_API_KEY env > saved config.')}
79
- ${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')}
80
84
  ${ui.dim('Config: ~/.config/mayar/config.json (chmod 600)')}
81
85
  `;
82
86
 
@@ -88,6 +92,7 @@ function parseFlags(argv) {
88
92
  if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
89
93
  if (a === '--json') flags.json = true;
90
94
  else if (a === '--force') flags.force = true;
95
+ else if (a === '--no-browser') flags['no-browser'] = true;
91
96
  else if (a === '--api-key') flags.apiKey = argv[++i];
92
97
  else if (a.startsWith('--api-key=')) flags.apiKey = a.slice('--api-key='.length);
93
98
  else if (a === '--page') flags.page = argv[++i];
@@ -140,6 +145,10 @@ async function run(argv) {
140
145
  const init = require('./commands/init');
141
146
  return await init.run({ flags });
142
147
  }
148
+ if (cmd === 'login') {
149
+ const login = require('./commands/login');
150
+ return await login.run({ flags });
151
+ }
143
152
  if (cmd === 'api-key' || cmd === 'apikey') {
144
153
  const apikey = require('./commands/apikey');
145
154
  return await apikey.run({ positional: [sub, ...rest].filter((x) => x !== undefined) });
@@ -196,4 +205,4 @@ async function run(argv) {
196
205
  }
197
206
  }
198
207
 
199
- module.exports = { run };
208
+ module.exports = { run, parseFlags };
@@ -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 };
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 };