@vex-chat/cli 0.4.0 → 0.5.0
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 +11 -5
- package/package.json +2 -2
- package/src/vex-chat.js +32 -187
package/README.md
CHANGED
|
@@ -6,12 +6,18 @@ Terminal chat client for Vex backed by `@vex-chat/libvex`.
|
|
|
6
6
|
pnpm vex
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
On first run, `vex` prompts for a username and registers a local
|
|
10
|
-
|
|
9
|
+
On first run, `vex` prompts for a username and password, registers a local
|
|
10
|
+
device key, and opens straight into a live chat session.
|
|
11
11
|
|
|
12
12
|
By default the CLI connects to production at `api.vex.wtf`. For local Spire development, pass `--local` to use `127.0.0.1:16777` over http/ws. Custom targets can use `--api-url <url>` to set both the host and protocol, or `--host <host:port> --http` to set them separately.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
New accounts use the password you set during registration. Passkeys can be added
|
|
15
|
+
later as a supplemental recovery/sign-in method. When adding this machine to an
|
|
16
|
+
account that already exists, the browser passkey page can restore the pending
|
|
17
|
+
device with a saved passkey. Use `--passkey-url <url>` or
|
|
18
|
+
`VEX_CHAT_PASSKEY_URL` when the WebAuthn page is hosted somewhere other than the
|
|
19
|
+
API origin; use `--no-browser` or `VEX_CHAT_NO_BROWSER=1` to print the URL
|
|
20
|
+
without launching a browser.
|
|
15
21
|
|
|
16
22
|
Inside the app:
|
|
17
23
|
|
|
@@ -43,8 +49,8 @@ Inside the app:
|
|
|
43
49
|
Scriptable helpers still exist:
|
|
44
50
|
|
|
45
51
|
```sh
|
|
46
|
-
pnpm vex auth register alice
|
|
47
|
-
pnpm vex auth register bob
|
|
52
|
+
pnpm vex auth register alice alice-password
|
|
53
|
+
pnpm vex auth register bob bob-password
|
|
48
54
|
pnpm vex alice
|
|
49
55
|
pnpm vex bob
|
|
50
56
|
pnpm vex auth accounts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Terminal client for vex-chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"better-sqlite3": "11.10.0",
|
|
33
33
|
"msgpackr": "^1.11.9",
|
|
34
|
-
"@vex-chat/libvex": "^
|
|
34
|
+
"@vex-chat/libvex": "^8.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.6.0",
|
package/src/vex-chat.js
CHANGED
|
@@ -593,96 +593,6 @@ function isRemovedStoredDeviceError(err) {
|
|
|
593
593
|
return err?.name === "RemovedStoredDeviceError";
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
function getClientBearerToken(client) {
|
|
597
|
-
if (typeof client.token === "string" && client.token.length > 0) {
|
|
598
|
-
return client.token;
|
|
599
|
-
}
|
|
600
|
-
const authorization =
|
|
601
|
-
client.http?.defaults?.headers?.common?.Authorization ??
|
|
602
|
-
client.http?.defaults?.headers?.common?.authorization;
|
|
603
|
-
if (typeof authorization !== "string") {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
const match = /^Bearer\s+(.+)$/i.exec(authorization.trim());
|
|
607
|
-
return match?.[1] ?? null;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function responseBodyMessage(data) {
|
|
611
|
-
if (!data || typeof data !== "object" || ArrayBuffer.isView(data)) {
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
if (data instanceof ArrayBuffer) {
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
const error = data.error;
|
|
618
|
-
if (typeof error === "string") return error;
|
|
619
|
-
if (error && typeof error === "object") {
|
|
620
|
-
const nestedMessage = error.message;
|
|
621
|
-
if (typeof nestedMessage === "string") return nestedMessage;
|
|
622
|
-
}
|
|
623
|
-
const message = data.message;
|
|
624
|
-
return typeof message === "string" ? message : null;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function bytesFromResponseBody(data) {
|
|
628
|
-
if (ArrayBuffer.isView(data)) {
|
|
629
|
-
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
630
|
-
}
|
|
631
|
-
if (data instanceof ArrayBuffer) {
|
|
632
|
-
return new Uint8Array(data);
|
|
633
|
-
}
|
|
634
|
-
return null;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function decodeBinaryResponseBody(data) {
|
|
638
|
-
const bytes = bytesFromResponseBody(data);
|
|
639
|
-
if (!bytes) return null;
|
|
640
|
-
try {
|
|
641
|
-
return unpack(bytes);
|
|
642
|
-
} catch {
|
|
643
|
-
// Express JSON error bodies also arrive as ArrayBuffer with fetch.
|
|
644
|
-
}
|
|
645
|
-
const text = new TextDecoder("utf-8", { fatal: false })
|
|
646
|
-
.decode(bytes)
|
|
647
|
-
.trim();
|
|
648
|
-
if (!text) return null;
|
|
649
|
-
if (text.startsWith("{") || text.startsWith("[")) {
|
|
650
|
-
try {
|
|
651
|
-
return JSON.parse(text);
|
|
652
|
-
} catch {
|
|
653
|
-
return text;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
return text;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function errorResponseMessage(err) {
|
|
660
|
-
const data = err?.response?.data;
|
|
661
|
-
const binaryBody = decodeBinaryResponseBody(data);
|
|
662
|
-
const binaryMessage = responseBodyMessage(binaryBody);
|
|
663
|
-
if (binaryMessage) return binaryMessage;
|
|
664
|
-
if (typeof binaryBody === "string") return binaryBody;
|
|
665
|
-
const objectMessage = responseBodyMessage(data);
|
|
666
|
-
if (objectMessage) return objectMessage;
|
|
667
|
-
if (typeof data === "string") {
|
|
668
|
-
try {
|
|
669
|
-
const parsed = JSON.parse(data);
|
|
670
|
-
const jsonMessage = responseBodyMessage(parsed);
|
|
671
|
-
if (jsonMessage) return jsonMessage;
|
|
672
|
-
} catch {
|
|
673
|
-
return data;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
return err instanceof Error ? err.message : String(err ?? "");
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function isInitialPasskeySetupRequired(err) {
|
|
680
|
-
if (err?.response?.status !== 403) return false;
|
|
681
|
-
return /passkey must be registered|passkey setup is required/i.test(
|
|
682
|
-
errorResponseMessage(err),
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
596
|
async function removeStoredDeviceAccount(ctx, config, accountRef) {
|
|
687
597
|
delete config.accounts[accountRef.key];
|
|
688
598
|
if (config.lastUsername === accountRef.key) {
|
|
@@ -697,11 +607,34 @@ function removedStoredDeviceError(ctx, username) {
|
|
|
697
607
|
return err;
|
|
698
608
|
}
|
|
699
609
|
|
|
610
|
+
async function resolveRegistrationPassword(ctx, args, rl = null) {
|
|
611
|
+
const provided = args[1] ?? ctx.password;
|
|
612
|
+
if (typeof provided === "string" && provided.trim().length > 0) {
|
|
613
|
+
return provided;
|
|
614
|
+
}
|
|
615
|
+
if (input.isTTY && output.isTTY) {
|
|
616
|
+
if (rl) {
|
|
617
|
+
const password = await askText(rl, "password");
|
|
618
|
+
if (password) return password;
|
|
619
|
+
} else {
|
|
620
|
+
const prompt = createInterface({ input, output });
|
|
621
|
+
try {
|
|
622
|
+
const password = await askText(prompt, "password");
|
|
623
|
+
if (password) return password;
|
|
624
|
+
} finally {
|
|
625
|
+
prompt.close();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
throw new Error(
|
|
630
|
+
"Password is required to register a new account. Usage: vex auth register <username> <password> or pass --password <password>.",
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
700
634
|
async function register(ctx, args) {
|
|
701
635
|
const requestedUsername = args[0] ?? ctx.username;
|
|
702
|
-
const password = args[1] ?? ctx.password;
|
|
703
636
|
if (!requestedUsername) {
|
|
704
|
-
throw new Error("Usage: vex-chat register <username>
|
|
637
|
+
throw new Error("Usage: vex-chat register <username> <password>");
|
|
705
638
|
}
|
|
706
639
|
const config = await readConfig(ctx.configPath);
|
|
707
640
|
const accountRef = resolveAccountEntry(ctx, config, requestedUsername);
|
|
@@ -713,6 +646,7 @@ async function register(ctx, args) {
|
|
|
713
646
|
`Local account already exists for ${username}. Use login or remove it from ${ctx.configPath}.`,
|
|
714
647
|
);
|
|
715
648
|
}
|
|
649
|
+
const password = await resolveRegistrationPassword(ctx, args);
|
|
716
650
|
const privateKey = Client.generateSecretKey();
|
|
717
651
|
const client = await Client.create(privateKey, ctx.clientOptions);
|
|
718
652
|
attachDebugClientEvents(ctx, client, `register:${username}`);
|
|
@@ -800,20 +734,6 @@ function buildPasskeyLoginUrl(ctx, { code, pending, username }) {
|
|
|
800
734
|
return url.toString();
|
|
801
735
|
}
|
|
802
736
|
|
|
803
|
-
function buildPasskeyRegistrationUrl(ctx, { token, userID, username }) {
|
|
804
|
-
const url = new URL(ctx.passkeyLoginUrl ?? defaultPasskeyLoginUrl(ctx));
|
|
805
|
-
url.hash = new URLSearchParams({
|
|
806
|
-
api: apiBaseUrl(ctx),
|
|
807
|
-
device: ctx.clientOptions.deviceName ?? "vex-chat-cli",
|
|
808
|
-
mode: "register",
|
|
809
|
-
name: ctx.clientOptions.deviceName ?? "vex-chat-cli",
|
|
810
|
-
token,
|
|
811
|
-
user: userID,
|
|
812
|
-
username,
|
|
813
|
-
}).toString();
|
|
814
|
-
return url.toString();
|
|
815
|
-
}
|
|
816
|
-
|
|
817
737
|
async function openExternalUrl(url) {
|
|
818
738
|
const [command, args] =
|
|
819
739
|
process.platform === "darwin"
|
|
@@ -863,68 +783,6 @@ async function launchPasskeyLogin(ctx, username, pending, code) {
|
|
|
863
783
|
}
|
|
864
784
|
}
|
|
865
785
|
|
|
866
|
-
async function launchInitialPasskeySetup(ctx, client, username) {
|
|
867
|
-
const token = getClientBearerToken(client);
|
|
868
|
-
if (!token) {
|
|
869
|
-
throw new Error(
|
|
870
|
-
"Passkey setup is required, but the CLI could not read the registration token.",
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
const userID = client.me.user().userID;
|
|
874
|
-
let url;
|
|
875
|
-
try {
|
|
876
|
-
url = buildPasskeyRegistrationUrl(ctx, {
|
|
877
|
-
token,
|
|
878
|
-
userID,
|
|
879
|
-
username,
|
|
880
|
-
});
|
|
881
|
-
} catch (err) {
|
|
882
|
-
throw new Error(
|
|
883
|
-
`Could not build passkey setup URL: ${err instanceof Error ? err.message : String(err)}`,
|
|
884
|
-
);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
console.log(
|
|
888
|
-
`${color(ROOT_ACCENT, "passkey setup")} ${color("dim", "opens in your browser")}`,
|
|
889
|
-
);
|
|
890
|
-
console.log(
|
|
891
|
-
color(
|
|
892
|
-
"dim",
|
|
893
|
-
"New accounts must add a passkey before the CLI can connect.",
|
|
894
|
-
),
|
|
895
|
-
);
|
|
896
|
-
console.log(`${color("dim", "url")} ${url}`);
|
|
897
|
-
if (ctx.openBrowser) {
|
|
898
|
-
const opened = await openExternalUrl(url);
|
|
899
|
-
if (!opened) {
|
|
900
|
-
console.log(
|
|
901
|
-
color(
|
|
902
|
-
"yellow",
|
|
903
|
-
"Could not open a browser. Copy the URL above.",
|
|
904
|
-
),
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
console.log(color(ROOT_ACCENT, "waiting for passkey setup..."));
|
|
910
|
-
for (let attempt = 0; attempt < 300; attempt++) {
|
|
911
|
-
await sleep(2000);
|
|
912
|
-
const passkeys = await client.passkeys.list().catch((err) => {
|
|
913
|
-
debugLog(ctx, "passkey.register.poll.error", {
|
|
914
|
-
error: err,
|
|
915
|
-
username,
|
|
916
|
-
});
|
|
917
|
-
return [];
|
|
918
|
-
});
|
|
919
|
-
if (passkeys.length > 0) {
|
|
920
|
-
if (input.isTTY && output.isTTY) output.write("\n");
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
if (input.isTTY && output.isTTY) output.write(color("dim", "."));
|
|
924
|
-
}
|
|
925
|
-
throw new Error("Timed out waiting for passkey setup.");
|
|
926
|
-
}
|
|
927
|
-
|
|
928
786
|
async function persistPendingLocalAccount(
|
|
929
787
|
ctx,
|
|
930
788
|
config,
|
|
@@ -1275,7 +1133,7 @@ async function authCommand(ctx, args) {
|
|
|
1275
1133
|
return;
|
|
1276
1134
|
default:
|
|
1277
1135
|
throw new Error(
|
|
1278
|
-
"Usage: vex auth register <username> | login <username> [password] | requests | use <username> | accounts | status",
|
|
1136
|
+
"Usage: vex auth register <username> <password> | login <username> [password] | requests | use <username> | accounts | status",
|
|
1279
1137
|
);
|
|
1280
1138
|
}
|
|
1281
1139
|
}
|
|
@@ -3417,11 +3275,12 @@ async function authenticateOrRegister(ctx, explicitUsername) {
|
|
|
3417
3275
|
if (answer && answer !== "y" && answer !== "yes") {
|
|
3418
3276
|
throw new Error("No local account selected.");
|
|
3419
3277
|
}
|
|
3278
|
+
const password = await resolveRegistrationPassword(ctx, [entered], rl);
|
|
3420
3279
|
const privateKey = Client.generateSecretKey();
|
|
3421
3280
|
const client = await Client.create(privateKey, ctx.clientOptions);
|
|
3422
3281
|
attachDebugClientEvents(ctx, client, `register:${entered}`);
|
|
3423
3282
|
try {
|
|
3424
|
-
const [, registerErr] = await client.register(entered);
|
|
3283
|
+
const [, registerErr] = await client.register(entered, password);
|
|
3425
3284
|
if (registerErr) throw registerErr;
|
|
3426
3285
|
await persistNewLocalAccount(
|
|
3427
3286
|
ctx,
|
|
@@ -3474,21 +3333,7 @@ async function connectAndWait(client, ctx = null, label = "client") {
|
|
|
3474
3333
|
debugLog(ctx, "client.connect.skip.connected", { label });
|
|
3475
3334
|
return;
|
|
3476
3335
|
}
|
|
3477
|
-
|
|
3478
|
-
await connectOnceAndWait(client, ctx, label);
|
|
3479
|
-
return;
|
|
3480
|
-
} catch (err) {
|
|
3481
|
-
if (!ctx || !isInitialPasskeySetupRequired(err)) {
|
|
3482
|
-
throw err;
|
|
3483
|
-
}
|
|
3484
|
-
const username = client.me.user().username ?? "account";
|
|
3485
|
-
debugLog(ctx, "client.connect.passkeySetup.required", {
|
|
3486
|
-
label,
|
|
3487
|
-
username,
|
|
3488
|
-
});
|
|
3489
|
-
await launchInitialPasskeySetup(ctx, client, username);
|
|
3490
|
-
await connectOnceAndWait(client, ctx, `${label}:passkey`);
|
|
3491
|
-
}
|
|
3336
|
+
await connectOnceAndWait(client, ctx, label);
|
|
3492
3337
|
}
|
|
3493
3338
|
|
|
3494
3339
|
async function connectOnceAndWait(client, ctx = null, label = "client") {
|
|
@@ -4688,7 +4533,7 @@ Commands:
|
|
|
4688
4533
|
vex open the live terminal chat app
|
|
4689
4534
|
vex <username> open as a specific local user
|
|
4690
4535
|
vex chat [username] open the live terminal chat app
|
|
4691
|
-
vex auth register <username>
|
|
4536
|
+
vex auth register <username> <password>
|
|
4692
4537
|
vex auth login <username> request approval as a second device
|
|
4693
4538
|
vex auth requests list pending device login requests
|
|
4694
4539
|
vex auth accounts
|
|
@@ -4699,7 +4544,7 @@ Commands:
|
|
|
4699
4544
|
Flags:
|
|
4700
4545
|
--username <name> local account to use
|
|
4701
4546
|
--user <name> alias for --username
|
|
4702
|
-
--password <password> fallback password for login
|
|
4547
|
+
--password <password> fallback password for register/login
|
|
4703
4548
|
--api-url <url> API base URL, e.g. http://127.0.0.1:16777
|
|
4704
4549
|
--host <host:port> API host, default api.vex.wtf
|
|
4705
4550
|
--local connect to local Spire at 127.0.0.1:16777 over http/ws
|