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 +16 -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 +12 -3
- package/src/commands/login.js +74 -0
- 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
|
|
@@ -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
|
|
@@ -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
|
|
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('
|
|
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
|
-
|
|
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 };
|