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.
- package/README.md +117 -46
- package/package.json +1 -1
- package/src/cli.js +114 -32
- package/src/commands/balance.js +1 -1
- package/src/commands/bundling.js +36 -0
- package/src/commands/credit.js +72 -0
- package/src/commands/customer.js +5 -7
- package/src/commands/discount.js +52 -0
- package/src/commands/installment.js +44 -0
- package/src/commands/invoice.js +53 -25
- package/src/commands/membership.js +92 -0
- package/src/commands/payment-link.js +23 -0
- package/src/commands/payment.js +30 -14
- package/src/commands/product.js +97 -18
- package/src/commands/qrcode.js +35 -5
- package/src/commands/review.js +90 -18
- package/src/commands/saas.js +18 -0
- package/src/commands/software.js +17 -0
- package/src/commands/transaction.js +39 -12
- package/src/commands/webhook.js +46 -13
- package/src/util.js +28 -1
- package/node_modules/@mayaross/auth/README.md +0 -204
- package/node_modules/@mayaross/auth/index.js +0 -146
- package/node_modules/@mayaross/auth/package.json +0 -10
package/src/commands/review.js
CHANGED
|
@@ -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/
|
|
19
|
-
apiKey,
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
ui.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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/
|
|
34
|
-
apiKey,
|
|
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/
|
|
42
|
-
apiKey,
|
|
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/
|
|
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:')}
|
|
54
|
-
if (d.tpvCount != null) process.stdout.write(`${ui.bold('Total volume:')}
|
|
55
|
-
if (d.trxCount != null) process.stdout.write(`${ui.bold('Transactions:')}
|
|
56
|
-
if (!d.date &&
|
|
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
|
}
|
package/src/commands/webhook.js
CHANGED
|
@@ -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
|
-
|
|
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/
|
|
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/
|
|
22
|
-
apiKey,
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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('
|
|
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 |
|