@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 +4 -0
- package/dist/loopback.js +36 -63
- package/package.json +1 -1
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
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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/
|
|
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(
|
|
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)
|
|
60
|
-
//
|
|
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
|
|
64
|
-
return
|
|
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
|
|
43
|
+
return refuse(response, 400, 'This address is only for the Invited CLI on this computer.');
|
|
68
44
|
if (request.method !== 'GET')
|
|
69
|
-
return
|
|
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
|
|
48
|
+
return refuse(response, 404, 'Not found. Return to the terminal.');
|
|
73
49
|
const query = url.searchParams;
|
|
74
|
-
// Another
|
|
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
|
|
77
|
-
const finish = (
|
|
52
|
+
return sendOn(response, options.issuer, 'ended');
|
|
53
|
+
const finish = (result, outcome) => {
|
|
78
54
|
finished = true;
|
|
79
|
-
answered =
|
|
80
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
126
|
-
* finishes reaching the browser (
|
|
127
|
-
* browser that retries its navigation
|
|
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.
|
|
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",
|