@flowapt/flowiq-cli 0.1.10 → 0.1.11
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 +24 -4
- package/package.json +1 -1
- package/src/commands/auth.js +152 -17
- package/src/index.js +6 -2
package/README.md
CHANGED
|
@@ -16,12 +16,24 @@ flowiq --version # should print 0.1.x
|
|
|
16
16
|
|
|
17
17
|
## Authenticate
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
string). Then:
|
|
19
|
+
Default flow — browser login (like `npm login`):
|
|
21
20
|
|
|
22
21
|
```bash
|
|
23
22
|
flowiq auth login
|
|
24
|
-
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The CLI shows a short code (e.g. `FQ4X-9KTB`), opens
|
|
26
|
+
`https://app.flowiq.live/cli-auth` when you press ENTER, and waits. A
|
|
27
|
+
logged-in FlowIQ **super admin** confirms the code on the page matches the
|
|
28
|
+
one in your terminal and clicks Approve — the CLI then receives a
|
|
29
|
+
freshly-minted personal key automatically. **Never approve a code you
|
|
30
|
+
didn't just see in your own terminal.**
|
|
31
|
+
|
|
32
|
+
Fallbacks (CI / no browser):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
flowiq auth login --token fiq_staff_… # supply a key directly
|
|
36
|
+
flowiq auth login --paste # prompt for a key on stdin
|
|
25
37
|
```
|
|
26
38
|
|
|
27
39
|
Your key is saved to `~/.config/flowiq/auth.json` with `chmod 600`. To
|
|
@@ -31,8 +43,16 @@ verify what you're authenticated as:
|
|
|
31
43
|
flowiq auth whoami
|
|
32
44
|
```
|
|
33
45
|
|
|
46
|
+
To rotate this device's token (mints a fresh key, revokes the old one
|
|
47
|
+
server-side — run whenever you want a new secret):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
flowiq auth refresh
|
|
51
|
+
```
|
|
52
|
+
|
|
34
53
|
To log out (deletes the local key file, does NOT revoke the key
|
|
35
|
-
server-side —
|
|
54
|
+
server-side — use `flowiq auth refresh` first if you want the old key
|
|
55
|
+
dead, or ask another super admin to revoke):
|
|
36
56
|
|
|
37
57
|
```bash
|
|
38
58
|
flowiq auth logout
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Command-line tool for FlowIQ staff: round-trip agent prompts, questionnaires, fine-tuning, pin-board tasks, webhooks, templates, agent-updates, chat exports, and live agent testing without ever touching service-role credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/auth.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
// `flowiq auth login` / `
|
|
2
|
-
//
|
|
1
|
+
// `flowiq auth login` / `logout` / `whoami` / `refresh`.
|
|
2
|
+
//
|
|
3
|
+
// Default login = browser device flow (like `npm login` / `gh auth login`):
|
|
4
|
+
// 1. POST /cli/login-start (unauthenticated) → device_code + user_code
|
|
5
|
+
// 2. Show the user code, press ENTER → open https://app.flowiq.live/cli-auth
|
|
6
|
+
// 3. A logged-in SUPER ADMIN confirms the codes match and approves
|
|
7
|
+
// 4. Poll /cli/login-poll until the key is minted server-side (mint-at-claim)
|
|
8
|
+
//
|
|
9
|
+
// Fallbacks: `--token <key>` (direct) or `--paste` (interactive paste) keep
|
|
10
|
+
// the original flow for CI or when no browser is handy.
|
|
11
|
+
//
|
|
12
|
+
// `flowiq auth refresh` rotates the saved key in place via /cli/token-rotate —
|
|
13
|
+
// run it on each device whenever you want a fresh secret; the old key is
|
|
14
|
+
// revoked server-side. If the saved key is already dead it falls back to the
|
|
15
|
+
// full browser login.
|
|
3
16
|
|
|
4
17
|
import readline from "node:readline";
|
|
5
|
-
import
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { loadConfig, saveConfig, clearConfig } from "../config.js";
|
|
6
21
|
import { http } from "../http.js";
|
|
7
22
|
|
|
8
23
|
function prompt(question, { silent = false } = {}) {
|
|
@@ -41,25 +56,42 @@ function prompt(question, { silent = false } = {}) {
|
|
|
41
56
|
});
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
// Unauthenticated POST — login-start / login-poll run BEFORE a token exists,
|
|
60
|
+
// so they can't go through http.js (which requires the saved token).
|
|
61
|
+
async function unauthedPost(apiUrl, endpoint, body) {
|
|
62
|
+
const url = `${apiUrl.replace(/\/$/, "")}/cli/${endpoint}`;
|
|
63
|
+
const resp = await fetch(url, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
66
|
+
body: JSON.stringify(body ?? {}),
|
|
67
|
+
});
|
|
68
|
+
const text = await resp.text();
|
|
69
|
+
let parsed;
|
|
70
|
+
try { parsed = text ? JSON.parse(text) : {}; } catch { parsed = { _raw: text }; }
|
|
71
|
+
if (!resp.ok) {
|
|
72
|
+
throw new Error(parsed.message || parsed.error || `${endpoint} → HTTP ${resp.status}`);
|
|
53
73
|
}
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
function openBrowser(url) {
|
|
78
|
+
const platform = process.platform;
|
|
79
|
+
let cmd, args;
|
|
80
|
+
if (platform === "darwin") { cmd = "open"; args = [url]; }
|
|
81
|
+
else if (platform === "win32") { cmd = "cmd"; args = ["/c", "start", "", url]; }
|
|
82
|
+
else { cmd = "xdg-open"; args = [url]; }
|
|
83
|
+
try {
|
|
84
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
58
88
|
}
|
|
89
|
+
}
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
const file = await saveConfig({ api_url: cfg.api_url, token });
|
|
91
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
62
92
|
|
|
93
|
+
async function saveAndVerify(apiUrl, token) {
|
|
94
|
+
const file = await saveConfig({ api_url: apiUrl, token });
|
|
63
95
|
try {
|
|
64
96
|
const me = await http.get("whoami");
|
|
65
97
|
console.log(`Logged in as ${me.user_email || me.user_id} (super_admin=${me.is_super_admin})`);
|
|
@@ -73,6 +105,109 @@ export async function login(opts = {}) {
|
|
|
73
105
|
}
|
|
74
106
|
}
|
|
75
107
|
|
|
108
|
+
async function browserLogin(cfg) {
|
|
109
|
+
const deviceName = `${os.hostname()} (${os.platform()})`;
|
|
110
|
+
let start;
|
|
111
|
+
try {
|
|
112
|
+
start = await unauthedPost(cfg.api_url, "login-start", { device_name: deviceName });
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(`Could not start browser login: ${e.message}`);
|
|
115
|
+
console.error("Fallback: flowiq auth login --paste");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log("");
|
|
120
|
+
console.log("First, confirm this code matches the one shown in your browser:");
|
|
121
|
+
console.log("");
|
|
122
|
+
console.log(` ${start.user_code}`);
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log(`Approval page: ${start.verification_url}`);
|
|
125
|
+
await prompt("Press ENTER to open in the browser... ");
|
|
126
|
+
if (!openBrowser(start.verification_url)) {
|
|
127
|
+
console.log("(Could not open a browser — visit the URL above manually.)");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const intervalMs = Math.max(2, start.interval || 5) * 1000;
|
|
131
|
+
const deadline = Date.now() + (start.expires_in || 600) * 1000;
|
|
132
|
+
process.stdout.write("Waiting for a super admin to approve");
|
|
133
|
+
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
await sleep(intervalMs);
|
|
136
|
+
let poll;
|
|
137
|
+
try {
|
|
138
|
+
poll = await unauthedPost(cfg.api_url, "login-poll", { device_code: start.device_code });
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error(`\nPolling failed: ${e.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (poll.status === "pending") { process.stdout.write("."); continue; }
|
|
145
|
+
if (poll.status === "slow_down") { await sleep(intervalMs); continue; }
|
|
146
|
+
process.stdout.write("\n");
|
|
147
|
+
|
|
148
|
+
if (poll.status === "approved") {
|
|
149
|
+
console.log(`Approved${poll.user_email ? ` by ${poll.user_email}` : ""}.`);
|
|
150
|
+
await saveAndVerify(cfg.api_url, poll.token);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (poll.status === "denied") { console.error("Login was DENIED in the browser."); process.exit(1); }
|
|
154
|
+
if (poll.status === "expired") { console.error("Login request expired. Re-run `flowiq auth login`."); process.exit(1); }
|
|
155
|
+
if (poll.status === "claimed") { console.error("This login was already claimed. Re-run `flowiq auth login`."); process.exit(1); }
|
|
156
|
+
console.error(`Unexpected status: ${poll.status}`); process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.error("\nTimed out waiting for approval. Re-run `flowiq auth login`.");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function login(opts = {}) {
|
|
164
|
+
const cfg = await loadConfig();
|
|
165
|
+
if (opts.apiUrl) cfg.api_url = opts.apiUrl;
|
|
166
|
+
|
|
167
|
+
// Fallback paths: --token (direct) or --paste (interactive paste).
|
|
168
|
+
let token = opts.token;
|
|
169
|
+
if (!token && opts.paste) {
|
|
170
|
+
process.stdout.write(`API URL: ${cfg.api_url}\n`);
|
|
171
|
+
token = (await prompt("Paste your staff API key (fiq_staff_…): ", { silent: true })).trim();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (token) {
|
|
175
|
+
if (!token.startsWith("fiq_staff_")) {
|
|
176
|
+
console.error(`Error: token must start with "fiq_staff_" (got ${token.slice(0, 12)}…).`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
await saveAndVerify(cfg.api_url, token);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Default: browser device flow.
|
|
184
|
+
await browserLogin(cfg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function refresh() {
|
|
188
|
+
const cfg = await loadConfig();
|
|
189
|
+
if (!cfg.token) {
|
|
190
|
+
console.log("No saved token — starting browser login instead.");
|
|
191
|
+
await browserLogin(cfg);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const rotated = await http.post("token-rotate", {});
|
|
197
|
+
await saveConfig({ api_url: cfg.api_url, token: rotated.token });
|
|
198
|
+
console.log(`Token rotated (${rotated.rotated_from} → ${rotated.key_prefix}).`);
|
|
199
|
+
console.log(rotated.old_key_revoked ? "Old key revoked." : "WARNING: old key could not be revoked — revoke it manually.");
|
|
200
|
+
} catch (e) {
|
|
201
|
+
if (e.status === 401) {
|
|
202
|
+
console.log("Saved token is no longer valid — starting browser login…");
|
|
203
|
+
await browserLogin(cfg);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
console.error(`Refresh failed: ${e.message}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
76
211
|
export async function logout() {
|
|
77
212
|
const removed = await clearConfig();
|
|
78
213
|
console.log(removed ? "Logged out." : "Already logged out.");
|
package/src/index.js
CHANGED
|
@@ -40,10 +40,14 @@ export function run(argv) {
|
|
|
40
40
|
// auth
|
|
41
41
|
const auth = program.command("auth").description("Manage CLI authentication");
|
|
42
42
|
auth.command("login")
|
|
43
|
-
.description("
|
|
44
|
-
.option("--token <token>", "supply
|
|
43
|
+
.description("Log in via the browser (super-admin approval); --token/--paste for the old key-paste flow")
|
|
44
|
+
.option("--token <token>", "supply a staff API key directly instead of the browser flow")
|
|
45
|
+
.option("--paste", "prompt for a staff API key on stdin instead of the browser flow")
|
|
45
46
|
.option("--api-url <url>", "override the API host (default https://api.flowiq.live)")
|
|
46
47
|
.action((opts) => authCmd.login(opts));
|
|
48
|
+
auth.command("refresh")
|
|
49
|
+
.description("Rotate this device's token in place (old key is revoked server-side)")
|
|
50
|
+
.action(() => authCmd.refresh());
|
|
47
51
|
auth.command("logout").description("Remove the saved token").action(() => authCmd.logout());
|
|
48
52
|
auth.command("whoami").description("Show the user behind the saved token").action(() => authCmd.whoami());
|
|
49
53
|
|