clipoox 0.2.0 → 0.2.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/lib/cli.mjs +1 -2
- package/lib/oauth.mjs +24 -5
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -51,7 +51,6 @@ Examples:
|
|
|
51
51
|
clipoox export canvas:… --out film.mp4`,
|
|
52
52
|
},
|
|
53
53
|
opening: { vi: 'Mở trình duyệt để đăng nhập Clipoox. Nếu trình duyệt không mở, dán link này vào trình duyệt:', en: 'Opening your browser to sign in to Clipoox. If it does not open, paste this link into your browser:' },
|
|
54
|
-
page: { vi: 'Đã đăng nhập Clipoox CLI. Bạn có thể đóng tab này và quay lại terminal.', en: 'Clipoox CLI is signed in. You can close this tab and go back to the terminal.' },
|
|
55
54
|
signedIn: { vi: 'Đã đăng nhập. Thử: clipoox call get_account_summary', en: 'Signed in. Try: clipoox call get_account_summary' },
|
|
56
55
|
signedOut: { vi: 'Đã đăng xuất trên máy này.', en: 'Signed out on this computer.' },
|
|
57
56
|
needLogin: { vi: 'Chưa đăng nhập (hoặc phiên đã hết hạn). Chạy: clipoox login', en: 'Not signed in (or the sign-in expired). Run: clipoox login' },
|
|
@@ -120,7 +119,7 @@ export async function main(argv, io = {}) {
|
|
|
120
119
|
if (!cmd || flags.help || cmd === 'help') { out(M.usage[L]); return cmd || flags.help ? 0 : 2 }
|
|
121
120
|
|
|
122
121
|
if (cmd === 'login') {
|
|
123
|
-
const creds = await login(server, { fetch, headers,
|
|
122
|
+
const creds = await login(server, { fetch, headers, lang: L, open: async (url) => { err(`${M.opening[L]}\n${url}`); if (!flags.noBrowser) (io.open || openBrowser)(url) } })
|
|
124
123
|
saveCredentials(server, creds, env)
|
|
125
124
|
out(M.signedIn[L]); return 0
|
|
126
125
|
}
|
package/lib/oauth.mjs
CHANGED
|
@@ -35,16 +35,35 @@ export async function discover(mcpUrl, { fetch = globalThis.fetch, headers = {}
|
|
|
35
35
|
return { resource: prm.resource || mcpUrl, metadata: as }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// The page the browser lands on after clipoox.com sends it back here: authorized, cancelled (the user pressed Cancel →
|
|
39
|
+
// error=access_denied) or failed. Language: the browser's first Accept-Language entry, else the CLI's own.
|
|
40
|
+
const PAGE = {
|
|
41
|
+
vi: { ok: ['Đã cấp quyền cho thiết bị', 'Bạn có thể đóng tab này và quay lại Clipoox CLI.'], no: ['Đã huỷ kết nối', 'Clipoox CLI chưa được cấp quyền. Bạn có thể đóng tab này và chạy lại clipoox login khi cần.'], bad: ['Không đăng nhập được', 'Đóng tab này và xem lỗi trong terminal.'] },
|
|
42
|
+
en: { ok: ['Device authorized', 'You can close this tab and return to the Clipoox CLI.'], no: ['Connection cancelled', 'Clipoox CLI was not given access. You can close this tab and run clipoox login again when you are ready.'], bad: ['Sign-in failed', 'Close this tab and check the terminal for the error.'] },
|
|
43
|
+
}
|
|
44
|
+
const ICON = {
|
|
45
|
+
ok: ['#2f5a3a', '#5fd47a', '<path d="M17 25.5l6 6 12-13"/>'],
|
|
46
|
+
no: ['#3a3d42', '#b9bec6', '<path d="M19 19l14 14M33 19L19 33"/>'],
|
|
47
|
+
bad: ['#5a2f2f', '#ff8b7c', '<path d="M26 16v12M26 35v.5"/>'],
|
|
48
|
+
}
|
|
49
|
+
export function callbackPage(query = {}, lang = 'en') {
|
|
50
|
+
const kind = query.error === 'access_denied' ? 'no' : query.error || !query.code ? 'bad' : 'ok'
|
|
51
|
+
const [title, text] = (PAGE[lang] || PAGE.en)[kind], [bg, fg, path] = ICON[kind]
|
|
52
|
+
return `<!doctype html><html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title} · Clipoox CLI</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px 16px;box-sizing:border-box;background:#141618 radial-gradient(rgba(255,255,255,.035) 1px,transparent 1px) 0 0/28px 28px;color:#f4f6f1;font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}main{width:min(560px,100%);padding:48px 32px;border-radius:16px;text-align:center;background:linear-gradient(180deg,#303337,#25272b);border:1px solid #3a3d42;box-shadow:0 30px 80px rgba(0,0,0,.4)}svg{width:104px;height:104px}h1{margin:22px 0 10px;font-size:40px;line-height:1.1;letter-spacing:-.01em}p{margin:0;color:#a8adb4;font-size:18px}</style></head><body><main><svg viewBox="0 0 52 52" aria-hidden="true"><circle cx="26" cy="26" r="26" fill="${bg}"/><g fill="none" stroke="${fg}" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">${path}</g></svg><h1>${title}</h1><p>${text}</p></main></body></html>`
|
|
53
|
+
}
|
|
54
|
+
const pageLang = (header, fallback) => { const first = String(header || '').split(',')[0].trim(); return /^[a-z]{2}/i.test(first) ? (/^vi\b/i.test(first) ? 'vi' : 'en') : fallback }
|
|
55
|
+
|
|
38
56
|
/** A one-off listener on 127.0.0.1 (random port) that waits for /callback?code&state. */
|
|
39
|
-
export function callbackServer({
|
|
57
|
+
export function callbackServer({ lang = 'en' } = {}) {
|
|
40
58
|
let resolve, reject
|
|
41
59
|
const result = new Promise((a, b) => { resolve = a; reject = b })
|
|
42
60
|
const server = http.createServer((req, res) => {
|
|
43
61
|
const url = new URL(req.url, 'http://127.0.0.1')
|
|
44
62
|
if (url.pathname !== '/callback') { res.writeHead(404).end(); return }
|
|
63
|
+
const query = Object.fromEntries(url.searchParams)
|
|
45
64
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
|
|
46
|
-
res.end(
|
|
47
|
-
resolve(
|
|
65
|
+
res.end(callbackPage(query, pageLang(req.headers['accept-language'], lang)))
|
|
66
|
+
resolve(query)
|
|
48
67
|
})
|
|
49
68
|
const ready = new Promise((ok, bad) => { server.once('error', bad); server.listen(0, '127.0.0.1', () => ok(server.address().port)) })
|
|
50
69
|
return { ready, result, close: () => server.close(), fail: (e) => reject(e) }
|
|
@@ -54,9 +73,9 @@ export function callbackServer({ page = 'Done — you can close this tab.' } = {
|
|
|
54
73
|
* Full sign-in. `open(url)` shows the authorize page to the user (their browser); resolves with the tokens to save.
|
|
55
74
|
* `timeoutMs` bounds the wait for the browser.
|
|
56
75
|
*/
|
|
57
|
-
export async function login(mcpUrl, { fetch = globalThis.fetch, open, headers = {}, clientName = 'Clipoox CLI',
|
|
76
|
+
export async function login(mcpUrl, { fetch = globalThis.fetch, open, headers = {}, clientName = 'Clipoox CLI', lang = 'en', timeoutMs = 300_000 } = {}) {
|
|
58
77
|
const { resource, metadata } = await discover(mcpUrl, { fetch, headers })
|
|
59
|
-
const cb = callbackServer({
|
|
78
|
+
const cb = callbackServer({ lang })
|
|
60
79
|
try {
|
|
61
80
|
const port = await cb.ready, redirectUri = `http://127.0.0.1:${port}/callback`
|
|
62
81
|
const reg = await json(await fetch(metadata.registration_endpoint, { method: 'POST', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify({ client_name: clientName, redirect_uris: [redirectUri], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'none', scope: SCOPE }) }), 'client registration')
|
package/package.json
CHANGED