@cobinar/dalus 0.1.0 → 0.1.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 +28 -10
- package/bin/dalus.js +6 -1
- package/package.json +1 -1
- package/src/commands/login.js +129 -22
package/README.md
CHANGED
|
@@ -18,27 +18,45 @@ relying on this day to day.
|
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
21
|
+
```
|
|
22
|
+
npm install -g @cobinar/dalus
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or from source, for working on dalus itself:
|
|
26
|
+
|
|
21
27
|
```
|
|
22
28
|
cd dalus
|
|
23
29
|
npm install
|
|
24
30
|
npm link # makes the `dalus` command available globally
|
|
25
31
|
```
|
|
26
32
|
|
|
27
|
-
(Not published to npm — this is source you own and can change.)
|
|
28
|
-
|
|
29
33
|
## Log in
|
|
30
34
|
|
|
31
35
|
```
|
|
32
|
-
dalus login
|
|
36
|
+
dalus login
|
|
33
37
|
```
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
Opens your browser to sign in with Cobinar (the same sign-in every
|
|
40
|
+
cobinar.com visitor uses — dalus doesn't have, and doesn't need, its own
|
|
41
|
+
separate login), then stores what it gets back in
|
|
42
|
+
`~/.dalus/credentials.json` (mode 0600), never inside a project
|
|
43
|
+
directory. You'll also be asked for your dashboard-worker URL the first
|
|
44
|
+
time — `dalus login --api-base <url>` to skip that prompt.
|
|
45
|
+
|
|
46
|
+
Mechanically: dalus starts a one-shot local server on an ephemeral port,
|
|
47
|
+
opens `https://cobinar.com/cli-login?port=<port>&ls=<random>`, and waits.
|
|
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
|
+
the result straight to dalus's local server (proving it's genuinely that
|
|
51
|
+
page, not some other local process, via the `ls` value) and dalus takes
|
|
52
|
+
it from there. No separate OAuth client, no manually copying a token out
|
|
53
|
+
of a browser session.
|
|
54
|
+
|
|
55
|
+
`--token <token> [--api-base <url>]` is kept as a manual fallback for a
|
|
56
|
+
CI runner or a headless box with no browser to open — get a token some
|
|
57
|
+
other way and hand it to dalus directly, the same way `gh auth login
|
|
58
|
+
--with-token` works. A real headless flow (a one-time code, email
|
|
59
|
+
verification) is planned but not built yet.
|
|
42
60
|
|
|
43
61
|
`dalus login --whoami` shows what's currently stored (token
|
|
44
62
|
redacted). `dalus login --logout` clears it.
|
package/bin/dalus.js
CHANGED
|
@@ -8,13 +8,18 @@ const { initCommand } = require('../src/commands/init');
|
|
|
8
8
|
const HELP = `dalus — deploy Cobinar projects from the command line
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
dalus login [--
|
|
11
|
+
dalus login [--web-base <url>] [--api-base <url>]
|
|
12
|
+
dalus login --token <token> [--api-base <url>]
|
|
12
13
|
dalus login --whoami
|
|
13
14
|
dalus login --logout
|
|
14
15
|
dalus init [--force]
|
|
15
16
|
dalus forge [--pages] [--workers] [--storage] [--database] [--vault]
|
|
16
17
|
[--env <name>] [--name <resourceName>] [--yes]
|
|
17
18
|
|
|
19
|
+
Bare "dalus login" opens your browser to sign in with Cobinar and stores
|
|
20
|
+
what it gets back. --token accepts one you already have some other way
|
|
21
|
+
(CI, a headless box with no browser) instead.
|
|
22
|
+
|
|
18
23
|
Bare "dalus forge" deploys every resource in dalus.jsonc / dalus.toml /
|
|
19
24
|
dalus.json found in the current directory. Passing one or more of
|
|
20
25
|
--pages/--workers/--storage/--database/--vault narrows it to just
|
package/package.json
CHANGED
package/src/commands/login.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const crypto = require('crypto');
|
|
2
4
|
const readline = require('readline');
|
|
5
|
+
const { execFile } = require('child_process');
|
|
3
6
|
const { saveCredentials, clearCredentials, loadCredentials, CREDENTIALS_PATH } = require('../config');
|
|
4
7
|
|
|
8
|
+
const DEFAULT_WEB_BASE = 'https://cobinar.com';
|
|
9
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
5
11
|
function ask(question) {
|
|
6
12
|
return new Promise((resolve) => {
|
|
7
13
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -9,13 +15,88 @@ function ask(question) {
|
|
|
9
15
|
});
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
function openBrowser(url) {
|
|
19
|
+
// execFile, not exec — no shell involved, so the URL can't be
|
|
20
|
+
// misinterpreted as shell syntax no matter what's in it.
|
|
21
|
+
const done = () => {}; // best-effort; the URL is always printed too
|
|
22
|
+
if (process.platform === 'darwin') execFile('open', [url], done);
|
|
23
|
+
else if (process.platform === 'win32') execFile('cmd', ['/c', 'start', '""', url], done);
|
|
24
|
+
else execFile('xdg-open', [url], done);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 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
|
+
// to POST the result back here once the normal auth.cobinar.com round
|
|
30
|
+
// trip finishes. dalus never talks to auth.cobinar.com directly and
|
|
31
|
+
// never gets its own client_id/secret — it rides along on cobinar.com's
|
|
32
|
+
// existing one, same as any other visitor signing in there. See
|
|
33
|
+
// cobinar-main's public/cli-login/index.html and public/auth/callback.html
|
|
34
|
+
// for the other half of this handshake.
|
|
35
|
+
function browserLogin(webBase) {
|
|
36
|
+
// "ls" (local state): proves the browser tab that finishes is the one
|
|
37
|
+
// THIS process opened, not some other local process guessing the port
|
|
38
|
+
// and racing it — the port alone isn't a secret (any local process can
|
|
39
|
+
// see what's listening), this is.
|
|
40
|
+
const ls = crypto.randomBytes(24).toString('hex');
|
|
41
|
+
const allowedOrigin = new URL(webBase).origin;
|
|
42
|
+
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let settled = false;
|
|
45
|
+
|
|
46
|
+
const server = http.createServer((req, res) => {
|
|
47
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
48
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
49
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
50
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
51
|
+
|
|
52
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
53
|
+
if (url.pathname !== '/complete' || req.method !== 'POST') { res.writeHead(404); res.end(); return; }
|
|
54
|
+
|
|
55
|
+
let body = '';
|
|
56
|
+
req.on('data', (chunk) => {
|
|
57
|
+
body += chunk;
|
|
58
|
+
if (body.length > 1e6) req.destroy(); // this payload is a token + a small profile, never anywhere near this size
|
|
59
|
+
});
|
|
60
|
+
req.on('end', () => {
|
|
61
|
+
let parsed = null;
|
|
62
|
+
try { parsed = JSON.parse(body); } catch { /* falls through to the ok:false below */ }
|
|
63
|
+
|
|
64
|
+
if (!parsed || parsed.ls !== ls || typeof parsed.token !== 'string' || !parsed.token) {
|
|
65
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
66
|
+
res.end(JSON.stringify({ ok: false }));
|
|
67
|
+
return; // deliberately not finish()/reject() here — a stray or malicious request to this port shouldn't cancel a login still in progress
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
71
|
+
res.end(JSON.stringify({ ok: true }));
|
|
72
|
+
finish(null, parsed);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function finish(err, result) {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
server.close();
|
|
81
|
+
if (err) reject(err); else resolve(result);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
finish(new Error(`Timed out waiting for sign-in (${LOGIN_TIMEOUT_MS / 1000}s). Run "dalus login" again.`));
|
|
86
|
+
}, LOGIN_TIMEOUT_MS);
|
|
87
|
+
|
|
88
|
+
server.on('error', (err) => finish(err));
|
|
89
|
+
|
|
90
|
+
server.listen(0, '127.0.0.1', () => {
|
|
91
|
+
const port = server.address().port;
|
|
92
|
+
const loginUrl = `${webBase}/cli-login?port=${port}&ls=${ls}`;
|
|
93
|
+
console.log('Opening your browser to sign in with Cobinar...');
|
|
94
|
+
console.log(`If it doesn't open automatically, visit:\n ${loginUrl}\n`);
|
|
95
|
+
openBrowser(loginUrl);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
19
100
|
async function loginCommand(args) {
|
|
20
101
|
if (args.includes('--logout')) {
|
|
21
102
|
clearCredentials();
|
|
@@ -26,29 +107,55 @@ async function loginCommand(args) {
|
|
|
26
107
|
const creds = loadCredentials();
|
|
27
108
|
if (!creds) { console.log('Not logged in.'); return; }
|
|
28
109
|
console.log(`API base: ${creds.apiBase}`);
|
|
110
|
+
if (creds.user && creds.user.email) console.log(`Signed in as: ${creds.user.email}`);
|
|
29
111
|
console.log(`Token: ${creds.token.slice(0, 8)}...${creds.token.slice(-4)} (stored in ${CREDENTIALS_PATH})`);
|
|
30
112
|
return;
|
|
31
113
|
}
|
|
32
114
|
|
|
33
|
-
const apiBaseArgIdx = args.indexOf('--api-base');
|
|
34
115
|
const tokenArgIdx = args.indexOf('--token');
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
116
|
+
const apiBaseArgIdx = args.indexOf('--api-base');
|
|
117
|
+
|
|
118
|
+
// --token stays as a manual escape hatch for CI/headless boxes that
|
|
119
|
+
// can't open a browser at all — not the default anymore, but not worth
|
|
120
|
+
// removing until there's a real headless flow (one-time code / email,
|
|
121
|
+
// planned later) to replace it with.
|
|
122
|
+
if (tokenArgIdx !== -1) {
|
|
123
|
+
const token = args[tokenArgIdx + 1];
|
|
124
|
+
const apiBase = apiBaseArgIdx !== -1 ? args[apiBaseArgIdx + 1] : await ask('Cobinar dashboard-worker URL: ');
|
|
125
|
+
if (!token || !apiBase) {
|
|
126
|
+
console.error('Both a worker URL and a token are required.');
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
saveCredentials({ apiBase: apiBase.replace(/\/$/, ''), token });
|
|
131
|
+
console.log(`Saved to ${CREDENTIALS_PATH}. Run "dalus forge" from a project directory to deploy.`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const webBaseArgIdx = args.indexOf('--web-base');
|
|
136
|
+
const webBase = (webBaseArgIdx !== -1 ? args[webBaseArgIdx + 1] : DEFAULT_WEB_BASE).replace(/\/$/, '');
|
|
137
|
+
|
|
138
|
+
let result;
|
|
139
|
+
try {
|
|
140
|
+
result = await browserLogin(webBase);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error(err.message);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const apiBase = apiBaseArgIdx !== -1
|
|
148
|
+
? args[apiBaseArgIdx + 1]
|
|
149
|
+
: await ask('Cobinar dashboard-worker URL (e.g. https://worker.lobby.cobinar.com): ');
|
|
150
|
+
if (!apiBase) {
|
|
151
|
+
console.error('A dashboard-worker URL is required.');
|
|
46
152
|
process.exitCode = 1;
|
|
47
153
|
return;
|
|
48
154
|
}
|
|
49
155
|
|
|
50
|
-
saveCredentials({ apiBase: apiBase.replace(/\/$/, ''), token });
|
|
51
|
-
console.log(`
|
|
156
|
+
saveCredentials({ apiBase: apiBase.replace(/\/$/, ''), token: result.token, user: result.user || null });
|
|
157
|
+
console.log(`Signed in${result.user && result.user.email ? ' as ' + result.user.email : ''}. Saved to ${CREDENTIALS_PATH}.`);
|
|
158
|
+
console.log('Run "dalus forge" from a project directory to deploy.');
|
|
52
159
|
}
|
|
53
160
|
|
|
54
|
-
module.exports = { loginCommand };
|
|
161
|
+
module.exports = { loginCommand, browserLogin };
|