nukebase 0.0.1 → 2.0.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 +42 -3
- package/bin/nukebase.js +96 -1
- package/lib/account-commands.js +369 -0
- package/lib/args.js +47 -0
- package/lib/credentials.js +118 -0
- package/lib/login.js +167 -0
- package/lib/project-commands.js +2323 -0
- package/lib/project.js +125 -0
- package/lib/sdk.js +94 -0
- package/lib/ui.js +91 -0
- package/lib/unzip.js +97 -0
- package/package.json +9 -4
package/lib/login.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The ways `nukebase login` gets a CLI token. Each returns
|
|
2
|
+
// { uid, token, label } for credentials.put().
|
|
3
|
+
//
|
|
4
|
+
// browser open /cli-auth.html, approve, a one-time code comes back to a
|
|
5
|
+
// local server, swapped (with a PKCE secret) for the token
|
|
6
|
+
// device a short code to type on the website, for SSH / no browser
|
|
7
|
+
// emailLink the email sign-in link, delivered back to a local server
|
|
8
|
+
// password username + password (the master account)
|
|
9
|
+
//
|
|
10
|
+
// The last two get an ordinary session first and swap it at /cli/exchange,
|
|
11
|
+
// which also signs that session out.
|
|
12
|
+
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const { openBrowser, hostOf } = require('./ui');
|
|
17
|
+
|
|
18
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
const deviceName = () => os.hostname() || 'Unknown device';
|
|
21
|
+
const b64url = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
22
|
+
|
|
23
|
+
function page(mark, title, message, accent) {
|
|
24
|
+
return `<!doctype html><meta charset="utf-8"><title>NukeBase</title><body style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f1f5f9;margin:0;">
|
|
25
|
+
<div style="max-width:420px;margin:80px auto;background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:36px;text-align:center;">
|
|
26
|
+
<div style="font-size:18px;font-weight:800;color:#0f172a;letter-spacing:-0.3px;">◆ NukeBase</div>
|
|
27
|
+
<div style="font-size:40px;margin:16px 0 8px;color:${accent};">${mark}</div>
|
|
28
|
+
<h1 style="font-size:20px;color:#0f172a;margin:0 0 8px;">${title}</h1>
|
|
29
|
+
<p style="color:#475569;margin:0;font-size:15px;">${message}</p>
|
|
30
|
+
</div></body>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A one-shot server on 127.0.0.1 for the browser to come back to. Resolves
|
|
34
|
+
// with the callback's query once one arrives carrying the right state.
|
|
35
|
+
function startLoopback(state) {
|
|
36
|
+
return new Promise((resolveStart, rejectStart) => {
|
|
37
|
+
let finish;
|
|
38
|
+
const result = new Promise((res, rej) => { finish = { res, rej }; });
|
|
39
|
+
const server = http.createServer((req, res) => {
|
|
40
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
41
|
+
if (url.pathname !== '/callback') { res.writeHead(404); return res.end(); }
|
|
42
|
+
const q = Object.fromEntries(url.searchParams);
|
|
43
|
+
if (q.state !== state) {
|
|
44
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
45
|
+
return res.end(page('✕', 'Sign-in error', 'This sign-in could not be verified. Return to your terminal and try again.', '#dc2626'));
|
|
46
|
+
}
|
|
47
|
+
const denied = q.error === 'access_denied';
|
|
48
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
49
|
+
res.end(denied
|
|
50
|
+
? page('✕', 'Cancelled', 'The CLI was not signed in. You can close this tab.', '#64748b')
|
|
51
|
+
: page('✓', 'CLI signed in', 'You can close this tab and return to your terminal.', '#16a34a'));
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
server.close();
|
|
54
|
+
finish.res(q);
|
|
55
|
+
});
|
|
56
|
+
const timer = setTimeout(() => { server.close(); finish.rej(new Error('No response within 5 minutes. Run nukebase login again.')); }, LOGIN_TIMEOUT_MS);
|
|
57
|
+
server.on('error', rejectStart);
|
|
58
|
+
server.listen(0, '127.0.0.1', () => resolveStart({ port: server.address().port, result }));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function postJson(url, body, cookie) {
|
|
63
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
64
|
+
if (cookie) headers.Cookie = cookie;
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body || {}) });
|
|
68
|
+
} catch (e) {
|
|
69
|
+
throw new Error(`Could not reach ${hostOf(url)} (${e.cause?.message || e.message})`);
|
|
70
|
+
}
|
|
71
|
+
let data = {};
|
|
72
|
+
try { data = await res.json(); } catch {}
|
|
73
|
+
return { status: res.status, ok: res.ok, data };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A server from before CLI sign-in answers 404 (or the static 404 page).
|
|
77
|
+
function needsNewerServer(r, api) {
|
|
78
|
+
if (r.status === 404 && !r.data.error) {
|
|
79
|
+
throw new Error(`${hostOf(api)} doesn't support CLI sign-in yet. Try again after it's updated.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function browser(api, { email } = {}) {
|
|
84
|
+
const state = b64url(crypto.randomBytes(24));
|
|
85
|
+
const verifier = b64url(crypto.randomBytes(32));
|
|
86
|
+
const challenge = b64url(crypto.createHash('sha256').update(verifier).digest());
|
|
87
|
+
const { port, result } = await startLoopback(state);
|
|
88
|
+
|
|
89
|
+
const url = new URL(`${api}/cli-auth.html`);
|
|
90
|
+
url.searchParams.set('port', String(port));
|
|
91
|
+
url.searchParams.set('state', state);
|
|
92
|
+
url.searchParams.set('challenge', challenge);
|
|
93
|
+
url.searchParams.set('device', deviceName());
|
|
94
|
+
if (email) url.searchParams.set('email', email);
|
|
95
|
+
|
|
96
|
+
console.log('\n Opening your browser to approve this CLI. If it doesn\'t open, visit:');
|
|
97
|
+
console.log(` ${url}\n`);
|
|
98
|
+
console.log(' Waiting for approval… (Ctrl+C to cancel)');
|
|
99
|
+
openBrowser(url.toString());
|
|
100
|
+
|
|
101
|
+
const q = await result;
|
|
102
|
+
if (q.error) throw new Error('Sign-in was cancelled in the browser.');
|
|
103
|
+
if (!q.code) throw new Error('The browser did not return a sign-in code.');
|
|
104
|
+
const r = await postJson(`${api}/cli/token`, { code: q.code, verifier });
|
|
105
|
+
needsNewerServer(r, api);
|
|
106
|
+
if (!r.ok) throw new Error(r.data.error || `Sign-in failed (${r.status})`);
|
|
107
|
+
return r.data;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function device(api) {
|
|
111
|
+
const start = await postJson(`${api}/cli/device-start`, { device: deviceName() });
|
|
112
|
+
needsNewerServer(start, api);
|
|
113
|
+
if (!start.ok) throw new Error(start.data.error || `Could not start sign-in (${start.status})`);
|
|
114
|
+
const { deviceCode, userCode, verifyUrl, interval = 5, expiresIn = 600 } = start.data;
|
|
115
|
+
|
|
116
|
+
console.log('\n On any device, open:');
|
|
117
|
+
console.log(` ${verifyUrl}`);
|
|
118
|
+
console.log(' and enter this code:\n');
|
|
119
|
+
console.log(` ${userCode}\n`);
|
|
120
|
+
console.log(` Waiting for approval… (expires in ${Math.round(expiresIn / 60)} minutes)`);
|
|
121
|
+
|
|
122
|
+
let wait = Math.max(interval, 2) * 1000;
|
|
123
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
124
|
+
while (Date.now() < deadline) {
|
|
125
|
+
await new Promise(r => setTimeout(r, wait));
|
|
126
|
+
const r = await postJson(`${api}/cli/device-poll`, { deviceCode });
|
|
127
|
+
if (r.status === 410) throw new Error('The code expired. Run nukebase login --device again.');
|
|
128
|
+
if (!r.ok) throw new Error(r.data.error || `Sign-in failed (${r.status})`);
|
|
129
|
+
if (r.data.status === 'slow_down') { wait += 1000; continue; }
|
|
130
|
+
if (r.data.status === 'approved') return { uid: r.data.uid, token: r.data.token, label: r.data.label };
|
|
131
|
+
}
|
|
132
|
+
throw new Error('The code expired. Run nukebase login --device again.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Swap an ordinary session for a CLI token, then sign the session out.
|
|
136
|
+
async function exchange(api, uid, token) {
|
|
137
|
+
const r = await postJson(`${api}/cli/exchange`, { device: deviceName(), revokeCurrent: true }, `uid=${uid}; token=${token}`);
|
|
138
|
+
needsNewerServer(r, api);
|
|
139
|
+
if (r.status === 401) throw new Error('That sign-in is no longer valid.');
|
|
140
|
+
if (!r.ok) throw new Error(r.data.error || `Sign-in failed (${r.status})`);
|
|
141
|
+
return r.data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function emailLink(api, email) {
|
|
145
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
146
|
+
const { port, result } = await startLoopback(state);
|
|
147
|
+
const r = await postJson(`${api}/magic-link`, { email, redirectUri: `http://127.0.0.1:${port}/callback`, state });
|
|
148
|
+
if (r.data.status !== 'Success') throw new Error(r.data.message || 'Could not send the sign-in link.');
|
|
149
|
+
|
|
150
|
+
console.log(`\n ✉ Sign-in link sent to ${email} (if it has a NukeBase account).`);
|
|
151
|
+
console.log(' Open it in a browser on this computer.');
|
|
152
|
+
console.log('\n Waiting for the link to be clicked… (expires in 5 minutes)');
|
|
153
|
+
const q = await result;
|
|
154
|
+
if (!q.uid || !q.token) throw new Error('The sign-in link did not return a session.');
|
|
155
|
+
return exchange(api, q.uid, q.token);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function password(api, username, pass) {
|
|
159
|
+
const r = await postJson(`${api}/login`, { username, password: pass });
|
|
160
|
+
if (r.data.status !== 'Success' || !r.data.uid || !r.data.token) {
|
|
161
|
+
throw new Error(r.data.message || 'Sign-in failed');
|
|
162
|
+
}
|
|
163
|
+
const out = await exchange(api, r.data.uid, r.data.token);
|
|
164
|
+
return { ...out, label: out.label || username };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { browser, device, emailLink, password, exchange, deviceName };
|