@profullstack/qryptchat 0.4.1 → 0.4.3
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/{chunk-7ane8p3w.js → chunk-8m3j5hfk.js} +48 -7
- package/dist/commands.js +1 -1
- package/dist/mcp.js +1 -1
- package/dist/tui.js +8 -5
- package/package.json +1 -1
|
@@ -964,25 +964,48 @@ class QcClient {
|
|
|
964
964
|
await this.refresh();
|
|
965
965
|
return this.session.access_token;
|
|
966
966
|
}
|
|
967
|
-
async
|
|
967
|
+
async reauth(usedToken) {
|
|
968
|
+
if (this.session.access_token === usedToken)
|
|
969
|
+
await this.refresh();
|
|
970
|
+
}
|
|
971
|
+
async request(path, { method = "GET", body, signal, retry = true, throttled = 0 } = {}) {
|
|
972
|
+
const used = await this.token();
|
|
968
973
|
const res = await this.fetch(`${this.base}${path}`, {
|
|
969
974
|
method,
|
|
970
975
|
signal,
|
|
971
976
|
headers: {
|
|
972
|
-
Authorization: `Bearer ${
|
|
977
|
+
Authorization: `Bearer ${used}`,
|
|
973
978
|
...body !== undefined ? { "Content-Type": "application/json" } : {}
|
|
974
979
|
},
|
|
975
980
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
976
981
|
});
|
|
977
982
|
if (res.status === 401 && retry) {
|
|
978
|
-
await this.
|
|
979
|
-
return this.request(path, { method, body, signal, retry: false });
|
|
983
|
+
await this.reauth(used);
|
|
984
|
+
return this.request(path, { method, body, signal, retry: false, throttled });
|
|
985
|
+
}
|
|
986
|
+
if (res.status === 429 && throttled < 3) {
|
|
987
|
+
await sleep(retryAfterMs(res, throttled), signal);
|
|
988
|
+
return this.request(path, { method, body, signal, retry, throttled: throttled + 1 });
|
|
980
989
|
}
|
|
981
990
|
const data = await res.json().catch(() => ({}));
|
|
982
991
|
if (!res.ok)
|
|
983
992
|
throw new QcError(data.error_description || data.error || `HTTP ${res.status}`, res.status);
|
|
984
993
|
return data;
|
|
985
994
|
}
|
|
995
|
+
async keyCheck() {
|
|
996
|
+
try {
|
|
997
|
+
const me = this.me?.id;
|
|
998
|
+
const mine = this.session.keys?.keys1024?.publicKey;
|
|
999
|
+
if (!me || !mine)
|
|
1000
|
+
return "unknown";
|
|
1001
|
+
const { public_keys: keys = {} } = await this.request("/api/crypto/public-keys", { method: "POST", body: { user_ids: [me] } });
|
|
1002
|
+
if (!keys[me])
|
|
1003
|
+
return "unknown";
|
|
1004
|
+
return keys[me] === mine ? "ok" : "mismatch";
|
|
1005
|
+
} catch {
|
|
1006
|
+
return "unknown";
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
986
1009
|
async conversations() {
|
|
987
1010
|
const { conversations = [] } = await this.request("/api/conversations/load", { method: "POST", body: {} });
|
|
988
1011
|
return conversations.map((c) => ({ ...c, title: conversationTitle(c, this.me) }));
|
|
@@ -1047,21 +1070,31 @@ class QcClient {
|
|
|
1047
1070
|
}
|
|
1048
1071
|
async events(onEvent, { signal, onStatus = () => {} } = {}) {
|
|
1049
1072
|
let delay = 1000;
|
|
1073
|
+
let reauthed = false;
|
|
1050
1074
|
while (!signal?.aborted) {
|
|
1051
1075
|
onStatus("connecting");
|
|
1052
1076
|
try {
|
|
1077
|
+
const used = await this.token();
|
|
1053
1078
|
const res = await this.fetch(`${this.base}/api/events`, {
|
|
1054
|
-
headers: { Authorization: `Bearer ${
|
|
1079
|
+
headers: { Authorization: `Bearer ${used}`, Accept: "text/event-stream" },
|
|
1055
1080
|
signal
|
|
1056
1081
|
});
|
|
1057
1082
|
if (res.status === 401) {
|
|
1058
|
-
|
|
1083
|
+
if (reauthed)
|
|
1084
|
+
throw new QcError("The server refused this session. Run qc login.", 401);
|
|
1085
|
+
reauthed = true;
|
|
1086
|
+
await this.reauth(used);
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (res.status === 429) {
|
|
1090
|
+
await sleep(retryAfterMs(res, 2), signal);
|
|
1059
1091
|
continue;
|
|
1060
1092
|
}
|
|
1061
1093
|
if (!res.ok || !res.body)
|
|
1062
1094
|
throw new Error(`HTTP ${res.status}`);
|
|
1063
1095
|
onStatus("live");
|
|
1064
1096
|
delay = 1000;
|
|
1097
|
+
reauthed = false;
|
|
1065
1098
|
await readSse(res.body, onEvent);
|
|
1066
1099
|
} catch (err) {
|
|
1067
1100
|
if (signal?.aborted)
|
|
@@ -1111,6 +1144,10 @@ async function readSse(body, onEvent) {
|
|
|
1111
1144
|
}
|
|
1112
1145
|
}
|
|
1113
1146
|
}
|
|
1147
|
+
function retryAfterMs(res, attempt = 0) {
|
|
1148
|
+
const s = Number(res.headers?.get?.("retry-after"));
|
|
1149
|
+
return Number.isFinite(s) && s > 0 ? Math.min(s, 60) * 1000 : 2000 * 2 ** attempt;
|
|
1150
|
+
}
|
|
1114
1151
|
function sleep(ms, signal) {
|
|
1115
1152
|
return new Promise((resolve) => {
|
|
1116
1153
|
const t = setTimeout(resolve, ms);
|
|
@@ -1417,7 +1454,11 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1417
1454
|
}
|
|
1418
1455
|
case "login": {
|
|
1419
1456
|
const { session } = await login({ oob: !!flags.oob });
|
|
1420
|
-
|
|
1457
|
+
out(`Signed in as @${session.user?.username ?? "unknown"} on ${session.base}. Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
|
|
1458
|
+
if (await new QcClient(session).keyCheck() === "mismatch") {
|
|
1459
|
+
out("Warning: the browser handed over keys that are not your account's current keys, so messages will not decrypt here. Restore your keys in that browser (Settings > Keys) or approve from the device you chat on, then run qc login again.");
|
|
1460
|
+
}
|
|
1461
|
+
return;
|
|
1421
1462
|
}
|
|
1422
1463
|
case "logout":
|
|
1423
1464
|
clearSession();
|
package/dist/commands.js
CHANGED
package/dist/mcp.js
CHANGED
package/dist/tui.js
CHANGED
|
@@ -203,16 +203,17 @@ function saveRecent(list) {
|
|
|
203
203
|
writeFileSync(recentFile(), JSON.stringify(list), { mode: 384 });
|
|
204
204
|
} catch {}
|
|
205
205
|
}
|
|
206
|
+
var KEY_MISMATCH = "Keys don't match your account, so messages can't decrypt. Run qc login and approve from a browser that can read your chats.";
|
|
206
207
|
async function runTui(client, { initialChat } = {}) {
|
|
207
208
|
const state = initialState(client.me);
|
|
208
209
|
state.recent = loadRecent();
|
|
209
210
|
const abort = new AbortController;
|
|
210
211
|
const app = await createApp({ quitKeys: ["ctrl+c"], focusNavigation: false });
|
|
211
212
|
const redraw = () => app.invalidate();
|
|
212
|
-
const note = (msg) => {
|
|
213
|
+
const note = (msg, { sticky = false } = {}) => {
|
|
213
214
|
state.notice = msg;
|
|
214
215
|
redraw();
|
|
215
|
-
if (msg)
|
|
216
|
+
if (msg && !sticky)
|
|
216
217
|
setTimeout(() => {
|
|
217
218
|
if (state.notice === msg) {
|
|
218
219
|
state.notice = "";
|
|
@@ -231,6 +232,7 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
231
232
|
note(`Could not load messages: ${err.message}`);
|
|
232
233
|
}
|
|
233
234
|
}
|
|
235
|
+
let keysChecked = false;
|
|
234
236
|
async function loadConversations() {
|
|
235
237
|
try {
|
|
236
238
|
const before = state.activeId;
|
|
@@ -243,9 +245,10 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
243
245
|
state.selected = Math.max(0, state.conversations.findIndex((c) => c.id === before));
|
|
244
246
|
}
|
|
245
247
|
redraw();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
if (!keysChecked) {
|
|
249
|
+
keysChecked = true;
|
|
250
|
+
if (await client.keyCheck() === "mismatch")
|
|
251
|
+
note(KEY_MISMATCH, { sticky: true });
|
|
249
252
|
}
|
|
250
253
|
} catch (err) {
|
|
251
254
|
state.loading = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/qryptchat",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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": {
|