@invitedlabs/cli 0.2.0 → 0.2.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
@@ -70,6 +70,10 @@ Fragments reach the CLI only when you share them on the consent screen (`invited
70
70
  3. Remove the `invited` entry from each host you connected (`claude mcp remove invited`, or delete it from the file listed above).
71
71
  4. `npm uninstall --global @invitedlabs/cli` if you installed it.
72
72
 
73
+ ## Changes in 0.2.1
74
+
75
+ - After you approve or cancel the log-in in your browser, the CLI sends the browser to Invited's own page for it, in the same look as invited.you's log-in pages. The page says only whether it connected; the code stays with the CLI.
76
+
73
77
  ## Changes in 0.2.0
74
78
 
75
79
  - The CLI reads only the fragments you share with it, never your Page, as every connected assistant does since 8 November 2026. `invited login` always asks you to choose them (`--private` is still taken); `whoami` and `fragments` print them; `status` no longer shows a Page. 0.1.0 reads only after `invited login --private`, and its `invited fragments` answers "Invited answered in a form this CLI does not know.": update to 0.2.0.
package/dist/loopback.js CHANGED
@@ -1,51 +1,27 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import { createServer } from 'node:http';
3
2
  import { duration } from './format.js';
4
- const escape = (text) => text.replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`);
5
- /** The Invited lockup, the Missing piece and the wordmark (public/brand/missing-piece/lockup-ink.svg, the Figma
6
- * kit's drawing), drawn in the page's ink. */
7
- const LOCKUP = '<svg viewBox="69 90.25 394.75 105.75" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Invited" role="img">' +
8
- '<path fill-rule="evenodd" clip-rule="evenodd" d="M70.2305 150.001C70.2305 161.904 74.9589 173.319 83.3755 181.736C91.7921 190.153 103.208 194.881 115.11 194.881C127.013 194.881 138.429 190.153 146.845 181.736C155.262 173.319 159.99 161.904 159.99 150.001C159.99 138.098 155.262 126.683 146.845 118.266C138.429 109.85 127.013 105.121 115.11 105.121C103.208 105.121 91.7921 109.85 83.3755 118.266C74.9589 126.683 70.2305 138.098 70.2305 150.001ZM118.898 129.349C118.898 125.521 119.959 124.453 123.794 124.453H135.762C139.59 124.453 140.658 125.521 140.658 129.349V141.317C140.658 145.153 139.59 146.213 135.762 146.213H123.794C119.959 146.213 118.898 145.153 118.898 141.317V129.349Z" fill="currentColor"/>' +
9
- '<path d="M149.268 100.665C148.336 96.9456 149.111 95.6604 152.831 94.7288L164.445 91.832C168.165 90.9072 169.45 91.6824 170.382 95.402L173.278 107.016C174.203 110.736 173.428 112.021 169.708 112.946L158.094 115.843C154.374 116.774 153.089 115.999 152.164 112.28L149.268 100.665Z" fill="currentColor"/>' +
10
- '<path d="M211.085 184.179H198.029V115.827H211.085V184.179ZM261.975 184.179H249.591V153.843C249.591 147.123 247.191 143.763 241.143 143.763C234.615 143.763 230.391 148.467 230.391 156.531V184.179H218.007V134.643H229.815V142.707C232.503 137.427 236.919 133.683 244.983 133.683C256.311 133.683 261.975 139.443 261.975 151.443V184.179ZM295.223 184.179H280.247L261.911 134.643H274.871L287.735 173.331L300.599 134.643H313.559L295.223 184.179ZM328.225 184.179H315.841V134.643H328.225V184.179ZM322.081 127.251C317.473 127.251 314.881 124.467 314.881 120.819C314.881 117.171 317.473 114.387 322.081 114.387C326.689 114.387 329.185 117.171 329.185 120.819C329.185 124.467 326.689 127.251 322.081 127.251ZM353.535 184.659C342.303 184.659 338.559 181.203 338.559 170.259V143.475H330.783V134.643H338.559V119.667H350.943V134.643H361.695V143.475H350.943V170.739C350.943 174.579 352.575 175.923 356.511 175.923L361.695 175.827V184.179C359.679 184.371 355.743 184.659 353.535 184.659ZM386.843 185.139C370.715 185.139 361.691 175.059 361.691 159.411C361.691 144.147 370.331 133.683 386.267 133.683C402.299 133.683 410.939 144.243 410.939 158.835C410.939 159.891 410.843 160.851 410.747 162.003H374.075C374.459 172.083 379.259 176.691 386.843 176.691C392.987 176.691 397.019 173.619 398.459 168.531H409.883C407.771 178.707 400.187 185.139 386.843 185.139ZM386.267 142.131C379.451 142.131 375.131 146.067 374.075 153.939H398.555C397.499 146.067 393.083 142.131 386.267 142.131ZM435.073 185.139C421.729 185.139 413.281 175.635 413.281 159.411C413.281 143.187 421.729 133.683 435.073 133.683C442.561 133.683 447.553 136.755 450.337 142.131V115.827H462.721V184.179H450.912V175.731C448.225 181.491 443.329 185.139 435.073 185.139ZM437.953 175.731C445.345 175.731 450.337 170.739 450.337 159.411C450.337 148.083 445.345 143.091 437.953 143.091C430.657 143.091 425.665 148.083 425.665 159.411C425.665 170.739 430.657 175.731 437.953 175.731Z" fill="currentColor"/>' +
11
- '</svg>';
12
- /** The page the browser shows when it comes back to the CLI: the sign-in
13
- * screens' look (dark, the lockup, one cut-corner panel; styles/invited.css
14
- * tokens copied here, since this page loads nothing from anywhere). The
15
- * stylesheet carries a per-response nonce, so the policy admits nothing else. */
16
- function page(response, status, title, message, after) {
17
- const nonce = randomBytes(16).toString('base64url');
18
- const tone = status === 200 ? 'Connected' : 'Not connected';
19
- const style = `:root{color-scheme:dark}*{box-sizing:border-box}` +
20
- `body{margin:0;min-height:100dvh;display:grid;place-items:center;padding:32px 24px;background:#121212;color:#f5f5f5;` +
21
- `font:400 14px/20px 'Creato Display','Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased}` +
22
- // The design system's cut-corner frame (styles/invited.css, .iv-dialog): a
23
- // 1px frame in the line colour, and the surface inside it cut 2 − √2 of a
24
- // pixel less, so the diagonal frame is as thick as the straight one.
25
- `main{width:100%;max-width:440px;padding:1px;background:#4a4a4a;` +
26
- `clip-path:polygon(0 0,calc(100% - 16px) 0,100% 16px,100% 100%,16px 100%,0 calc(100% - 16px))}` +
27
- `main>div{padding:40px 36px 36px;background:#1c1c1c;` +
28
- `clip-path:polygon(0 0,calc(100% - 15.414px) 0,100% 15.414px,100% 100%,15.414px 100%,0 calc(100% - 15.414px))}` +
29
- `.lockup{display:block;width:clamp(70px,23vw,107px);height:auto;margin:0 0 28px;color:#f5f5f5}` +
30
- `.eyebrow{margin:0 0 12px;font:400 11px/16px 'B612 Mono',ui-monospace,Menlo,Consolas,monospace;letter-spacing:.08em;text-transform:uppercase;color:#bdbdbd}` +
31
- `.eyebrow b{font-weight:400;color:${status === 200 ? '#8fd3a4' : '#f0b36c'}}` +
32
- `h1{margin:0 0 10px;font:500 24px/30px 'Creato Display','Helvetica Neue',Arial,sans-serif;letter-spacing:-.3px;text-wrap:balance}` +
33
- `p{margin:0;color:#bdbdbd}` +
34
- `@media (max-width:480px){main>div{padding:28px 24px 24px}}`;
35
- const body = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
36
- `<title>${escape(title)}</title><style nonce="${nonce}">${style}</style></head>` +
37
- `<body><main><div>${LOCKUP.replace('<svg ', '<svg class="lockup" ')}` +
38
- `<p class="eyebrow">Invited CLI · <b>${tone}</b></p>` +
39
- `<h1>${escape(title)}</h1><p>${escape(message)}</p></div></main></body></html>`;
3
+ /** Where the browser goes once it has come back to the CLI: Invited's own page for it (since 0.2.1; owner, 8 November
4
+ * 2026: "align that styling with the new pages"), the log-in screens' frame with the clouds, which says in a line
5
+ * what the terminal says in full. The address carries only that word, and no referrer goes with it, so Invited never
6
+ * sees the code. */
7
+ function sendOn(response, issuer, result, after) {
8
+ response.writeHead(303, {
9
+ Location: `${issuer}/auth/cli?result=${result}`,
10
+ 'Cache-Control': 'no-store',
11
+ 'Referrer-Policy': 'no-referrer',
12
+ Connection: 'close',
13
+ });
14
+ response.end(after);
15
+ }
16
+ /** A request that is no log-in's answer (another host, method or path): a line of text, and nowhere to go. */
17
+ function refuse(response, status, message) {
40
18
  response.writeHead(status, {
41
- 'Content-Type': 'text/html; charset=utf-8',
19
+ 'Content-Type': 'text/plain; charset=utf-8',
42
20
  'Cache-Control': 'no-store',
43
- 'Content-Security-Policy': `default-src 'none'; style-src 'nonce-${nonce}'`,
44
21
  'X-Content-Type-Options': 'nosniff',
45
- 'Referrer-Policy': 'no-referrer',
46
22
  Connection: 'close',
47
23
  });
48
- response.end(body, after);
24
+ response.end(message);
49
25
  }
50
26
  export async function listenForCallback(options) {
51
27
  let settle;
@@ -56,39 +32,39 @@ export async function listenForCallback(options) {
56
32
  const server = createServer((request, response) => {
57
33
  const host = `127.0.0.1:${port}`;
58
34
  if (finished) {
59
- // The same answer again (a browser retrying its navigation) sees the
60
- // same page; anything else learns the sign-in is over.
35
+ // The same answer again (a browser retrying its navigation) goes where
36
+ // it went; anything else learns the log-in has ended.
61
37
  const again = new URL(request.url ?? '/', `http://${host}`);
62
38
  if (answered && again.pathname === '/callback' && again.searchParams.get('state') === options.state)
63
- return page(response, answered.status, answered.title, answered.message);
64
- return page(response, 410, 'Already finished', 'This log-in has finished. Return to the terminal.');
39
+ return sendOn(response, options.issuer, answered);
40
+ return sendOn(response, options.issuer, 'ended');
65
41
  }
66
42
  if (request.headers.host !== host)
67
- return page(response, 400, 'Not this log-in', 'This address is only for the Invited CLI on this computer.');
43
+ return refuse(response, 400, 'This address is only for the Invited CLI on this computer.');
68
44
  if (request.method !== 'GET')
69
- return page(response, 405, 'Not allowed', 'Return to the terminal.');
45
+ return refuse(response, 405, 'Not allowed. Return to the terminal.');
70
46
  const url = new URL(request.url ?? '/', `http://${host}`);
71
47
  if (url.pathname !== '/callback')
72
- return page(response, 404, 'Not found', 'Return to the terminal.');
48
+ return refuse(response, 404, 'Not found. Return to the terminal.');
73
49
  const query = url.searchParams;
74
- // Another request (a stray tab, a page probing ports) changes nothing.
50
+ // Another log-in's answer (a stray tab) changes nothing.
75
51
  if (query.getAll('state').length !== 1 || query.get('state') !== options.state)
76
- return page(response, 400, 'Not this log-in', 'This answer does not belong to the log-in the terminal started. Return to the terminal.');
77
- const finish = (status, title, message, result) => {
52
+ return sendOn(response, options.issuer, 'ended');
53
+ const finish = (result, outcome) => {
78
54
  finished = true;
79
- answered = { status, title, message };
80
- page(response, status, title, message, () => settle(result));
55
+ answered = result;
56
+ sendOn(response, options.issuer, result, () => settle(outcome));
81
57
  };
82
58
  const iss = query.get('iss');
83
59
  if ((options.requireIss && iss === null) || (iss !== null && iss !== options.issuer))
84
- return finish(400, 'Refused', 'The answer did not come from Invited. Return to the terminal.', {
60
+ return finish('failed', {
85
61
  ok: false,
86
62
  message: `The browser's answer came from ${iss ?? 'an unnamed server'}, not ${options.issuer}, so it was refused. Run \`invited login\` again.`,
87
63
  });
88
64
  const error = query.get('error');
89
65
  if (error !== null) {
90
66
  const description = query.get('error_description');
91
- return finish(400, 'Not connected', 'The Invited CLI was not connected. Return to the terminal.', {
67
+ return finish(error === 'access_denied' ? 'cancelled' : 'failed', {
92
68
  ok: false,
93
69
  message: error === 'access_denied'
94
70
  ? 'You cancelled the connection in your browser, so nothing was connected.'
@@ -97,14 +73,11 @@ export async function listenForCallback(options) {
97
73
  }
98
74
  const code = query.get('code');
99
75
  if (!code)
100
- return finish(400, 'Not connected', 'The answer had no code. Return to the terminal.', {
76
+ return finish('failed', {
101
77
  ok: false,
102
78
  message: 'The browser came back without a log-in code. Run `invited login` again.',
103
79
  });
104
- return finish(200, 'Invited CLI connected', 'You can close this tab and return to the terminal.', {
105
- ok: true,
106
- code,
107
- });
80
+ return finish('connected', { ok: true, code });
108
81
  });
109
82
  await new Promise((resolve, reject) => {
110
83
  server.once('error', reject);
@@ -122,9 +95,9 @@ export async function listenForCallback(options) {
122
95
  });
123
96
  close();
124
97
  }, options.timeoutMs);
125
- /** Once the browser has been answered, closing lingers a moment: the page
126
- * finishes reaching the browser (it was once cut off mid-response), and a
127
- * browser that retries its navigation gets the same page again instead of
98
+ /** Once the browser has been answered, closing lingers a moment: the answer
99
+ * finishes reaching the browser (a page was once cut off mid-response), and
100
+ * a browser that retries its navigation is sent on again instead of seeing
128
101
  * "This site can't be reached". The time limit closes at once. */
129
102
  function close(force = false) {
130
103
  clearTimeout(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invitedlabs/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The invited command: connect terminal agents and AI assistants to your Invited Identity through Invited's MCP connector.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://invited.you",