@botdocs/cli 0.14.0 → 0.14.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/dist/commands/login.js +72 -14
- package/package.json +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import open from 'open';
|
|
4
5
|
import { render } from 'ink';
|
|
@@ -36,11 +37,22 @@ async function loginWithToken(token, syncLibrary) {
|
|
|
36
37
|
process.exit(1);
|
|
37
38
|
}
|
|
38
39
|
const baseUrl = getApiUrl();
|
|
40
|
+
// Stamp the device identity on the validating call so the token's device
|
|
41
|
+
// registers (and the free-tier device/session caps bind) the moment it's
|
|
42
|
+
// saved — not just on the next command. Best-effort: a failure to resolve
|
|
43
|
+
// the id never blocks login.
|
|
44
|
+
const { deviceId, machineName } = deviceIdentity();
|
|
45
|
+
const headers = {
|
|
46
|
+
Authorization: `Bearer ${token}`,
|
|
47
|
+
Accept: 'application/json',
|
|
48
|
+
};
|
|
49
|
+
if (deviceId)
|
|
50
|
+
headers['X-Device-Id'] = deviceId;
|
|
51
|
+
if (machineName)
|
|
52
|
+
headers['X-Machine-Name'] = machineName;
|
|
39
53
|
let res;
|
|
40
54
|
try {
|
|
41
|
-
res = await fetch(`${baseUrl}/api/cli/whoami`, {
|
|
42
|
-
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
43
|
-
});
|
|
55
|
+
res = await fetch(`${baseUrl}/api/cli/whoami`, { headers });
|
|
44
56
|
}
|
|
45
57
|
catch (err) {
|
|
46
58
|
console.error(`Failed to contact ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -70,21 +82,51 @@ async function loginWithToken(token, syncLibrary) {
|
|
|
70
82
|
process.exit(1);
|
|
71
83
|
}
|
|
72
84
|
const user = (await res.json());
|
|
85
|
+
// Persist the SAME device id we sent on the whoami header so the device that
|
|
86
|
+
// just registered server-side matches every subsequent request.
|
|
73
87
|
saveAuth({
|
|
74
88
|
token,
|
|
75
89
|
username: user.username,
|
|
76
90
|
displayName: user.displayName,
|
|
77
91
|
syncLibrary,
|
|
92
|
+
...persistedDeviceField(deviceId),
|
|
78
93
|
});
|
|
79
|
-
// Generate + persist the stable device identity now, so the very next
|
|
80
|
-
// authenticated request carries an X-Device-Id (no-op when BOTDOCS_DEVICE_ID
|
|
81
|
-
// is set — that override always wins and isn't persisted).
|
|
82
|
-
getOrCreateDeviceId();
|
|
83
94
|
printSignedIn(user.username, syncLibrary);
|
|
84
95
|
}
|
|
96
|
+
/** Best-effort stable device identity for this CLI install. Mirrors
|
|
97
|
+
* `attachDeviceHeaders` in lib/api.ts, but the login flows can't reuse it: they
|
|
98
|
+
* use raw `fetch` (there's no bearer to attach yet) and they ALSO need the id
|
|
99
|
+
* for the browser auth URL. Returns nulls if it can't be resolved (e.g. a
|
|
100
|
+
* read-only home dir) so login never hard-fails on device identity.
|
|
101
|
+
*
|
|
102
|
+
* Resolve this ONCE per login and thread the same id into both the request
|
|
103
|
+
* (header/URL) and `persistedDeviceField` — the server registers the device
|
|
104
|
+
* under the forwarded id, so the persisted id must match or the next command
|
|
105
|
+
* would present as a different device. */
|
|
106
|
+
function deviceIdentity() {
|
|
107
|
+
try {
|
|
108
|
+
return { deviceId: getOrCreateDeviceId(), machineName: os.hostname() };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { deviceId: null, machineName: null };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** The `auth.json` fragment carrying the device id to persist. `getOrCreateDeviceId`
|
|
115
|
+
* only persists when an auth file already exists, but at login time we're
|
|
116
|
+
* writing that file for the first time — so we fold the id straight into the
|
|
117
|
+
* `saveAuth` payload. A `BOTDOCS_DEVICE_ID` override always wins at read time
|
|
118
|
+
* and is intentionally NOT persisted (so auth.json stays portable across CI
|
|
119
|
+
* runners), so we omit it in that case. */
|
|
120
|
+
function persistedDeviceField(deviceId) {
|
|
121
|
+
if (process.env.BOTDOCS_DEVICE_ID?.trim())
|
|
122
|
+
return {};
|
|
123
|
+
return deviceId ? { deviceId } : {};
|
|
124
|
+
}
|
|
85
125
|
/** Helper: register a new state with the server and return the public auth
|
|
86
|
-
* URL plus the last-6-chars suffix the user can verify in their browser.
|
|
87
|
-
|
|
126
|
+
* URL plus the last-6-chars suffix the user can verify in their browser. The
|
|
127
|
+
* caller passes the resolved device identity so the SAME id is forwarded in
|
|
128
|
+
* the URL and persisted to auth.json. */
|
|
129
|
+
async function initLoginState(baseUrl, deviceId, machineName) {
|
|
88
130
|
const state = randomBytes(32).toString('hex');
|
|
89
131
|
const initRes = await fetch(`${baseUrl}/api/cli/auth/init`, {
|
|
90
132
|
method: 'POST',
|
|
@@ -118,9 +160,19 @@ async function initLoginState(baseUrl) {
|
|
|
118
160
|
// state carried in the URL hash fragment (#state=...), which browsers
|
|
119
161
|
// never send to the server and never write to history. That refactor is
|
|
120
162
|
// out of scope for the P3 bucket — tracked as a separate item.
|
|
163
|
+
//
|
|
164
|
+
// Forward this install's device identity in the URL. The browser that lands
|
|
165
|
+
// /cli-auth has no X-Device-Id header of its own, so the page relays these
|
|
166
|
+
// into the grant POST body — that's what registers the device + binds the
|
|
167
|
+
// free-tier caps at login time (rather than only on the next CLI command).
|
|
168
|
+
const params = new URLSearchParams({ state });
|
|
169
|
+
if (deviceId)
|
|
170
|
+
params.set('device', deviceId);
|
|
171
|
+
if (machineName)
|
|
172
|
+
params.set('machine', machineName);
|
|
121
173
|
return {
|
|
122
174
|
state,
|
|
123
|
-
authUrl: `${baseUrl}/cli-auth
|
|
175
|
+
authUrl: `${baseUrl}/cli-auth?${params.toString()}`,
|
|
124
176
|
tail: state.slice(-6),
|
|
125
177
|
};
|
|
126
178
|
}
|
|
@@ -131,6 +183,9 @@ async function initLoginState(baseUrl) {
|
|
|
131
183
|
* the component's effect call `useApp().exit()` to unmount. */
|
|
132
184
|
async function loginInkInteractive(syncLibrary) {
|
|
133
185
|
const baseUrl = getApiUrl();
|
|
186
|
+
// Resolve the device identity ONCE — forwarded to the browser via the auth
|
|
187
|
+
// URL and persisted (below) under the same id.
|
|
188
|
+
const { deviceId, machineName } = deviceIdentity();
|
|
134
189
|
let status = 'initializing';
|
|
135
190
|
let authUrl;
|
|
136
191
|
let stateTail;
|
|
@@ -164,7 +219,7 @@ async function loginInkInteractive(syncLibrary) {
|
|
|
164
219
|
// Phase 1: register state with the server.
|
|
165
220
|
let init;
|
|
166
221
|
try {
|
|
167
|
-
init = await initLoginState(baseUrl);
|
|
222
|
+
init = await initLoginState(baseUrl, deviceId, machineName);
|
|
168
223
|
}
|
|
169
224
|
catch (err) {
|
|
170
225
|
status = 'error';
|
|
@@ -201,8 +256,8 @@ async function loginInkInteractive(syncLibrary) {
|
|
|
201
256
|
username: granted.username,
|
|
202
257
|
displayName: granted.displayName,
|
|
203
258
|
syncLibrary,
|
|
259
|
+
...persistedDeviceField(deviceId),
|
|
204
260
|
});
|
|
205
|
-
getOrCreateDeviceId();
|
|
206
261
|
resolvedUsername = granted.username;
|
|
207
262
|
status = 'success';
|
|
208
263
|
rerender();
|
|
@@ -217,9 +272,12 @@ async function loginInkInteractive(syncLibrary) {
|
|
|
217
272
|
* polling indicator; the spinner degrades gracefully when stdout isn't a TTY. */
|
|
218
273
|
async function loginPlainInteractive(syncLibrary) {
|
|
219
274
|
const baseUrl = getApiUrl();
|
|
275
|
+
// Resolve the device identity ONCE — forwarded to the browser via the auth
|
|
276
|
+
// URL and persisted (below) under the same id.
|
|
277
|
+
const { deviceId, machineName } = deviceIdentity();
|
|
220
278
|
let init;
|
|
221
279
|
try {
|
|
222
|
-
init = await initLoginState(baseUrl);
|
|
280
|
+
init = await initLoginState(baseUrl, deviceId, machineName);
|
|
223
281
|
}
|
|
224
282
|
catch (err) {
|
|
225
283
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -250,8 +308,8 @@ async function loginPlainInteractive(syncLibrary) {
|
|
|
250
308
|
username: granted.username,
|
|
251
309
|
displayName: granted.displayName,
|
|
252
310
|
syncLibrary,
|
|
311
|
+
...persistedDeviceField(deviceId),
|
|
253
312
|
});
|
|
254
|
-
getOrCreateDeviceId();
|
|
255
313
|
printSyncLibraryHint(syncLibrary);
|
|
256
314
|
}
|
|
257
315
|
/** Polls the server with exponential backoff. Returns the granted payload on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botdocs/cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "CLI for BotDocs — author, publish, install, and sync agent skills across Claude, Claude Code, Cursor, Codex, ChatGPT, Windsurf, Copilot, Gemini, Antigravity, and OpenCode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"botdocs",
|