@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 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
- 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.
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
- console.error(err.message || err);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cobinar/dalus",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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"
@@ -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
- // 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) {
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 token + a small profile, never anywhere near this size
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.token !== 'string' || !parsed.token) {
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
- res.writeHead(200, { 'Content-Type': 'application/json' });
71
- res.end(JSON.stringify({ ok: true }));
72
- finish(null, parsed);
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 };