@cobinar/dalus 0.1.1 → 0.1.3
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 +13 -5
- package/bin/dalus.js +5 -1
- package/package.json +1 -1
- package/src/commands/login.js +84 -16
package/README.md
CHANGED
|
@@ -46,11 +46,19 @@ time — `dalus login --api-base <url>` to skip that prompt.
|
|
|
46
46
|
Mechanically: dalus starts a one-shot local server on an ephemeral port,
|
|
47
47
|
opens `https://cobinar.com/cli-login?port=<port>&ls=<random>`, and waits.
|
|
48
48
|
That page runs the exact same auth.cobinar.com round trip as a normal
|
|
49
|
-
sign-in; once it succeeds, cobinar.com's own `/auth/callback` page POSTs
|
|
50
|
-
|
|
51
|
-
page, not some other local process, via the `ls` value)
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
sign-in; once it succeeds, cobinar.com's own `/auth/callback` page POSTs a
|
|
50
|
+
short-lived, single-use code to dalus's local server (proving it's
|
|
51
|
+
genuinely that page, not some other local process, via the `ls` value).
|
|
52
|
+
dalus then redeems that code itself, directly against
|
|
53
|
+
`cobinar-developers-worker`'s `/auth/redeem-code` (override with
|
|
54
|
+
`--developers-base` for staging), which is what actually mints the
|
|
55
|
+
signed token dashboard-worker checks — cobinar.com's callback never
|
|
56
|
+
holds or sees that token, only the one-time code. No separate OAuth
|
|
57
|
+
client, no manually copying anything out of a browser session.
|
|
58
|
+
|
|
59
|
+
The stored token lasts 2 hours (`cobinar-developers-worker` sets that
|
|
60
|
+
expiry) — `dalus login` again once a command starts failing with a
|
|
61
|
+
session-expired error.
|
|
54
62
|
|
|
55
63
|
`--token <token> [--api-base <url>]` is kept as a manual fallback for a
|
|
56
64
|
CI runner or a headless box with no browser to open — get a token some
|
package/bin/dalus.js
CHANGED
|
@@ -60,6 +60,10 @@ async function main() {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
main().catch((err) => {
|
|
63
|
-
|
|
63
|
+
if (err && err.status === 401) {
|
|
64
|
+
console.error('Your Cobinar session expired or was rejected. Run "dalus login" again.');
|
|
65
|
+
} else {
|
|
66
|
+
console.error(err.message || err);
|
|
67
|
+
}
|
|
64
68
|
process.exitCode = 1;
|
|
65
69
|
});
|
package/package.json
CHANGED
package/src/commands/login.js
CHANGED
|
@@ -6,6 +6,13 @@ const { execFile } = require('child_process');
|
|
|
6
6
|
const { saveCredentials, clearCredentials, loadCredentials, CREDENTIALS_PATH } = require('../config');
|
|
7
7
|
|
|
8
8
|
const DEFAULT_WEB_BASE = 'https://cobinar.com';
|
|
9
|
+
// cobinar-developers-worker — mints the actual bearer token dashboard-worker
|
|
10
|
+
// checks (a signed "workerToken"), from the one-time code cobinar.com's
|
|
11
|
+
// callback hands us. Not the same thing as cobinar.com itself or as
|
|
12
|
+
// auth.cobinar.com (Cobinar's general sign-in provider, which cobinar.com
|
|
13
|
+
// is a client of) — those are three separate services. See the comment on
|
|
14
|
+
// exchangeSsoCode below for the exact handoff.
|
|
15
|
+
const DEFAULT_DEVELOPERS_BASE = 'https://worker.dashboard.cobinar.com';
|
|
9
16
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
17
|
|
|
11
18
|
function ask(question) {
|
|
@@ -24,15 +31,62 @@ function openBrowser(url) {
|
|
|
24
31
|
else execFile('xdg-open', [url], done);
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
function postJson(urlString, body) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const url = new URL(urlString);
|
|
37
|
+
const data = JSON.stringify(body);
|
|
38
|
+
const req = require(url.protocol === 'http:' ? 'http' : 'https').request(
|
|
39
|
+
{
|
|
40
|
+
hostname: url.hostname,
|
|
41
|
+
port: url.port || (url.protocol === 'http:' ? 80 : 443),
|
|
42
|
+
path: url.pathname + url.search,
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
45
|
+
},
|
|
46
|
+
(res) => {
|
|
47
|
+
let out = '';
|
|
48
|
+
res.on('data', (c) => (out += c));
|
|
49
|
+
res.on('end', () => {
|
|
50
|
+
let parsed = null;
|
|
51
|
+
try { parsed = JSON.parse(out); } catch { /* leaves parsed null below */ }
|
|
52
|
+
resolve({ status: res.statusCode, json: parsed });
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
req.on('error', reject);
|
|
57
|
+
req.write(data);
|
|
58
|
+
req.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The other half of cobinar-developers-worker's POST /auth/redeem-code:
|
|
63
|
+
// cobinar.com's callback page writes { uid, email, name, picture } to a KV
|
|
64
|
+
// namespace it shares with cobinar-developers-worker, keyed by a one-time
|
|
65
|
+
// code, and hands US that code (over the loopback server below, once the
|
|
66
|
+
// browser gets there). Redeeming it here — not on cobinar.com's server, and
|
|
67
|
+
// not in the browser — is what actually mints the workerToken dashboard-
|
|
68
|
+
// worker will accept; nothing before this point has needed dashboard-worker
|
|
69
|
+
// to trust anything, since a workerToken is the first credential in this
|
|
70
|
+
// whole flow it actually checks.
|
|
71
|
+
async function exchangeSsoCode(developersBase, code) {
|
|
72
|
+
const { status, json } = await postJson(`${developersBase}/auth/redeem-code`, { code });
|
|
73
|
+
if (status !== 200 || !json || typeof json.workerToken !== 'string') {
|
|
74
|
+
const detail = json && json.error ? json.error : `HTTP ${status}`;
|
|
75
|
+
throw new Error(`Could not finish signing in (${detail}). Run "dalus login" again.`);
|
|
76
|
+
}
|
|
77
|
+
return json;
|
|
78
|
+
}
|
|
79
|
+
|
|
27
80
|
// Starts a one-shot local server, opens the browser to Cobinar's own web
|
|
28
|
-
// login (cobinar-main's public/cli-login/ page), and waits for that page
|
|
29
|
-
//
|
|
30
|
-
// trip finishes. dalus never talks to auth.cobinar.com directly and
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
81
|
+
// login (cobinar-main's public/cli-login/ page), and waits for that page to
|
|
82
|
+
// POST a one-time SSO code back here once the normal auth.cobinar.com round
|
|
83
|
+
// trip finishes. dalus never talks to auth.cobinar.com directly and never
|
|
84
|
+
// gets its own client_id/secret — it rides along on cobinar.com's existing
|
|
85
|
+
// one, same as any other visitor signing in there. See cobinar-main's
|
|
86
|
+
// public/cli-login/index.html and public/auth/callback.html for the other
|
|
87
|
+
// half of this handshake, and exchangeSsoCode above for what happens to the
|
|
88
|
+
// code once it arrives.
|
|
89
|
+
function browserLogin(webBase, developersBase) {
|
|
36
90
|
// "ls" (local state): proves the browser tab that finishes is the one
|
|
37
91
|
// THIS process opened, not some other local process guessing the port
|
|
38
92
|
// and racing it — the port alone isn't a secret (any local process can
|
|
@@ -55,21 +109,31 @@ function browserLogin(webBase) {
|
|
|
55
109
|
let body = '';
|
|
56
110
|
req.on('data', (chunk) => {
|
|
57
111
|
body += chunk;
|
|
58
|
-
if (body.length > 1e6) req.destroy(); // this payload is a
|
|
112
|
+
if (body.length > 1e6) req.destroy(); // this payload is a short code + a small profile, never anywhere near this size
|
|
59
113
|
});
|
|
60
|
-
req.on('end', () => {
|
|
114
|
+
req.on('end', async () => {
|
|
61
115
|
let parsed = null;
|
|
62
116
|
try { parsed = JSON.parse(body); } catch { /* falls through to the ok:false below */ }
|
|
63
117
|
|
|
64
|
-
if (!parsed || parsed.ls !== ls || typeof parsed.
|
|
118
|
+
if (!parsed || parsed.ls !== ls || typeof parsed.code !== 'string' || !parsed.code) {
|
|
65
119
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
66
120
|
res.end(JSON.stringify({ ok: false }));
|
|
67
121
|
return; // deliberately not finish()/reject() here — a stray or malicious request to this port shouldn't cancel a login still in progress
|
|
68
122
|
}
|
|
69
123
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
124
|
+
try {
|
|
125
|
+
const redeemed = await exchangeSsoCode(developersBase, parsed.code);
|
|
126
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ ok: true }));
|
|
128
|
+
finish(null, {
|
|
129
|
+
token: redeemed.workerToken,
|
|
130
|
+
user: parsed.user || { email: redeemed.cobinarEmail, name: redeemed.displayName, picture: redeemed.photoURL },
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
134
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
135
|
+
finish(err);
|
|
136
|
+
}
|
|
73
137
|
});
|
|
74
138
|
});
|
|
75
139
|
|
|
@@ -109,6 +173,7 @@ async function loginCommand(args) {
|
|
|
109
173
|
console.log(`API base: ${creds.apiBase}`);
|
|
110
174
|
if (creds.user && creds.user.email) console.log(`Signed in as: ${creds.user.email}`);
|
|
111
175
|
console.log(`Token: ${creds.token.slice(0, 8)}...${creds.token.slice(-4)} (stored in ${CREDENTIALS_PATH})`);
|
|
176
|
+
console.log('Note: this token expires 2 hours after signing in — "dalus login" again once it does.');
|
|
112
177
|
return;
|
|
113
178
|
}
|
|
114
179
|
|
|
@@ -134,10 +199,12 @@ async function loginCommand(args) {
|
|
|
134
199
|
|
|
135
200
|
const webBaseArgIdx = args.indexOf('--web-base');
|
|
136
201
|
const webBase = (webBaseArgIdx !== -1 ? args[webBaseArgIdx + 1] : DEFAULT_WEB_BASE).replace(/\/$/, '');
|
|
202
|
+
const developersBaseArgIdx = args.indexOf('--developers-base');
|
|
203
|
+
const developersBase = (developersBaseArgIdx !== -1 ? args[developersBaseArgIdx + 1] : DEFAULT_DEVELOPERS_BASE).replace(/\/$/, '');
|
|
137
204
|
|
|
138
205
|
let result;
|
|
139
206
|
try {
|
|
140
|
-
result = await browserLogin(webBase);
|
|
207
|
+
result = await browserLogin(webBase, developersBase);
|
|
141
208
|
} catch (err) {
|
|
142
209
|
console.error(err.message);
|
|
143
210
|
process.exitCode = 1;
|
|
@@ -155,7 +222,8 @@ async function loginCommand(args) {
|
|
|
155
222
|
|
|
156
223
|
saveCredentials({ apiBase: apiBase.replace(/\/$/, ''), token: result.token, user: result.user || null });
|
|
157
224
|
console.log(`Signed in${result.user && result.user.email ? ' as ' + result.user.email : ''}. Saved to ${CREDENTIALS_PATH}.`);
|
|
225
|
+
console.log('This session lasts 2 hours — "dalus login" again once it expires.');
|
|
158
226
|
console.log('Run "dalus forge" from a project directory to deploy.');
|
|
159
227
|
}
|
|
160
228
|
|
|
161
|
-
module.exports = { loginCommand, browserLogin };
|
|
229
|
+
module.exports = { loginCommand, browserLogin, exchangeSsoCode };
|