@vex-chat/cli 0.1.3 → 0.1.5
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/package.json +2 -2
- package/src/vex-chat.js +382 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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": "^6.
|
|
34
|
+
"@vex-chat/libvex": "^6.7.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.6.0",
|
package/src/vex-chat.js
CHANGED
|
@@ -359,14 +359,238 @@ function httpFromApiUrl(raw) {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
function normalizeAccountHost(host) {
|
|
363
|
+
return String(host ?? DEFAULT_HOST)
|
|
364
|
+
.trim()
|
|
365
|
+
.toLowerCase();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizeAccountName(value) {
|
|
369
|
+
return String(value ?? "")
|
|
370
|
+
.trim()
|
|
371
|
+
.toLowerCase();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function accountKeyFor(ctx, username) {
|
|
375
|
+
return `${normalizeAccountName(username)}@${normalizeAccountHost(ctx.clientOptions.host)}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function parseAccountSelector(ctx, value) {
|
|
379
|
+
const selector = normalizeAccountName(value);
|
|
380
|
+
const currentHost = normalizeAccountHost(ctx.clientOptions.host);
|
|
381
|
+
const at = selector.lastIndexOf("@");
|
|
382
|
+
if (at > 0) {
|
|
383
|
+
const host = selector.slice(at + 1);
|
|
384
|
+
return {
|
|
385
|
+
host,
|
|
386
|
+
hostMatches: host === currentHost,
|
|
387
|
+
key: selector,
|
|
388
|
+
scoped: true,
|
|
389
|
+
username: selector.slice(0, at),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
host: currentHost,
|
|
394
|
+
hostMatches: true,
|
|
395
|
+
key: `${selector}@${currentHost}`,
|
|
396
|
+
scoped: false,
|
|
397
|
+
username: selector,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function stripExpiredPendingApproval(account) {
|
|
402
|
+
const expiresAt = account?.pendingApproval?.expiresAt;
|
|
403
|
+
if (typeof expiresAt !== "string") return false;
|
|
404
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
405
|
+
if (Number.isFinite(expiresAtMs) && expiresAtMs > Date.now()) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
delete account.pendingApproval;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function normalizeStoredAccount(config, key, fallbackUsername) {
|
|
413
|
+
const account = config.accounts[key];
|
|
414
|
+
if (!account || typeof account !== "object") return false;
|
|
415
|
+
let changed = stripExpiredPendingApproval(account);
|
|
416
|
+
const username =
|
|
417
|
+
typeof account.username === "string" && account.username.trim()
|
|
418
|
+
? account.username.trim().toLowerCase()
|
|
419
|
+
: fallbackUsername;
|
|
420
|
+
if (account.username !== username) {
|
|
421
|
+
account.username = username;
|
|
422
|
+
changed = true;
|
|
423
|
+
}
|
|
424
|
+
return changed;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function removeUnusableAccount(config, key) {
|
|
428
|
+
const account = config.accounts[key];
|
|
429
|
+
if (!account || account.deviceID || account.pendingApproval) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
delete config.accounts[key];
|
|
433
|
+
if (config.lastUsername === key) {
|
|
434
|
+
delete config.lastUsername;
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function resolveAccountEntry(ctx, config, selector) {
|
|
440
|
+
const parsed = parseAccountSelector(ctx, selector);
|
|
441
|
+
let changed = false;
|
|
442
|
+
|
|
443
|
+
if (!parsed.username) {
|
|
444
|
+
return { ...parsed, account: null, changed };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const exact = config.accounts[parsed.key];
|
|
448
|
+
if (parsed.scoped) {
|
|
449
|
+
if (exact) {
|
|
450
|
+
changed =
|
|
451
|
+
normalizeStoredAccount(config, parsed.key, parsed.username) ||
|
|
452
|
+
changed;
|
|
453
|
+
if (removeUnusableAccount(config, parsed.key)) {
|
|
454
|
+
return { ...parsed, account: null, changed: true };
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
...parsed,
|
|
458
|
+
account: config.accounts[parsed.key],
|
|
459
|
+
changed,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return { ...parsed, account: null, changed };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const scopedKey = accountKeyFor(ctx, parsed.username);
|
|
466
|
+
const scoped = config.accounts[scopedKey];
|
|
467
|
+
if (scoped) {
|
|
468
|
+
changed =
|
|
469
|
+
normalizeStoredAccount(config, scopedKey, parsed.username) ||
|
|
470
|
+
changed;
|
|
471
|
+
if (removeUnusableAccount(config, scopedKey)) {
|
|
472
|
+
return {
|
|
473
|
+
...parsed,
|
|
474
|
+
account: null,
|
|
475
|
+
changed: true,
|
|
476
|
+
key: scopedKey,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const legacy = config.accounts[parsed.username];
|
|
480
|
+
if (legacy && stripExpiredPendingApproval(legacy)) {
|
|
481
|
+
delete config.accounts[parsed.username];
|
|
482
|
+
changed = true;
|
|
483
|
+
}
|
|
484
|
+
if (config.lastUsername === parsed.username) {
|
|
485
|
+
config.lastUsername = scopedKey;
|
|
486
|
+
changed = true;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
...parsed,
|
|
490
|
+
account: config.accounts[scopedKey],
|
|
491
|
+
changed,
|
|
492
|
+
key: scopedKey,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const legacy = config.accounts[parsed.username];
|
|
497
|
+
if (!legacy) {
|
|
498
|
+
return { ...parsed, account: null, changed, key: scopedKey };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
stripExpiredPendingApproval(legacy);
|
|
502
|
+
if (!legacy.deviceID && !legacy.pendingApproval) {
|
|
503
|
+
delete config.accounts[parsed.username];
|
|
504
|
+
if (config.lastUsername === parsed.username) {
|
|
505
|
+
delete config.lastUsername;
|
|
506
|
+
}
|
|
507
|
+
return { ...parsed, account: null, changed: true, key: scopedKey };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
config.accounts[scopedKey] = {
|
|
511
|
+
...legacy,
|
|
512
|
+
username:
|
|
513
|
+
typeof legacy.username === "string" && legacy.username.trim()
|
|
514
|
+
? legacy.username.trim().toLowerCase()
|
|
515
|
+
: parsed.username,
|
|
516
|
+
};
|
|
517
|
+
delete config.accounts[parsed.username];
|
|
518
|
+
if (config.lastUsername === parsed.username) {
|
|
519
|
+
config.lastUsername = scopedKey;
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
...parsed,
|
|
523
|
+
account: config.accounts[scopedKey],
|
|
524
|
+
changed: true,
|
|
525
|
+
key: scopedKey,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function writeConfigIfChanged(ctx, config, changed) {
|
|
530
|
+
if (changed) {
|
|
531
|
+
await writeConfig(ctx.configPath, config);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function assertAccountHostMatches(ctx, accountRef) {
|
|
536
|
+
if (!accountRef.scoped || accountRef.hostMatches) return;
|
|
537
|
+
const currentHost = normalizeAccountHost(ctx.clientOptions.host);
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Local account ${accountRef.key} is for ${accountRef.host}; current host is ${currentHost}. Pass --host ${accountRef.host}.`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function deleteLocalAccount(ctx, config, username) {
|
|
544
|
+
const { key } = parseAccountSelector(ctx, username);
|
|
545
|
+
delete config.accounts[key];
|
|
546
|
+
if (config.lastUsername === key) {
|
|
547
|
+
delete config.lastUsername;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function formatRemovedDeviceLoginHint(ctx, username) {
|
|
552
|
+
const host = normalizeAccountHost(ctx.clientOptions.host);
|
|
553
|
+
return `Local device for ${username}@${host} is no longer on the account. Run vex auth login ${username} --host ${host} to add this machine as a new device.`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function isMissingStoredDeviceError(err) {
|
|
557
|
+
if (err?.response?.status === 404) return true;
|
|
558
|
+
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
559
|
+
return /\b(?:http|status(?: code)?)?\s*404\b|404[^\n]*not found|not found[^\n]*404/i.test(
|
|
560
|
+
message,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function isRemovedStoredDeviceError(err) {
|
|
565
|
+
return err?.name === "RemovedStoredDeviceError";
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function removeStoredDeviceAccount(ctx, config, accountRef) {
|
|
569
|
+
delete config.accounts[accountRef.key];
|
|
570
|
+
if (config.lastUsername === accountRef.key) {
|
|
571
|
+
delete config.lastUsername;
|
|
572
|
+
}
|
|
573
|
+
await writeConfig(ctx.configPath, config);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function removedStoredDeviceError(ctx, username) {
|
|
577
|
+
const err = new Error(formatRemovedDeviceLoginHint(ctx, username));
|
|
578
|
+
err.name = "RemovedStoredDeviceError";
|
|
579
|
+
return err;
|
|
580
|
+
}
|
|
581
|
+
|
|
362
582
|
async function register(ctx, args) {
|
|
363
|
-
const
|
|
583
|
+
const requestedUsername = args[0] ?? ctx.username;
|
|
364
584
|
const password = args[1] ?? ctx.password;
|
|
365
|
-
if (!
|
|
585
|
+
if (!requestedUsername) {
|
|
366
586
|
throw new Error("Usage: vex-chat register <username> [password]");
|
|
367
587
|
}
|
|
368
588
|
const config = await readConfig(ctx.configPath);
|
|
369
|
-
|
|
589
|
+
const accountRef = resolveAccountEntry(ctx, config, requestedUsername);
|
|
590
|
+
await writeConfigIfChanged(ctx, config, accountRef.changed);
|
|
591
|
+
assertAccountHostMatches(ctx, accountRef);
|
|
592
|
+
const { username } = accountRef;
|
|
593
|
+
if (accountRef.account) {
|
|
370
594
|
throw new Error(
|
|
371
595
|
`Local account already exists for ${username}. Use login or remove it from ${ctx.configPath}.`,
|
|
372
596
|
);
|
|
@@ -414,15 +638,17 @@ async function persistNewLocalAccount(
|
|
|
414
638
|
client,
|
|
415
639
|
deviceID = client.me.device().deviceID,
|
|
416
640
|
) {
|
|
417
|
-
|
|
641
|
+
const accountRef = parseAccountSelector(ctx, username);
|
|
642
|
+
const storedUsername = client.me.user().username ?? accountRef.username;
|
|
643
|
+
config.accounts[accountRef.key] = {
|
|
418
644
|
deviceID,
|
|
419
645
|
privateKey,
|
|
420
646
|
userID: client.me.user().userID,
|
|
421
|
-
username,
|
|
647
|
+
username: storedUsername,
|
|
422
648
|
};
|
|
423
|
-
config.lastUsername =
|
|
649
|
+
config.lastUsername = accountRef.key;
|
|
424
650
|
await writeConfig(ctx.configPath, config);
|
|
425
|
-
return config.accounts[
|
|
651
|
+
return { ...config.accounts[accountRef.key], accountKey: accountRef.key };
|
|
426
652
|
}
|
|
427
653
|
|
|
428
654
|
async function persistPendingLocalAccount(
|
|
@@ -432,8 +658,9 @@ async function persistPendingLocalAccount(
|
|
|
432
658
|
privateKey,
|
|
433
659
|
pending,
|
|
434
660
|
) {
|
|
435
|
-
const
|
|
436
|
-
config.accounts[
|
|
661
|
+
const accountRef = parseAccountSelector(ctx, username);
|
|
662
|
+
const previous = config.accounts[accountRef.key] ?? {};
|
|
663
|
+
config.accounts[accountRef.key] = {
|
|
437
664
|
...previous,
|
|
438
665
|
privateKey,
|
|
439
666
|
pendingApproval: {
|
|
@@ -442,38 +669,38 @@ async function persistPendingLocalAccount(
|
|
|
442
669
|
requestID: pending.requestID,
|
|
443
670
|
},
|
|
444
671
|
...(pending.userID ? { userID: pending.userID } : {}),
|
|
445
|
-
username,
|
|
672
|
+
username: accountRef.username,
|
|
446
673
|
};
|
|
447
|
-
config.lastUsername =
|
|
674
|
+
config.lastUsername = accountRef.key;
|
|
448
675
|
await writeConfig(ctx.configPath, config);
|
|
449
|
-
return config.accounts[
|
|
676
|
+
return { ...config.accounts[accountRef.key], accountKey: accountRef.key };
|
|
450
677
|
}
|
|
451
678
|
|
|
452
679
|
async function login(ctx, args) {
|
|
453
|
-
const
|
|
680
|
+
const requestedUsername = args[0] ?? ctx.username;
|
|
454
681
|
const password = args[1] ?? ctx.password;
|
|
455
|
-
if (!
|
|
682
|
+
if (!requestedUsername) {
|
|
456
683
|
throw new Error("Usage: vex-chat login <username> [password]");
|
|
457
684
|
}
|
|
685
|
+
const { username } = parseAccountSelector(ctx, requestedUsername);
|
|
458
686
|
if (!password) {
|
|
459
|
-
await loginWithDeviceApproval(ctx,
|
|
687
|
+
await loginWithDeviceApproval(ctx, requestedUsername);
|
|
460
688
|
return;
|
|
461
689
|
}
|
|
462
|
-
const { client, config } = await makeClient(ctx,
|
|
690
|
+
const { client, config } = await makeClient(ctx, requestedUsername);
|
|
463
691
|
attachDebugClientEvents(ctx, client, `login:${username}`);
|
|
464
692
|
try {
|
|
465
693
|
const loginResult = await client.login(username, password);
|
|
466
694
|
if (!loginResult.ok)
|
|
467
695
|
throw new Error(loginResult.error ?? "Login failed.");
|
|
468
696
|
await connectAndWait(client, ctx, `login:${username}`);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
userID: client.me.user().userID,
|
|
697
|
+
await persistNewLocalAccount(
|
|
698
|
+
ctx,
|
|
699
|
+
config,
|
|
473
700
|
username,
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
701
|
+
client.getKeys().private,
|
|
702
|
+
client,
|
|
703
|
+
);
|
|
477
704
|
console.log(
|
|
478
705
|
`${color(ROOT_ACCENT, "logged in")} ${color(userAccent(client.me.user().userID), username)}`,
|
|
479
706
|
);
|
|
@@ -485,35 +712,60 @@ async function login(ctx, args) {
|
|
|
485
712
|
|
|
486
713
|
async function loginWithDeviceApproval(ctx, username) {
|
|
487
714
|
const config = await readConfig(ctx.configPath);
|
|
488
|
-
|
|
489
|
-
|
|
715
|
+
const accountRef = resolveAccountEntry(ctx, config, username);
|
|
716
|
+
await writeConfigIfChanged(ctx, config, accountRef.changed);
|
|
717
|
+
assertAccountHostMatches(ctx, accountRef);
|
|
718
|
+
if (accountRef.account) {
|
|
719
|
+
let existing;
|
|
490
720
|
try {
|
|
721
|
+
existing = await authenticate(ctx, accountRef.key);
|
|
722
|
+
} catch (err) {
|
|
723
|
+
if (!isRemovedStoredDeviceError(err)) {
|
|
724
|
+
throw err;
|
|
725
|
+
}
|
|
491
726
|
console.log(
|
|
492
|
-
|
|
727
|
+
color(
|
|
728
|
+
"yellow",
|
|
729
|
+
`local device removed; requesting approval for ${accountRef.username} as a new device`,
|
|
730
|
+
),
|
|
493
731
|
);
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
732
|
+
await removeStoredDeviceAccount(ctx, config, accountRef);
|
|
733
|
+
}
|
|
734
|
+
if (existing) {
|
|
735
|
+
const { client } = existing;
|
|
736
|
+
try {
|
|
737
|
+
console.log(
|
|
738
|
+
`${color(ROOT_ACCENT, "using")} ${color(userAccent(client.me.user().userID), accountRef.username)}`,
|
|
739
|
+
);
|
|
740
|
+
printWhoami(client);
|
|
741
|
+
} finally {
|
|
742
|
+
await client.close().catch(() => {});
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
497
745
|
}
|
|
498
|
-
return;
|
|
499
746
|
}
|
|
500
747
|
|
|
748
|
+
const { username: accountUsername } = accountRef;
|
|
501
749
|
const privateKey = Client.generateSecretKey();
|
|
502
750
|
const client = await Client.create(privateKey, ctx.clientOptions);
|
|
503
|
-
attachDebugClientEvents(ctx, client, `login-request:${
|
|
751
|
+
attachDebugClientEvents(ctx, client, `login-request:${accountUsername}`);
|
|
504
752
|
try {
|
|
505
|
-
const [, registerErr] = await client.register(
|
|
753
|
+
const [, registerErr] = await client.register(accountUsername);
|
|
506
754
|
if (!registerErr) {
|
|
507
|
-
await connectAndWait(
|
|
755
|
+
await connectAndWait(
|
|
756
|
+
client,
|
|
757
|
+
ctx,
|
|
758
|
+
`login-request:${accountUsername}`,
|
|
759
|
+
);
|
|
508
760
|
await persistNewLocalAccount(
|
|
509
761
|
ctx,
|
|
510
762
|
config,
|
|
511
|
-
|
|
763
|
+
accountUsername,
|
|
512
764
|
privateKey,
|
|
513
765
|
client,
|
|
514
766
|
);
|
|
515
767
|
console.log(
|
|
516
|
-
`${color(ROOT_ACCENT, "registered")} ${color(userAccent(client.me.user().userID),
|
|
768
|
+
`${color(ROOT_ACCENT, "registered")} ${color(userAccent(client.me.user().userID), accountUsername)}`,
|
|
517
769
|
);
|
|
518
770
|
printWhoami(client);
|
|
519
771
|
return;
|
|
@@ -521,12 +773,19 @@ async function loginWithDeviceApproval(ctx, username) {
|
|
|
521
773
|
if (!isDeviceApprovalRequired(registerErr)) {
|
|
522
774
|
throw registerErr;
|
|
523
775
|
}
|
|
524
|
-
await waitForDeviceApproval(
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
776
|
+
await waitForDeviceApproval(
|
|
777
|
+
ctx,
|
|
778
|
+
client,
|
|
779
|
+
config,
|
|
780
|
+
accountUsername,
|
|
781
|
+
privateKey,
|
|
782
|
+
{
|
|
783
|
+
challenge: registerErr.challenge,
|
|
784
|
+
expiresAt: registerErr.expiresAt,
|
|
785
|
+
requestID: registerErr.requestID,
|
|
786
|
+
userID: registerErr.userID,
|
|
787
|
+
},
|
|
788
|
+
);
|
|
530
789
|
} finally {
|
|
531
790
|
await client.close().catch(() => {});
|
|
532
791
|
}
|
|
@@ -591,7 +850,7 @@ async function waitForDeviceApproval(
|
|
|
591
850
|
requestID: pending.requestID,
|
|
592
851
|
})
|
|
593
852
|
.catch(() => {});
|
|
594
|
-
|
|
853
|
+
deleteLocalAccount(ctx, config, username);
|
|
595
854
|
await writeConfig(ctx.configPath, config);
|
|
596
855
|
throw new Error("Device login cancelled.");
|
|
597
856
|
}
|
|
@@ -672,7 +931,7 @@ async function waitForDeviceApproval(
|
|
|
672
931
|
);
|
|
673
932
|
}
|
|
674
933
|
}
|
|
675
|
-
|
|
934
|
+
deleteLocalAccount(ctx, config, username);
|
|
676
935
|
await writeConfig(ctx.configPath, config);
|
|
677
936
|
throw new Error(`Device login ${current.status}.`);
|
|
678
937
|
}
|
|
@@ -812,18 +1071,21 @@ function formatDeviceRequestLine(request) {
|
|
|
812
1071
|
}
|
|
813
1072
|
|
|
814
1073
|
async function useAccount(ctx, args) {
|
|
815
|
-
const
|
|
1074
|
+
const requestedUsername = requireArg(args, 0, "username");
|
|
816
1075
|
const config = await readConfig(ctx.configPath);
|
|
817
|
-
|
|
1076
|
+
const accountRef = resolveAccountEntry(ctx, config, requestedUsername);
|
|
1077
|
+
await writeConfigIfChanged(ctx, config, accountRef.changed);
|
|
1078
|
+
assertAccountHostMatches(ctx, accountRef);
|
|
1079
|
+
if (!accountRef.account) {
|
|
818
1080
|
throw new Error(
|
|
819
|
-
`No local account for ${username}. Run vex auth register ${username} first.`,
|
|
1081
|
+
`No local account for ${accountRef.username}. Run vex auth register ${accountRef.username} first.`,
|
|
820
1082
|
);
|
|
821
1083
|
}
|
|
822
|
-
config.lastUsername =
|
|
1084
|
+
config.lastUsername = accountRef.key;
|
|
823
1085
|
await writeConfig(ctx.configPath, config);
|
|
824
|
-
const account = config.accounts[
|
|
1086
|
+
const account = config.accounts[accountRef.key];
|
|
825
1087
|
console.log(
|
|
826
|
-
`${color(ROOT_ACCENT, "using")} ${color(userAccent(account.userID),
|
|
1088
|
+
`${color(ROOT_ACCENT, "using")} ${color(userAccent(account.userID), accountRef.key)}`,
|
|
827
1089
|
);
|
|
828
1090
|
}
|
|
829
1091
|
|
|
@@ -890,14 +1152,14 @@ async function channelCommand(ctx, args) {
|
|
|
890
1152
|
await useChannel(ctx, args);
|
|
891
1153
|
return;
|
|
892
1154
|
}
|
|
893
|
-
await withReadyClient(ctx, args, async (client, rest) => {
|
|
1155
|
+
await withReadyClient(ctx, args, async (client, rest, meta) => {
|
|
894
1156
|
if (sub === "list" || sub === "ls") {
|
|
895
1157
|
const serverID = requireArg(rest, 0, "server id");
|
|
896
1158
|
printChannels(await client.channels.retrieve(serverID));
|
|
897
1159
|
return;
|
|
898
1160
|
}
|
|
899
1161
|
if (sub === "history") {
|
|
900
|
-
const accountState = accountUiState(meta.config, meta.account);
|
|
1162
|
+
const accountState = accountUiState(ctx, meta.config, meta.account);
|
|
901
1163
|
const channelID = rest[0] ?? accountState.lastChannel;
|
|
902
1164
|
if (!channelID)
|
|
903
1165
|
throw new Error(
|
|
@@ -976,7 +1238,7 @@ async function groupCommand(ctx, args) {
|
|
|
976
1238
|
|
|
977
1239
|
async function sendCommand(ctx, args) {
|
|
978
1240
|
await withReadyClient(ctx, args, async (client, rest, meta) => {
|
|
979
|
-
const accountState = accountUiState(meta.config, meta.account);
|
|
1241
|
+
const accountState = accountUiState(ctx, meta.config, meta.account);
|
|
980
1242
|
let channelID = rest[0];
|
|
981
1243
|
let messageParts = rest.slice(1);
|
|
982
1244
|
if (messageParts.length === 0 && accountState.lastChannel) {
|
|
@@ -1043,7 +1305,7 @@ async function createServerInChat(ctx, client, state, name, rl) {
|
|
|
1043
1305
|
|
|
1044
1306
|
async function createInviteInteractive(ctx, client, state, args, rl) {
|
|
1045
1307
|
const config = await readConfig(ctx.configPath);
|
|
1046
|
-
const accountState = accountUiState(config, state.account);
|
|
1308
|
+
const accountState = accountUiState(ctx, config, state.account);
|
|
1047
1309
|
let serverID =
|
|
1048
1310
|
state.target?.type === "channel" && state.target.serverID
|
|
1049
1311
|
? state.target.serverID
|
|
@@ -2140,7 +2402,7 @@ async function chat(ctx, args) {
|
|
|
2140
2402
|
username,
|
|
2141
2403
|
);
|
|
2142
2404
|
attachDebugClientEvents(ctx, client, `chat:${account.username}`);
|
|
2143
|
-
const accountState = accountUiState(config, account);
|
|
2405
|
+
const accountState = accountUiState(ctx, config, account);
|
|
2144
2406
|
const state = {
|
|
2145
2407
|
account,
|
|
2146
2408
|
avatarMarkers: new Map(),
|
|
@@ -2703,13 +2965,20 @@ async function withReadyClient(ctx, args, fn) {
|
|
|
2703
2965
|
|
|
2704
2966
|
async function authenticate(ctx, explicitUsername) {
|
|
2705
2967
|
const config = await readConfig(ctx.configPath);
|
|
2706
|
-
const
|
|
2968
|
+
const accountRef = resolveAccountEntry(
|
|
2969
|
+
ctx,
|
|
2970
|
+
config,
|
|
2971
|
+
explicitUsername ?? config.lastUsername,
|
|
2972
|
+
);
|
|
2973
|
+
await writeConfigIfChanged(ctx, config, accountRef.changed);
|
|
2974
|
+
assertAccountHostMatches(ctx, accountRef);
|
|
2975
|
+
const { username } = accountRef;
|
|
2707
2976
|
if (!username) {
|
|
2708
2977
|
throw new Error(
|
|
2709
2978
|
"No local account selected. Use --username or run register/login first.",
|
|
2710
2979
|
);
|
|
2711
2980
|
}
|
|
2712
|
-
const account =
|
|
2981
|
+
const account = accountRef.account;
|
|
2713
2982
|
if (!account) {
|
|
2714
2983
|
throw new Error(
|
|
2715
2984
|
`No local account for ${username}. Run register/login first.`,
|
|
@@ -2736,6 +3005,11 @@ async function authenticate(ctx, explicitUsername) {
|
|
|
2736
3005
|
account.pendingApproval,
|
|
2737
3006
|
);
|
|
2738
3007
|
}
|
|
3008
|
+
if (deviceErr && isMissingStoredDeviceError(deviceErr)) {
|
|
3009
|
+
await client.close().catch(() => {});
|
|
3010
|
+
await removeStoredDeviceAccount(ctx, config, accountRef);
|
|
3011
|
+
throw removedStoredDeviceError(ctx, username);
|
|
3012
|
+
}
|
|
2739
3013
|
if (deviceErr && ctx.password) {
|
|
2740
3014
|
const loginResult = await client.login(username, ctx.password);
|
|
2741
3015
|
if (!loginResult.ok)
|
|
@@ -2750,9 +3024,13 @@ async function authenticate(ctx, explicitUsername) {
|
|
|
2750
3024
|
}
|
|
2751
3025
|
account.userID = client.me.user().userID;
|
|
2752
3026
|
account.username = client.me.user().username ?? username;
|
|
2753
|
-
config.accounts[
|
|
3027
|
+
config.accounts[accountRef.key] = account;
|
|
2754
3028
|
await writeConfig(ctx.configPath, config);
|
|
2755
|
-
return {
|
|
3029
|
+
return {
|
|
3030
|
+
account: { ...account, accountKey: accountRef.key },
|
|
3031
|
+
client,
|
|
3032
|
+
config,
|
|
3033
|
+
};
|
|
2756
3034
|
}
|
|
2757
3035
|
|
|
2758
3036
|
async function resolveStoredDeviceID(ctx, client, account, username) {
|
|
@@ -2775,20 +3053,45 @@ async function resolveStoredDeviceID(ctx, client, account, username) {
|
|
|
2775
3053
|
|
|
2776
3054
|
async function authenticateOrRegister(ctx, explicitUsername) {
|
|
2777
3055
|
const config = await readConfig(ctx.configPath);
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2780
|
-
|
|
3056
|
+
const accountRef = resolveAccountEntry(
|
|
3057
|
+
ctx,
|
|
3058
|
+
config,
|
|
3059
|
+
explicitUsername ?? config.lastUsername,
|
|
3060
|
+
);
|
|
3061
|
+
await writeConfigIfChanged(ctx, config, accountRef.changed);
|
|
3062
|
+
assertAccountHostMatches(ctx, accountRef);
|
|
3063
|
+
if (accountRef.account) {
|
|
3064
|
+
try {
|
|
3065
|
+
return await authenticate(ctx, accountRef.key);
|
|
3066
|
+
} catch (err) {
|
|
3067
|
+
if (!isRemovedStoredDeviceError(err)) {
|
|
3068
|
+
throw err;
|
|
3069
|
+
}
|
|
3070
|
+
await removeStoredDeviceAccount(ctx, config, accountRef);
|
|
3071
|
+
console.log(
|
|
3072
|
+
color(
|
|
3073
|
+
"yellow",
|
|
3074
|
+
`local device removed; setting up ${accountRef.username} as a new device`,
|
|
3075
|
+
),
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
2781
3078
|
}
|
|
2782
3079
|
|
|
2783
3080
|
const rl = createInterface({ input, output });
|
|
2784
3081
|
try {
|
|
2785
3082
|
console.log("Welcome to vex.");
|
|
2786
|
-
const
|
|
3083
|
+
const enteredRaw = (
|
|
3084
|
+
accountRef.username || (await rl.question("username: "))
|
|
3085
|
+
)
|
|
2787
3086
|
.trim()
|
|
2788
3087
|
.toLowerCase();
|
|
3088
|
+
const enteredRef = resolveAccountEntry(ctx, config, enteredRaw);
|
|
3089
|
+
await writeConfigIfChanged(ctx, config, enteredRef.changed);
|
|
3090
|
+
assertAccountHostMatches(ctx, enteredRef);
|
|
3091
|
+
const entered = enteredRef.username;
|
|
2789
3092
|
if (!entered) throw new Error("username is required");
|
|
2790
|
-
if (
|
|
2791
|
-
return authenticate(ctx,
|
|
3093
|
+
if (enteredRef.account) {
|
|
3094
|
+
return authenticate(ctx, enteredRef.key);
|
|
2792
3095
|
}
|
|
2793
3096
|
const answer = (await rl.question(`register ${entered}? [Y/n] `))
|
|
2794
3097
|
.trim()
|
|
@@ -2834,7 +3137,10 @@ async function authenticateOrRegister(ctx, explicitUsername) {
|
|
|
2834
3137
|
|
|
2835
3138
|
async function makeClient(ctx, username) {
|
|
2836
3139
|
const config = await readConfig(ctx.configPath);
|
|
2837
|
-
const
|
|
3140
|
+
const accountRef = resolveAccountEntry(ctx, config, username);
|
|
3141
|
+
await writeConfigIfChanged(ctx, config, accountRef.changed);
|
|
3142
|
+
assertAccountHostMatches(ctx, accountRef);
|
|
3143
|
+
const account = accountRef.account;
|
|
2838
3144
|
const privateKey = account?.privateKey ?? Client.generateSecretKey();
|
|
2839
3145
|
const client = await Client.create(privateKey, ctx.clientOptions);
|
|
2840
3146
|
return { client, config };
|
|
@@ -3523,9 +3829,11 @@ function targetToAccountUi(target) {
|
|
|
3523
3829
|
return patch;
|
|
3524
3830
|
}
|
|
3525
3831
|
|
|
3526
|
-
function accountUiState(config, account) {
|
|
3832
|
+
function accountUiState(ctx, config, account) {
|
|
3527
3833
|
if (!account) return {};
|
|
3528
|
-
const key =
|
|
3834
|
+
const key =
|
|
3835
|
+
account.accountKey ??
|
|
3836
|
+
(account.username ? accountKeyFor(ctx, account.username) : null);
|
|
3529
3837
|
const stored = key ? config.accounts?.[key]?.ui : null;
|
|
3530
3838
|
if (!stored || typeof stored !== "object") return {};
|
|
3531
3839
|
return {
|
|
@@ -3543,9 +3851,14 @@ function accountUiState(config, account) {
|
|
|
3543
3851
|
|
|
3544
3852
|
async function saveAccountUiState(ctx, account, patch) {
|
|
3545
3853
|
const config = await readConfig(ctx.configPath);
|
|
3546
|
-
const key =
|
|
3854
|
+
const key =
|
|
3855
|
+
account?.accountKey ??
|
|
3856
|
+
(account?.username ? accountKeyFor(ctx, account.username) : null);
|
|
3547
3857
|
if (!key || !config.accounts[key]) return;
|
|
3548
|
-
const current = accountUiState(config,
|
|
3858
|
+
const current = accountUiState(ctx, config, {
|
|
3859
|
+
...config.accounts[key],
|
|
3860
|
+
accountKey: key,
|
|
3861
|
+
});
|
|
3549
3862
|
config.accounts[key] = {
|
|
3550
3863
|
...config.accounts[key],
|
|
3551
3864
|
ui: {
|