@profullstack/qryptchat 0.13.0 → 0.14.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.
|
@@ -16,6 +16,13 @@ import {
|
|
|
16
16
|
expandMentions
|
|
17
17
|
} from "./chunk-4ea1xy2c.js";
|
|
18
18
|
|
|
19
|
+
// src/cli/login.js
|
|
20
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
21
|
+
import { createServer } from "node:http";
|
|
22
|
+
import { hostname } from "node:os";
|
|
23
|
+
import { spawn } from "node:child_process";
|
|
24
|
+
import { createInterface } from "node:readline/promises";
|
|
25
|
+
|
|
19
26
|
// src/cli/crypto.js
|
|
20
27
|
import { MlKem1024 as MlKem10242 } from "mlkem";
|
|
21
28
|
|
|
@@ -222,6 +229,17 @@ class PostQuantumEncryptionService {
|
|
|
222
229
|
console.error("\uD83D\uDD10 Failed to load ML-KEM-768 keys:", error);
|
|
223
230
|
}
|
|
224
231
|
}
|
|
232
|
+
async storedUserKeys(alg) {
|
|
233
|
+
const is768 = alg === this.kemName768;
|
|
234
|
+
if (is768 ? !this.userKeys768 : !this.userKeys) {
|
|
235
|
+
await (is768 ? this.loadUserKeys768() : this.loadUserKeys());
|
|
236
|
+
}
|
|
237
|
+
const keys = is768 ? this.userKeys768 : this.userKeys;
|
|
238
|
+
if (!keys?.privateKey) {
|
|
239
|
+
throw new Error("No encryption keys on this device. Restore them in Settings > Keys (backup PIN).");
|
|
240
|
+
}
|
|
241
|
+
return keys;
|
|
242
|
+
}
|
|
225
243
|
async getPublicKey() {
|
|
226
244
|
const keys = await this.getUserKeys();
|
|
227
245
|
return keys.publicKey;
|
|
@@ -346,7 +364,7 @@ class PostQuantumEncryptionService {
|
|
|
346
364
|
return "[Legacy encrypted message - please delete]";
|
|
347
365
|
}
|
|
348
366
|
const open = async (alg) => {
|
|
349
|
-
const keys =
|
|
367
|
+
const keys = await this.storedUserKeys(alg);
|
|
350
368
|
const privateKeyBytes = this.stripKeyHeaderIfPresent(Base64.decode(keys.privateKey));
|
|
351
369
|
const expected = alg === this.kemName768 ? 2400 : 3168;
|
|
352
370
|
if (privateKeyBytes.length !== expected) {
|
|
@@ -553,6 +571,188 @@ async function openKeyBlob(blob, ephemeral) {
|
|
|
553
571
|
return { keys1024: keys.keys1024, keys768: keys.keys768 };
|
|
554
572
|
}
|
|
555
573
|
|
|
574
|
+
// src/lib/auth/cli-match.js
|
|
575
|
+
var ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
576
|
+
async function matchCode(challenge, kemPublicKey) {
|
|
577
|
+
const bytes = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(`qc-login
|
|
578
|
+
${challenge}
|
|
579
|
+
${kemPublicKey}`)));
|
|
580
|
+
let out = "";
|
|
581
|
+
for (let i = 0;i < 8; i++)
|
|
582
|
+
out += ALPHABET[bytes[i] % ALPHABET.length];
|
|
583
|
+
return `${out.slice(0, 4)}-${out.slice(4)}`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/cli/login.js
|
|
587
|
+
var b64url = (buf) => Buffer.from(buf).toString("base64url");
|
|
588
|
+
var TIMEOUT_MS = 5 * 60 * 1000;
|
|
589
|
+
var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title>
|
|
590
|
+
<style>body{font:16px system-ui,sans-serif;display:grid;place-items:center;min-height:90vh;background:#0b0d12;color:#e6e8ee}main{max-width:28rem;text-align:center}</style>
|
|
591
|
+
<main><h1>${title}</h1><p>${body}</p></main>`;
|
|
592
|
+
function isRemote({ platform = process.platform, env = process.env } = {}) {
|
|
593
|
+
if (env.QC_NO_BROWSER)
|
|
594
|
+
return true;
|
|
595
|
+
if (platform === "darwin" || platform === "win32")
|
|
596
|
+
return false;
|
|
597
|
+
return !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
598
|
+
}
|
|
599
|
+
function parsePasted(input, state) {
|
|
600
|
+
const text = String(input || "").trim();
|
|
601
|
+
if (!text)
|
|
602
|
+
return null;
|
|
603
|
+
if (/^https?:\/\//i.test(text)) {
|
|
604
|
+
let url;
|
|
605
|
+
try {
|
|
606
|
+
url = new URL(text);
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
const got = url.searchParams.get("state");
|
|
611
|
+
if (got && got !== state)
|
|
612
|
+
throw new Error("That link belongs to a different login. Run qc login again.");
|
|
613
|
+
return url.searchParams.get("code");
|
|
614
|
+
}
|
|
615
|
+
return /^[A-Za-z0-9_-]{20,}$/.test(text) ? text : null;
|
|
616
|
+
}
|
|
617
|
+
function openBrowser2(url, { platform = process.platform, env = process.env } = {}) {
|
|
618
|
+
if (isRemote({ platform, env }))
|
|
619
|
+
return false;
|
|
620
|
+
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
621
|
+
try {
|
|
622
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
623
|
+
child.on("error", () => {});
|
|
624
|
+
child.unref();
|
|
625
|
+
return true;
|
|
626
|
+
} catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function waitForCallback(server, state) {
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
const timer = setTimeout(() => reject(new Error("Timed out waiting for the browser (5 minutes). Run qc login again.")), TIMEOUT_MS);
|
|
633
|
+
server.on("request", (req, res) => {
|
|
634
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
635
|
+
if (url.pathname !== "/callback") {
|
|
636
|
+
res.writeHead(404).end();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const done = (status, title, body) => {
|
|
640
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" }).end(PAGE(title, body));
|
|
641
|
+
};
|
|
642
|
+
if (url.searchParams.get("state") !== state) {
|
|
643
|
+
done(400, "That was not this login", "The state did not match. Go back to your terminal.");
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
clearTimeout(timer);
|
|
647
|
+
const error = url.searchParams.get("error");
|
|
648
|
+
if (error) {
|
|
649
|
+
done(200, "Not approved", "Nothing was shared. You can close this tab.");
|
|
650
|
+
reject(new Error(error === "access_denied" ? "Login was denied in the browser." : `Login failed: ${error}`));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
done(200, "Signed in", "You can close this tab and go back to your terminal.");
|
|
654
|
+
resolve(url.searchParams.get("code"));
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
async function login2(options = {}) {
|
|
659
|
+
const env = options.env ?? process.env;
|
|
660
|
+
const print = options.print ?? ((line) => process.stderr.write(`${line}
|
|
661
|
+
`));
|
|
662
|
+
const base = (options.base ?? baseUrl(null, env)).replace(/\/+$/, "");
|
|
663
|
+
const verifier = b64url(randomBytes(32));
|
|
664
|
+
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
665
|
+
const state = b64url(randomBytes(16));
|
|
666
|
+
const ephemeral = await ephemeralKeypair();
|
|
667
|
+
let server;
|
|
668
|
+
let redirectUri = "oob";
|
|
669
|
+
const oob = options.oob || isRemote({ env });
|
|
670
|
+
if (!oob) {
|
|
671
|
+
server = createServer();
|
|
672
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
673
|
+
redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
|
|
674
|
+
}
|
|
675
|
+
const url = new URL("/cli/authorize", base);
|
|
676
|
+
for (const [k, v] of Object.entries({
|
|
677
|
+
response_type: "code",
|
|
678
|
+
code_challenge: challenge,
|
|
679
|
+
code_challenge_method: "S256",
|
|
680
|
+
redirect_uri: redirectUri,
|
|
681
|
+
state,
|
|
682
|
+
client_name: `qc on ${hostname()}`,
|
|
683
|
+
kem: ephemeral.publicKey
|
|
684
|
+
})) {
|
|
685
|
+
url.searchParams.set(k, v);
|
|
686
|
+
}
|
|
687
|
+
const match = await matchCode(challenge, ephemeral.publicKey);
|
|
688
|
+
print("");
|
|
689
|
+
print(` Confirmation code: ${match}`);
|
|
690
|
+
print(" Check that the browser shows the same code before you approve.");
|
|
691
|
+
print("");
|
|
692
|
+
const opened = !oob && openBrowser2(url.toString(), { env });
|
|
693
|
+
print(opened ? " Opened your browser. If it did not, open:" : " Open this link in a browser where you are signed in to qrypt.chat:");
|
|
694
|
+
print(` ${url.toString()}`);
|
|
695
|
+
print("");
|
|
696
|
+
let code;
|
|
697
|
+
let rl;
|
|
698
|
+
try {
|
|
699
|
+
if (server) {
|
|
700
|
+
const waits = [waitForCallback(server, state)];
|
|
701
|
+
if (process.stdin.isTTY) {
|
|
702
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
703
|
+
waits.push((async () => {
|
|
704
|
+
for (;; ) {
|
|
705
|
+
let line;
|
|
706
|
+
try {
|
|
707
|
+
line = await rl.question(" Waiting for the browser… or paste the URL it ended up on: ");
|
|
708
|
+
} catch {
|
|
709
|
+
return new Promise(() => {});
|
|
710
|
+
}
|
|
711
|
+
const pasted = parsePasted(line, state);
|
|
712
|
+
if (pasted)
|
|
713
|
+
return pasted;
|
|
714
|
+
print(" That is not a qc login code or callback URL.");
|
|
715
|
+
}
|
|
716
|
+
})());
|
|
717
|
+
}
|
|
718
|
+
code = await Promise.race(waits);
|
|
719
|
+
} else {
|
|
720
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
721
|
+
for (;; ) {
|
|
722
|
+
code = parsePasted(await rl.question(" Paste the code (or the URL) from the browser: "), state);
|
|
723
|
+
if (code)
|
|
724
|
+
break;
|
|
725
|
+
print(" That is not a qc login code.");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} finally {
|
|
729
|
+
rl?.close();
|
|
730
|
+
server?.close();
|
|
731
|
+
}
|
|
732
|
+
if (!code)
|
|
733
|
+
throw new Error("No code came back from the browser.");
|
|
734
|
+
const res = await fetch(`${base}/api/cli/token`, {
|
|
735
|
+
method: "POST",
|
|
736
|
+
headers: { "Content-Type": "application/json" },
|
|
737
|
+
body: JSON.stringify({ grant_type: "authorization_code", code, code_verifier: verifier, redirect_uri: redirectUri })
|
|
738
|
+
});
|
|
739
|
+
const token = await res.json().catch(() => ({}));
|
|
740
|
+
if (!res.ok)
|
|
741
|
+
throw new Error(token.error_description || token.error || `Sign-in failed (HTTP ${res.status})`);
|
|
742
|
+
const keys = await openKeyBlob(token.key_blob, ephemeral);
|
|
743
|
+
const session = {
|
|
744
|
+
base,
|
|
745
|
+
access_token: token.access_token,
|
|
746
|
+
refresh_token: token.refresh_token,
|
|
747
|
+
expires_at: token.expires_at,
|
|
748
|
+
user: token.user,
|
|
749
|
+
keys,
|
|
750
|
+
created_at: new Date().toISOString()
|
|
751
|
+
};
|
|
752
|
+
const save = await storeSession(session, env, options.vault);
|
|
753
|
+
return { session, save };
|
|
754
|
+
}
|
|
755
|
+
|
|
556
756
|
// src/lib/chat/reactions.js
|
|
557
757
|
var REACTION_TYPE = "reaction";
|
|
558
758
|
var MAX_EMOJI_LENGTH = 32;
|
|
@@ -959,195 +1159,6 @@ function conversationTitle(c, me) {
|
|
|
959
1159
|
return others.length ? others.join(", ") : "Just you";
|
|
960
1160
|
}
|
|
961
1161
|
|
|
962
|
-
// src/cli/login.js
|
|
963
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
964
|
-
import { createServer } from "node:http";
|
|
965
|
-
import { hostname } from "node:os";
|
|
966
|
-
import { spawn } from "node:child_process";
|
|
967
|
-
import { createInterface } from "node:readline/promises";
|
|
968
|
-
|
|
969
|
-
// src/lib/auth/cli-match.js
|
|
970
|
-
var ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
971
|
-
async function matchCode(challenge, kemPublicKey) {
|
|
972
|
-
const bytes = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(`qc-login
|
|
973
|
-
${challenge}
|
|
974
|
-
${kemPublicKey}`)));
|
|
975
|
-
let out = "";
|
|
976
|
-
for (let i = 0;i < 8; i++)
|
|
977
|
-
out += ALPHABET[bytes[i] % ALPHABET.length];
|
|
978
|
-
return `${out.slice(0, 4)}-${out.slice(4)}`;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// src/cli/login.js
|
|
982
|
-
var b64url = (buf) => Buffer.from(buf).toString("base64url");
|
|
983
|
-
var TIMEOUT_MS = 5 * 60 * 1000;
|
|
984
|
-
var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title>
|
|
985
|
-
<style>body{font:16px system-ui,sans-serif;display:grid;place-items:center;min-height:90vh;background:#0b0d12;color:#e6e8ee}main{max-width:28rem;text-align:center}</style>
|
|
986
|
-
<main><h1>${title}</h1><p>${body}</p></main>`;
|
|
987
|
-
function isRemote({ platform = process.platform, env = process.env } = {}) {
|
|
988
|
-
if (env.QC_NO_BROWSER)
|
|
989
|
-
return true;
|
|
990
|
-
if (platform === "darwin" || platform === "win32")
|
|
991
|
-
return false;
|
|
992
|
-
return !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
993
|
-
}
|
|
994
|
-
function parsePasted(input, state) {
|
|
995
|
-
const text = String(input || "").trim();
|
|
996
|
-
if (!text)
|
|
997
|
-
return null;
|
|
998
|
-
if (/^https?:\/\//i.test(text)) {
|
|
999
|
-
let url;
|
|
1000
|
-
try {
|
|
1001
|
-
url = new URL(text);
|
|
1002
|
-
} catch {
|
|
1003
|
-
return null;
|
|
1004
|
-
}
|
|
1005
|
-
const got = url.searchParams.get("state");
|
|
1006
|
-
if (got && got !== state)
|
|
1007
|
-
throw new Error("That link belongs to a different login. Run qc login again.");
|
|
1008
|
-
return url.searchParams.get("code");
|
|
1009
|
-
}
|
|
1010
|
-
return /^[A-Za-z0-9_-]{20,}$/.test(text) ? text : null;
|
|
1011
|
-
}
|
|
1012
|
-
function openBrowser2(url, { platform = process.platform, env = process.env } = {}) {
|
|
1013
|
-
if (isRemote({ platform, env }))
|
|
1014
|
-
return false;
|
|
1015
|
-
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1016
|
-
try {
|
|
1017
|
-
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
1018
|
-
child.on("error", () => {});
|
|
1019
|
-
child.unref();
|
|
1020
|
-
return true;
|
|
1021
|
-
} catch {
|
|
1022
|
-
return false;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function waitForCallback(server, state) {
|
|
1026
|
-
return new Promise((resolve, reject) => {
|
|
1027
|
-
const timer = setTimeout(() => reject(new Error("Timed out waiting for the browser (5 minutes). Run qc login again.")), TIMEOUT_MS);
|
|
1028
|
-
server.on("request", (req, res) => {
|
|
1029
|
-
const url = new URL(req.url, "http://127.0.0.1");
|
|
1030
|
-
if (url.pathname !== "/callback") {
|
|
1031
|
-
res.writeHead(404).end();
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
const done = (status, title, body) => {
|
|
1035
|
-
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" }).end(PAGE(title, body));
|
|
1036
|
-
};
|
|
1037
|
-
if (url.searchParams.get("state") !== state) {
|
|
1038
|
-
done(400, "That was not this login", "The state did not match. Go back to your terminal.");
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
clearTimeout(timer);
|
|
1042
|
-
const error = url.searchParams.get("error");
|
|
1043
|
-
if (error) {
|
|
1044
|
-
done(200, "Not approved", "Nothing was shared. You can close this tab.");
|
|
1045
|
-
reject(new Error(error === "access_denied" ? "Login was denied in the browser." : `Login failed: ${error}`));
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
done(200, "Signed in", "You can close this tab and go back to your terminal.");
|
|
1049
|
-
resolve(url.searchParams.get("code"));
|
|
1050
|
-
});
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
async function login2(options = {}) {
|
|
1054
|
-
const env = options.env ?? process.env;
|
|
1055
|
-
const print = options.print ?? ((line) => process.stderr.write(`${line}
|
|
1056
|
-
`));
|
|
1057
|
-
const base = (options.base ?? baseUrl(null, env)).replace(/\/+$/, "");
|
|
1058
|
-
const verifier = b64url(randomBytes(32));
|
|
1059
|
-
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
1060
|
-
const state = b64url(randomBytes(16));
|
|
1061
|
-
const ephemeral = await ephemeralKeypair();
|
|
1062
|
-
let server;
|
|
1063
|
-
let redirectUri = "oob";
|
|
1064
|
-
const oob = options.oob || isRemote({ env });
|
|
1065
|
-
if (!oob) {
|
|
1066
|
-
server = createServer();
|
|
1067
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
1068
|
-
redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
|
|
1069
|
-
}
|
|
1070
|
-
const url = new URL("/cli/authorize", base);
|
|
1071
|
-
for (const [k, v] of Object.entries({
|
|
1072
|
-
response_type: "code",
|
|
1073
|
-
code_challenge: challenge,
|
|
1074
|
-
code_challenge_method: "S256",
|
|
1075
|
-
redirect_uri: redirectUri,
|
|
1076
|
-
state,
|
|
1077
|
-
client_name: `qc on ${hostname()}`,
|
|
1078
|
-
kem: ephemeral.publicKey
|
|
1079
|
-
})) {
|
|
1080
|
-
url.searchParams.set(k, v);
|
|
1081
|
-
}
|
|
1082
|
-
const match = await matchCode(challenge, ephemeral.publicKey);
|
|
1083
|
-
print("");
|
|
1084
|
-
print(` Confirmation code: ${match}`);
|
|
1085
|
-
print(" Check that the browser shows the same code before you approve.");
|
|
1086
|
-
print("");
|
|
1087
|
-
const opened = !oob && openBrowser2(url.toString(), { env });
|
|
1088
|
-
print(opened ? " Opened your browser. If it did not, open:" : " Open this link in a browser where you are signed in to qrypt.chat:");
|
|
1089
|
-
print(` ${url.toString()}`);
|
|
1090
|
-
print("");
|
|
1091
|
-
let code;
|
|
1092
|
-
let rl;
|
|
1093
|
-
try {
|
|
1094
|
-
if (server) {
|
|
1095
|
-
const waits = [waitForCallback(server, state)];
|
|
1096
|
-
if (process.stdin.isTTY) {
|
|
1097
|
-
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1098
|
-
waits.push((async () => {
|
|
1099
|
-
for (;; ) {
|
|
1100
|
-
let line;
|
|
1101
|
-
try {
|
|
1102
|
-
line = await rl.question(" Waiting for the browser… or paste the URL it ended up on: ");
|
|
1103
|
-
} catch {
|
|
1104
|
-
return new Promise(() => {});
|
|
1105
|
-
}
|
|
1106
|
-
const pasted = parsePasted(line, state);
|
|
1107
|
-
if (pasted)
|
|
1108
|
-
return pasted;
|
|
1109
|
-
print(" That is not a qc login code or callback URL.");
|
|
1110
|
-
}
|
|
1111
|
-
})());
|
|
1112
|
-
}
|
|
1113
|
-
code = await Promise.race(waits);
|
|
1114
|
-
} else {
|
|
1115
|
-
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1116
|
-
for (;; ) {
|
|
1117
|
-
code = parsePasted(await rl.question(" Paste the code (or the URL) from the browser: "), state);
|
|
1118
|
-
if (code)
|
|
1119
|
-
break;
|
|
1120
|
-
print(" That is not a qc login code.");
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
} finally {
|
|
1124
|
-
rl?.close();
|
|
1125
|
-
server?.close();
|
|
1126
|
-
}
|
|
1127
|
-
if (!code)
|
|
1128
|
-
throw new Error("No code came back from the browser.");
|
|
1129
|
-
const res = await fetch(`${base}/api/cli/token`, {
|
|
1130
|
-
method: "POST",
|
|
1131
|
-
headers: { "Content-Type": "application/json" },
|
|
1132
|
-
body: JSON.stringify({ grant_type: "authorization_code", code, code_verifier: verifier, redirect_uri: redirectUri })
|
|
1133
|
-
});
|
|
1134
|
-
const token = await res.json().catch(() => ({}));
|
|
1135
|
-
if (!res.ok)
|
|
1136
|
-
throw new Error(token.error_description || token.error || `Sign-in failed (HTTP ${res.status})`);
|
|
1137
|
-
const keys = await openKeyBlob(token.key_blob, ephemeral);
|
|
1138
|
-
const session = {
|
|
1139
|
-
base,
|
|
1140
|
-
access_token: token.access_token,
|
|
1141
|
-
refresh_token: token.refresh_token,
|
|
1142
|
-
expires_at: token.expires_at,
|
|
1143
|
-
user: token.user,
|
|
1144
|
-
keys,
|
|
1145
|
-
created_at: new Date().toISOString()
|
|
1146
|
-
};
|
|
1147
|
-
const save = await storeSession(session, env, options.vault);
|
|
1148
|
-
return { session, save };
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
1162
|
// src/cli/agent.js
|
|
1152
1163
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1153
1164
|
function parseInvite(input) {
|
|
@@ -1301,7 +1312,7 @@ Usage:
|
|
|
1301
1312
|
qc whoami who this terminal is signed in as
|
|
1302
1313
|
qc keys [publish] whether this terminal's keys are your account's; publish makes them the
|
|
1303
1314
|
account's again (when another device published keys qc does not hold)
|
|
1304
|
-
qc profile [--emoji \uD83D\uDD2D] [--pronouns she/her] [--website URL] [--bio TEXT]
|
|
1315
|
+
qc profile [--name "Ada L"] [--emoji \uD83D\uDD2D] [--pronouns she/her] [--website URL] [--bio TEXT]
|
|
1305
1316
|
show or set your public profile (an empty value clears it);
|
|
1306
1317
|
--openprofile prints it as OpenProfile.md
|
|
1307
1318
|
qc chats [--archived] list your chats (archived ones only with --archived); qc list is the same
|
|
@@ -1420,7 +1431,7 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1420
1431
|
}
|
|
1421
1432
|
case "agent": {
|
|
1422
1433
|
if (rest[0] === "call") {
|
|
1423
|
-
const { runVoiceAgent } = await import("./chunk-
|
|
1434
|
+
const { runVoiceAgent } = await import("./chunk-amknn876.js");
|
|
1424
1435
|
return runVoiceAgent(await client(flags), {
|
|
1425
1436
|
brain: typeof flags.brain === "string" ? flags.brain : undefined,
|
|
1426
1437
|
voice: typeof flags.voice === "string" ? flags.voice : undefined,
|
|
@@ -1529,9 +1540,11 @@ ${ok("Installed OpenEmoji. Restart the terminal, then run: qc fonts status")}`);
|
|
|
1529
1540
|
for (const k of ["emoji", "pronouns", "website", "bio"])
|
|
1530
1541
|
if (flags[k] !== undefined)
|
|
1531
1542
|
fields[k] = flags[k] === true ? "" : String(flags[k]);
|
|
1543
|
+
if (flags.name !== undefined && flags.name !== true)
|
|
1544
|
+
fields.displayName = String(flags.name);
|
|
1532
1545
|
const p = Object.keys(fields).length ? await c.updateProfile(fields) : await c.profile();
|
|
1533
1546
|
if (flags.json)
|
|
1534
|
-
return json({ username: p.username, emoji: p.emoji ?? null, pronouns: p.pronouns ?? null, website: p.website ?? null, bio: p.bio ?? null });
|
|
1547
|
+
return json({ username: p.username, displayName: p.displayName ?? p.display_name ?? null, emoji: p.emoji ?? null, pronouns: p.pronouns ?? null, website: p.website ?? null, bio: p.bio ?? null });
|
|
1535
1548
|
out(who(p));
|
|
1536
1549
|
if (p.website)
|
|
1537
1550
|
out(`${mark("link")}${p.website}`);
|
|
@@ -1539,7 +1552,7 @@ ${ok("Installed OpenEmoji. Restart the terminal, then run: qc fonts status")}`);
|
|
|
1539
1552
|
out(p.bio);
|
|
1540
1553
|
if (!Object.keys(fields).length)
|
|
1541
1554
|
out(`
|
|
1542
|
-
${info(
|
|
1555
|
+
${info('Set: qc profile --name "Ada L" --emoji :telescope: --pronouns she/her --website https://you.example (an empty value clears)')}`);
|
|
1543
1556
|
return;
|
|
1544
1557
|
}
|
|
1545
1558
|
case "chats":
|
|
@@ -1584,7 +1597,7 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
|
|
|
1584
1597
|
throw new Error('Usage: qc call <number> (e.g. qc call "(415) 555-1234" or qc call +442079460958)');
|
|
1585
1598
|
const c = await client(flags);
|
|
1586
1599
|
const url = `${c.base}/phone?to=${encodeURIComponent(rest.join(" "))}`;
|
|
1587
|
-
const { openBrowser } = await import("./chunk-
|
|
1600
|
+
const { openBrowser } = await import("./chunk-4mx8vtcw.js");
|
|
1588
1601
|
const opened = openBrowser(url);
|
|
1589
1602
|
return out(`${mark("send")}${opened === false ? "Open" : "Opened"} ${url}
|
|
1590
1603
|
Calls to phones need your browser for the audio; tap Call there.`);
|
package/dist/commands.js
CHANGED
package/dist/mcp.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/qryptchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "qc: qrypt.chat in your terminal. A full-screen end-to-end encrypted chat client (ML-KEM-1024), plus a scriptable CLI and an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|