mayar 0.2.0 → 1.0.1

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.
@@ -1,8 +1,8 @@
1
1
  const api = require('../api');
2
2
  const ui = require('../ui');
3
- const { checkResp } = require('../util');
3
+ const { checkResp, readData, pagination, cursorFooter } = require('../util');
4
4
 
5
- const USAGE = 'Usage: mayar review <list>';
5
+ const USAGE = 'Usage: mayar review <list|stats|create|update|bulk-status|product|product-customer>';
6
6
 
7
7
  function fmtDate(v) {
8
8
  if (v == null) return '';
@@ -10,29 +10,101 @@ function fmtDate(v) {
10
10
  return String(v);
11
11
  }
12
12
 
13
+ function renderList(body) {
14
+ const data = (body && body.data) || [];
15
+ const rows = data.map((r) => ({
16
+ id: r.id,
17
+ rating: r.rating,
18
+ customer: (r.customer && (r.customer.name || r.customer.email)) || '',
19
+ message: r.message || '',
20
+ status: r.status || '',
21
+ createdAt: fmtDate(r.createdAt),
22
+ }));
23
+ ui.table(rows, ['id', 'rating', 'customer', 'message', 'status', 'createdAt']);
24
+ cursorFooter(body, data.length);
25
+ }
26
+
27
+ function renderStats(body) {
28
+ const d = (body && body.data) || {};
29
+ process.stdout.write(`${ui.bold('Total reviews:')} ${d.total ?? 0}\n`);
30
+ process.stdout.write(`${ui.bold('Average rating:')} ${d.average ?? '—'}\n`);
31
+ if (Array.isArray(d.stats) && d.stats.length) {
32
+ process.stdout.write('\n');
33
+ ui.table(
34
+ d.stats.map((s) => ({
35
+ rating: s.rating,
36
+ totalRating: s.totalRating,
37
+ percentage: typeof s.percentage === 'number' ? `${s.percentage}%` : String(s.percentage ?? ''),
38
+ })),
39
+ ['rating', 'totalRating', 'percentage'],
40
+ );
41
+ }
42
+ }
43
+
13
44
  async function run({ apiKey, flags, positional }) {
14
- const [sub] = positional;
45
+ const [sub, ...rest] = positional;
15
46
  switch (sub) {
16
47
  case undefined:
17
48
  case 'list': {
18
- const res = await api.request('GET', '/hl/v1/reviews', {
19
- apiKey, query: { page: flags.page, pageSize: flags.pageSize },
49
+ const res = await api.request('GET', '/hl/v2/reviews', {
50
+ apiKey,
51
+ query: pagination(flags, {
52
+ status: flags.status, paymentLinkId: flags.paymentLinkId, rating: flags.rating,
53
+ }),
20
54
  });
21
55
  checkResp(res);
22
56
  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;
57
+ renderList(res.body); return;
58
+ }
59
+ case 'stats': {
60
+ const productId = rest[0];
61
+ const path = productId
62
+ ? `/hl/v2/products/${encodeURIComponent(productId)}/reviews/stats`
63
+ : '/hl/v2/merchants/reviews/stats';
64
+ const res = await api.request('GET', path, { apiKey });
65
+ checkResp(res);
66
+ if (flags.json) return ui.jsonOut(res.body);
67
+ renderStats(res.body); return;
68
+ }
69
+ case 'create': {
70
+ const body = readData(flags.data);
71
+ if (!body) throw new Error('mayar review create requires --data <json|@file> ({paymentLinkId,customerEmail,rating})');
72
+ const res = await api.request('POST', '/hl/v2/reviews/create', { apiKey, body });
73
+ checkResp(res); ui.jsonOut(res.body); return;
74
+ }
75
+ case 'update': {
76
+ if (!rest[0]) throw new Error('Usage: mayar review update <id> --data <json|@file>');
77
+ const body = readData(flags.data);
78
+ if (!body) throw new Error('mayar review update requires --data <json|@file>');
79
+ body.id = body.id || rest[0];
80
+ const res = await api.request('POST', `/hl/v2/reviews/${encodeURIComponent(rest[0])}/update`, { apiKey, body });
81
+ checkResp(res); ui.jsonOut(res.body); return;
82
+ }
83
+ case 'bulk-status':
84
+ case 'bulkstatus': {
85
+ const data = readData(flags.data);
86
+ if (!data) throw new Error('mayar review bulk-status requires --data <json|@file> (array of {id,status})');
87
+ const body = Array.isArray(data) ? { input: data } : data;
88
+ const res = await api.request('POST', '/hl/v2/reviews/bulk-status/update', { apiKey, body });
89
+ checkResp(res); ui.jsonOut(res.body); return;
90
+ }
91
+ case 'product': {
92
+ if (!rest[0]) throw new Error('Usage: mayar review product <paymentLinkId>');
93
+ const res = await api.request('GET', `/hl/v2/products/${encodeURIComponent(rest[0])}/reviews`, {
94
+ apiKey, query: pagination(flags, { rating: flags.rating, prioritizeMessage: flags.prioritizeMessage }),
95
+ });
96
+ checkResp(res);
97
+ if (flags.json) return ui.jsonOut(res.body);
98
+ renderList(res.body); return;
99
+ }
100
+ case 'product-customer':
101
+ case 'productcustomer': {
102
+ if (!rest[0]) throw new Error('Usage: mayar review product-customer <paymentLinkId> --customerId <id>');
103
+ if (!flags.customerId) throw new Error('mayar review product-customer requires --customerId <id>');
104
+ const res = await api.request('GET', `/hl/v2/products/${encodeURIComponent(rest[0])}/reviews/customer`, {
105
+ apiKey, query: { customerId: flags.customerId },
106
+ });
107
+ checkResp(res); ui.jsonOut(res.body); return;
36
108
  }
37
109
  default:
38
110
  throw new Error(USAGE);
@@ -0,0 +1,18 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar saas <activate|deactivate|verify> <licenseCode> <productId>';
6
+
7
+ async function run({ apiKey, flags, positional }) {
8
+ const [sub, licenseCode, productId] = positional;
9
+ if (!['activate', 'deactivate', 'verify'].includes(sub)) throw new Error(USAGE);
10
+ if (!licenseCode || !productId) throw new Error(USAGE);
11
+ // SaaS licensing lives under /saas/v2/license/* — NOT /hl/v2.
12
+ const res = await api.request('POST', `/saas/v2/license/${sub}`, {
13
+ apiKey, body: { licenseCode, productId },
14
+ });
15
+ checkResp(res); ui.jsonOut(res.body);
16
+ }
17
+
18
+ module.exports = { run };
@@ -0,0 +1,17 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar software verify <licenseCode> <productId>';
6
+
7
+ async function run({ apiKey, flags, positional }) {
8
+ const [sub, licenseCode, productId] = positional;
9
+ if (sub !== 'verify') throw new Error(USAGE);
10
+ if (!licenseCode || !productId) throw new Error(USAGE);
11
+ const res = await api.request('POST', '/software/v2/license/verify', {
12
+ apiKey, body: { licenseCode, productId },
13
+ });
14
+ checkResp(res); ui.jsonOut(res.body);
15
+ }
16
+
17
+ module.exports = { run };
@@ -1,8 +1,8 @@
1
1
  const api = require('../api');
2
2
  const ui = require('../ui');
3
- const { checkResp } = require('../util');
3
+ const { checkResp, pagination, cursorFooter } = require('../util');
4
4
 
5
- const USAGE = 'Usage: mayar tx <list|unpaid|daily>';
5
+ const USAGE = 'Usage: mayar tx <list|unpaid|daily|product>';
6
6
 
7
7
  function fmtDate(v) {
8
8
  if (v == null) return '';
@@ -22,40 +22,67 @@ function renderTx(body) {
22
22
  createdAt: fmtDate(t.createdAt),
23
23
  }));
24
24
  ui.table(rows, ['id', 'customer', 'amount', 'status', 'createdAt']);
25
+ cursorFooter(body, data.length);
25
26
  }
26
27
 
27
28
  async function run({ apiKey, flags, positional }) {
28
- const [sub] = positional;
29
+ const [sub, ...rest] = positional;
29
30
  switch (sub) {
30
31
  case undefined:
31
32
  case 'list':
32
33
  case 'paid': {
33
- const res = await api.request('GET', '/hl/v1/transactions', {
34
- apiKey, query: { page: flags.page, pageSize: flags.pageSize },
34
+ const res = await api.request('GET', '/hl/v2/transactions', {
35
+ apiKey,
36
+ query: pagination(flags, {
37
+ status: flags.status,
38
+ customerId: flags.customerId,
39
+ type: flags.type,
40
+ paymentLinkId: flags.paymentLinkId,
41
+ startAt: flags.startAt,
42
+ endAt: flags.endAt,
43
+ fields: flags.fields,
44
+ }),
35
45
  });
36
46
  checkResp(res);
37
47
  if (flags.json) return ui.jsonOut(res.body);
38
48
  renderTx(res.body); return;
39
49
  }
40
50
  case 'unpaid': {
41
- const res = await api.request('GET', '/hl/v1/transactions/unpaid', {
42
- apiKey, query: { page: flags.page, pageSize: flags.pageSize },
51
+ const res = await api.request('GET', '/hl/v2/transactions/unpaid', {
52
+ apiKey,
53
+ query: pagination(flags, {
54
+ status: flags.status,
55
+ customerId: flags.customerId,
56
+ paymentLinkId: flags.paymentLinkId,
57
+ startAt: flags.startAt,
58
+ endAt: flags.endAt,
59
+ fields: flags.fields,
60
+ }),
43
61
  });
44
62
  checkResp(res);
45
63
  if (flags.json) return ui.jsonOut(res.body);
46
64
  renderTx(res.body); return;
47
65
  }
48
66
  case 'daily': {
49
- const res = await api.request('GET', '/hl/v1/transactions/daily', { apiKey });
67
+ const res = await api.request('GET', '/hl/v2/transactions/daily', { apiKey });
50
68
  checkResp(res);
51
69
  if (flags.json) return ui.jsonOut(res.body);
52
70
  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);
71
+ if (d.date) process.stdout.write(`${ui.bold('Date:')} ${d.date}\n`);
72
+ if (d.tpvCount != null) process.stdout.write(`${ui.bold('Total volume:')} ${Number(d.tpvCount).toLocaleString('id-ID')}\n`);
73
+ if (d.trxCount != null) process.stdout.write(`${ui.bold('Transactions:')} ${d.trxCount}\n`);
74
+ if (!d.date && d.tpvCount == null && d.trxCount == null) ui.jsonOut(res.body);
57
75
  return;
58
76
  }
77
+ case 'product': {
78
+ if (!rest[0]) throw new Error('Usage: mayar tx product <productId>');
79
+ const res = await api.request('GET', `/hl/v2/products/${encodeURIComponent(rest[0])}/transactions`, {
80
+ apiKey, query: pagination(flags, { status: flags.status, customerId: flags.customerId, fields: flags.fields }),
81
+ });
82
+ checkResp(res);
83
+ if (flags.json) return ui.jsonOut(res.body);
84
+ renderTx(res.body); return;
85
+ }
59
86
  default:
60
87
  throw new Error(USAGE);
61
88
  }
@@ -1,38 +1,71 @@
1
1
  const api = require('../api');
2
2
  const ui = require('../ui');
3
- const { checkResp } = require('../util');
3
+ const { checkResp, pagination, cursorFooter } = require('../util');
4
4
 
5
- const USAGE = 'Usage: mayar webhook <register|test|history>';
5
+ const USAGE = 'Usage: mayar webhook <register|test|history|new-history|retry>';
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
+ function renderHistory(body) {
14
+ const data = (body && body.data) || [];
15
+ const rows = data.map((w) => ({
16
+ id: w.id, type: w.type, status: w.status,
17
+ urlDestination: w.urlDestination || '',
18
+ createdAt: fmtDate(w.createdAt),
19
+ }));
20
+ ui.table(rows, ['id', 'type', 'status', 'urlDestination', 'createdAt']);
21
+ cursorFooter(body, data.length);
22
+ }
6
23
 
7
24
  async function run({ apiKey, flags, positional }) {
8
25
  const [sub, ...rest] = positional;
9
26
  switch (sub) {
10
27
  case 'register': {
11
28
  if (!rest[0]) throw new Error('Usage: mayar webhook register <url>');
12
- const res = await api.request('GET', '/hl/v1/webhook/register', { apiKey, body: { urlHook: rest[0] } });
29
+ // v2: register is now POST /webhooks/update
30
+ const res = await api.request('POST', '/hl/v2/webhooks/update', {
31
+ apiKey, body: { urlHook: rest[0] },
32
+ });
13
33
  checkResp(res); ui.jsonOut(res.body); return;
14
34
  }
15
35
  case 'test': {
16
36
  if (!rest[0]) throw new Error('Usage: mayar webhook test <url>');
17
- const res = await api.request('POST', '/hl/v1/webhook/test', { apiKey, body: { urlHook: rest[0] } });
37
+ const res = await api.request('POST', '/hl/v2/webhooks/test', {
38
+ apiKey, body: { urlHook: rest[0] },
39
+ });
18
40
  checkResp(res); ui.jsonOut(res.body); return;
19
41
  }
20
42
  case 'history': {
21
- const res = await api.request('GET', '/hl/v1/webhook/history', {
22
- apiKey, query: { page: flags.page, pageSize: flags.pageSize },
43
+ const res = await api.request('GET', '/hl/v2/webhooks/history', {
44
+ apiKey,
45
+ query: pagination(flags, {
46
+ status: flags.status, type: flags.type,
47
+ urlDestination: flags.urlDestination,
48
+ startAt: flags.startAt, endAt: flags.endAt,
49
+ }),
23
50
  });
24
51
  checkResp(res);
25
52
  if (flags.json) return ui.jsonOut(res.body);
26
- const data = (res.body && res.body.data) || [];
27
- const rows = data.map((w) => ({
28
- id: w.id, type: w.type, status: w.status, urlDestination: w.urlDestination, createdAt: w.createdAt,
29
- }));
30
- ui.table(rows, ['id', 'type', 'status', 'urlDestination', 'createdAt']);
31
- return;
53
+ renderHistory(res.body); return;
54
+ }
55
+ case 'new-history':
56
+ case 'newhistory': {
57
+ const res = await api.request('GET', '/hl/v2/webhooks/new-history', {
58
+ apiKey, query: pagination(flags),
59
+ });
60
+ checkResp(res);
61
+ if (flags.json) return ui.jsonOut(res.body);
62
+ renderHistory(res.body); return;
32
63
  }
33
64
  case 'retry': {
34
65
  if (!rest[0]) throw new Error('Usage: mayar webhook retry <historyId>');
35
- const res = await api.request('GET', `/hl/v1/webhook/history/retry/${encodeURIComponent(rest[0])}`, { apiKey });
66
+ const res = await api.request('POST', '/hl/v2/webhooks/retry', {
67
+ apiKey, body: { webhookHistoryId: rest[0] },
68
+ });
36
69
  checkResp(res); ui.jsonOut(res.body); return;
37
70
  }
38
71
  default:
package/src/util.js CHANGED
@@ -19,6 +19,33 @@ function readData(value) {
19
19
  return JSON.parse(value);
20
20
  }
21
21
 
22
+ // Build a v2 cursor pagination query from flags.
23
+ // v2 uses limit + startingAfter. --pageSize is accepted as an alias for --limit,
24
+ // --after is the short form of --starting-after.
25
+ function pagination(flags, extra) {
26
+ const q = { ...(extra || {}) };
27
+ const limit = flags.limit ?? flags.pageSize;
28
+ const after = flags.after ?? flags.startingAfter ?? flags['starting-after'];
29
+ if (limit !== undefined && limit !== '') q.limit = limit;
30
+ if (after !== undefined && after !== '') q.startingAfter = after;
31
+ return q;
32
+ }
33
+
34
+ // Print the cursor footer after a paginated list. Mayar v2 returns
35
+ // nextStartingAfter + hasMore on the response envelope.
36
+ function cursorFooter(body, count) {
37
+ const m = body || {};
38
+ const parts = [];
39
+ if (typeof m.pageSize === 'number' || typeof m.limit === 'number') {
40
+ parts.push(`limit ${m.pageSize ?? m.limit}`);
41
+ }
42
+ if (typeof count === 'number') parts.push(`showing ${count}`);
43
+ if (m.nextStartingAfter) parts.push(`next: --after ${m.nextStartingAfter}`);
44
+ if (m.hasMore === true) parts.push('hasMore');
45
+ if (!parts.length) return;
46
+ process.stdout.write(ui.dim(parts.join(' · ')) + '\n');
47
+ }
48
+
22
49
  function maybeJson(flags, body, fallback) {
23
50
  if (flags.json) {
24
51
  ui.jsonOut(body);
@@ -31,4 +58,4 @@ function maybeJson(flags, body, fallback) {
31
58
  return false;
32
59
  }
33
60
 
34
- module.exports = { checkResp, readData, maybeJson };
61
+ module.exports = { checkResp, readData, maybeJson, pagination, cursorFooter };
@@ -1,204 +0,0 @@
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 |