@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 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 --api-base https://worker.lobby.cobinar.com --token <your bearer token>
36
+ dalus login
33
37
  ```
34
38
 
35
- dalus doesn't implement Cobinar's own signup/login flow that lives in
36
- `cobinar-developers-worker`, a separate system. This command stores a
37
- token you've already obtained from wherever that flow gives you one, the
38
- same way `gh auth login --with-token` accepts a token instead of
39
- reimplementing GitHub's own login. Omit `--api-base`/`--token` to be
40
- prompted instead. Credentials are stored in `~/.dalus/credentials.json`
41
- (mode 0600), never inside a project directory.
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 [--api-base <url>] [--token <token>]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cobinar/dalus",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Deploy Cobinar projects (Edge Compute, Static Hosting, Object Storage, Document DB, Vault) from the command line.",
5
5
  "bin": {
6
6
  "dalus": "./bin/dalus.js"
@@ -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
- // dalus does not implement Cobinar's own signup/login flow itself — that
13
- // lives in cobinar-developers-worker (a separate system) and issues the
14
- // bearer token the dashboard's browser session already uses. This
15
- // command just stores a token you've already obtained (from that flow,
16
- // wherever it prompts you for one) so dalus can send it on every
17
- // request, the same way `gh auth login --with-token` accepts a token
18
- // you got some other way rather than reimplementing GitHub's own login.
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 apiBase = apiBaseArgIdx !== -1 ? args[apiBaseArgIdx + 1] : await ask('Cobinar dashboard-worker URL (e.g. https://worker.lobby.cobinar.com): ');
36
- // Visible input, deliberately — Node's readline has no built-in masked
37
- // prompt, and a hand-rolled no-echo hack is the kind of thing that's
38
- // easy to get subtly wrong (garbled input, doesn't work across
39
- // terminals) without a real TTY to test it against. --token on the
40
- // command line, or piping the value in, avoids the prompt entirely if
41
- // that matters more than a visible paste in a scrollback buffer.
42
- const token = tokenArgIdx !== -1 ? args[tokenArgIdx + 1] : await ask('Bearer token (visible as you type/paste — see the note in README.md): ');
43
-
44
- if (!apiBase || !token) {
45
- console.error('Both a worker URL and a token are required.');
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(`Saved to ${CREDENTIALS_PATH}. Run "dalus forge" from a project directory to deploy.`);
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 };