@ih8e/express-cli 0.1.5 → 0.1.6
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 +1 -1
- package/dist/index.js +244 -88
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -26748,8 +26748,9 @@ var ApiClient = class {
|
|
|
26748
26748
|
const retryRes = await fetch(url, { ...options, headers: newHeaders });
|
|
26749
26749
|
return this.handleResponse(retryRes);
|
|
26750
26750
|
}
|
|
26751
|
+
throw new Error("Token expired and refresh failed. Please re-authenticate with `express auth qr`.");
|
|
26751
26752
|
}
|
|
26752
|
-
throw new Error(
|
|
26753
|
+
throw new Error(`API error 401: ${res.statusText}${text ? ` \u2014 ${text.slice(0, 200)}` : ""}`);
|
|
26753
26754
|
}
|
|
26754
26755
|
return this.handleResponse(res);
|
|
26755
26756
|
}
|
|
@@ -27725,50 +27726,99 @@ function createChatsCommand() {
|
|
|
27725
27726
|
init_esm_shims();
|
|
27726
27727
|
import { Command as Command8 } from "commander";
|
|
27727
27728
|
|
|
27728
|
-
// src/api/messaging.ts
|
|
27729
|
+
// src/api/messaging-ws.ts
|
|
27730
|
+
init_esm_shims();
|
|
27731
|
+
import WebSocket2 from "ws";
|
|
27732
|
+
import { randomBytes as randomBytes3, randomUUID as randomUUID4, createHash } from "crypto";
|
|
27733
|
+
import { readFile } from "fs/promises";
|
|
27734
|
+
import { basename } from "path";
|
|
27735
|
+
import nacl4 from "tweetnacl";
|
|
27736
|
+
import sodium2 from "libsodium-wrappers-sumo";
|
|
27737
|
+
import sharp from "sharp";
|
|
27738
|
+
|
|
27739
|
+
// src/api/file-service.ts
|
|
27729
27740
|
init_esm_shims();
|
|
27730
|
-
var
|
|
27741
|
+
var UPLOAD_PART_SIZE = 6291507;
|
|
27742
|
+
var IMAGE_MIME_TYPES = {
|
|
27743
|
+
".png": "image/png",
|
|
27744
|
+
".jpg": "image/jpeg",
|
|
27745
|
+
".jpeg": "image/jpeg",
|
|
27746
|
+
".gif": "image/gif",
|
|
27747
|
+
".webp": "image/webp"
|
|
27748
|
+
};
|
|
27749
|
+
function detectImageMimeType(fileName) {
|
|
27750
|
+
const ext = fileName.includes(".") ? "." + fileName.split(".").pop().toLowerCase() : "";
|
|
27751
|
+
return IMAGE_MIME_TYPES[ext] ?? null;
|
|
27752
|
+
}
|
|
27753
|
+
var FileServiceApi = class {
|
|
27731
27754
|
constructor(client) {
|
|
27732
27755
|
this.client = client;
|
|
27733
27756
|
}
|
|
27734
27757
|
client;
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
27742
|
-
|
|
27743
|
-
|
|
27744
|
-
|
|
27745
|
-
|
|
27746
|
-
|
|
27747
|
-
|
|
27748
|
-
|
|
27749
|
-
|
|
27750
|
-
|
|
27751
|
-
|
|
27752
|
-
|
|
27753
|
-
notification: {
|
|
27754
|
-
status: "ok",
|
|
27755
|
-
body: params.caption ?? ""
|
|
27758
|
+
/** Uploads an already-encrypted content blob plus an already-encrypted JPEG preview via the resumable protocol. */
|
|
27759
|
+
async uploadWithPreview(init, content, preview) {
|
|
27760
|
+
const resumableId = await this.initUpload(
|
|
27761
|
+
init,
|
|
27762
|
+
`content=${content.length};preview=${preview.data.length},${preview.mimeType}`
|
|
27763
|
+
);
|
|
27764
|
+
await this.uploadPart(resumableId, "content", content);
|
|
27765
|
+
const finalRes = await this.uploadPart(resumableId, "preview", preview.data);
|
|
27766
|
+
const body = await finalRes.json();
|
|
27767
|
+
return body.result;
|
|
27768
|
+
}
|
|
27769
|
+
async initUpload(init, shapes) {
|
|
27770
|
+
const res = await this.client.rawRequest("/api/v2/file_service/resumable", {
|
|
27771
|
+
method: "POST",
|
|
27772
|
+
headers: {
|
|
27773
|
+
"Content-Type": "application/json",
|
|
27774
|
+
"upload-part-size": String(UPLOAD_PART_SIZE),
|
|
27775
|
+
"upload-shapes": shapes
|
|
27756
27776
|
},
|
|
27757
|
-
|
|
27758
|
-
|
|
27759
|
-
|
|
27760
|
-
}
|
|
27761
|
-
}
|
|
27762
|
-
|
|
27777
|
+
body: JSON.stringify(init)
|
|
27778
|
+
});
|
|
27779
|
+
if (!res.ok) {
|
|
27780
|
+
throw new Error(`file upload init failed: ${res.status} ${await res.text()}`);
|
|
27781
|
+
}
|
|
27782
|
+
const resumableId = res.headers.get("upload-resumable-id");
|
|
27783
|
+
if (!resumableId) throw new Error("file upload init: missing upload-resumable-id header");
|
|
27784
|
+
return resumableId;
|
|
27785
|
+
}
|
|
27786
|
+
async uploadPart(resumableId, shape, data) {
|
|
27787
|
+
const res = await this.client.rawRequest(`/api/v2/file_service/resumable/${resumableId}`, {
|
|
27788
|
+
method: "POST",
|
|
27789
|
+
headers: {
|
|
27790
|
+
"Content-Type": "application/octet-stream",
|
|
27791
|
+
"upload-shape": shape,
|
|
27792
|
+
"upload-part-size": String(UPLOAD_PART_SIZE),
|
|
27793
|
+
"upload-range": `bytes=0-${data.length - 1}`
|
|
27794
|
+
},
|
|
27795
|
+
body: data
|
|
27796
|
+
});
|
|
27797
|
+
if (!res.ok) {
|
|
27798
|
+
throw new Error(`file upload part (${shape}) failed: ${res.status} ${await res.text()}`);
|
|
27799
|
+
}
|
|
27800
|
+
return res;
|
|
27763
27801
|
}
|
|
27764
27802
|
};
|
|
27765
27803
|
|
|
27766
|
-
// src/api/
|
|
27804
|
+
// src/api/file-crypto.ts
|
|
27767
27805
|
init_esm_shims();
|
|
27768
|
-
import WebSocket2 from "ws";
|
|
27769
|
-
import { randomBytes as randomBytes3, randomUUID as randomUUID4 } from "crypto";
|
|
27770
|
-
import nacl4 from "tweetnacl";
|
|
27771
27806
|
import sodium from "libsodium-wrappers-sumo";
|
|
27807
|
+
var FILE_CHUNK_SIZE = 2097152;
|
|
27808
|
+
async function encryptFileStream(data, key) {
|
|
27809
|
+
await sodium.ready;
|
|
27810
|
+
const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
|
|
27811
|
+
const parts = [header];
|
|
27812
|
+
for (let offset = 0; offset < data.length; offset += FILE_CHUNK_SIZE) {
|
|
27813
|
+
const chunk = data.subarray(offset, offset + FILE_CHUNK_SIZE);
|
|
27814
|
+
const isLast = offset + FILE_CHUNK_SIZE >= data.length;
|
|
27815
|
+
const tag = isLast ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL : sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
|
27816
|
+
parts.push(sodium.crypto_secretstream_xchacha20poly1305_push(state, chunk, null, tag));
|
|
27817
|
+
}
|
|
27818
|
+
return Buffer.concat(parts);
|
|
27819
|
+
}
|
|
27820
|
+
|
|
27821
|
+
// src/api/messaging-ws.ts
|
|
27772
27822
|
init_store();
|
|
27773
27823
|
function buildTextPayload(text, fromHuid, chatId) {
|
|
27774
27824
|
return JSON.stringify({
|
|
@@ -27784,12 +27834,49 @@ function buildTextPayload(text, fromHuid, chatId) {
|
|
|
27784
27834
|
body: text
|
|
27785
27835
|
});
|
|
27786
27836
|
}
|
|
27837
|
+
function buildImagePayload(params) {
|
|
27838
|
+
return JSON.stringify({
|
|
27839
|
+
type: "image",
|
|
27840
|
+
msg_id: randomUUID4(),
|
|
27841
|
+
from: params.fromHuid,
|
|
27842
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27843
|
+
group_chat_id: params.chatId,
|
|
27844
|
+
lat: 0,
|
|
27845
|
+
lng: 0,
|
|
27846
|
+
stealth_forwarding: false,
|
|
27847
|
+
payload: {
|
|
27848
|
+
file: params.contentPath,
|
|
27849
|
+
file_name: params.fileName,
|
|
27850
|
+
file_size: params.fileSize,
|
|
27851
|
+
file_hash: params.fileHash,
|
|
27852
|
+
file_mime_type: params.mimeType,
|
|
27853
|
+
chunk_size: FILE_CHUNK_SIZE,
|
|
27854
|
+
file_encryption_algo: "stream",
|
|
27855
|
+
file_preview: params.previewPath,
|
|
27856
|
+
file_preview_width: params.previewWidth,
|
|
27857
|
+
file_preview_height: params.previewHeight,
|
|
27858
|
+
blur_preview_file: params.blurPreviewDataUri,
|
|
27859
|
+
file_id: params.fileId
|
|
27860
|
+
},
|
|
27861
|
+
body: params.caption
|
|
27862
|
+
});
|
|
27863
|
+
}
|
|
27787
27864
|
function payloadAad(chatId, syncId) {
|
|
27788
27865
|
return new Uint8Array(Buffer.from(`${chatId}:${syncId}`));
|
|
27789
27866
|
}
|
|
27867
|
+
function wrapKeyForRecipients(symmetricKey, publicKeys, ctsKey) {
|
|
27868
|
+
return publicKeys.map((kdcKey) => {
|
|
27869
|
+
const recipientPubKey = new Uint8Array(Buffer.from(kdcKey.body, "base64"));
|
|
27870
|
+
const nonce = randomBytes3(nacl4.box.nonceLength);
|
|
27871
|
+
const ciphertext = nacl4.box(symmetricKey, nonce, recipientPubKey, ctsKey.privateKey);
|
|
27872
|
+
if (!ciphertext) throw new Error(`Failed to encrypt key for ${kdcKey.id}`);
|
|
27873
|
+
const combined = Buffer.concat([nonce, Buffer.from(ciphertext)]);
|
|
27874
|
+
return { key_id: kdcKey.id, key: combined.toString("base64"), algo: "xsalsa20:xchacha20_aead_ietf" };
|
|
27875
|
+
});
|
|
27876
|
+
}
|
|
27790
27877
|
async function sendMessageViaWebSocket(params) {
|
|
27791
27878
|
const { client, chatId, body, timeoutMs = 2e4 } = params;
|
|
27792
|
-
await
|
|
27879
|
+
await sodium2.ready;
|
|
27793
27880
|
const apigwKeys = loadApigwKeys();
|
|
27794
27881
|
if (!apigwKeys) throw new Error("No apigw keys. Run 'express auth login' first.");
|
|
27795
27882
|
const config = loadConfig();
|
|
@@ -27805,27 +27892,11 @@ async function sendMessageViaWebSocket(params) {
|
|
|
27805
27892
|
const publicKeys = kdcKeys.filter((k) => k.kind === "cts");
|
|
27806
27893
|
const symmetricKey = randomBytes3(32);
|
|
27807
27894
|
const syncId = randomUUID4();
|
|
27808
|
-
const encryptedKeys =
|
|
27809
|
-
const recipientPubKey = new Uint8Array(Buffer.from(kdcKey.body, "base64"));
|
|
27810
|
-
const nonce = randomBytes3(nacl4.box.nonceLength);
|
|
27811
|
-
const ciphertext2 = nacl4.box(
|
|
27812
|
-
symmetricKey,
|
|
27813
|
-
nonce,
|
|
27814
|
-
recipientPubKey,
|
|
27815
|
-
ctsKey.privateKey
|
|
27816
|
-
);
|
|
27817
|
-
if (!ciphertext2) throw new Error(`Failed to encrypt key for ${kdcKey.id}`);
|
|
27818
|
-
const combined = Buffer.concat([nonce, Buffer.from(ciphertext2)]);
|
|
27819
|
-
return {
|
|
27820
|
-
key_id: kdcKey.id,
|
|
27821
|
-
key: combined.toString("base64"),
|
|
27822
|
-
algo: "xsalsa20:xchacha20_aead_ietf"
|
|
27823
|
-
};
|
|
27824
|
-
});
|
|
27895
|
+
const encryptedKeys = wrapKeyForRecipients(symmetricKey, publicKeys, ctsKey);
|
|
27825
27896
|
const selfHuid = (await new UserApi(client).getSelfProfile()).user_huid;
|
|
27826
27897
|
const plaintext = new Uint8Array(Buffer.from(buildTextPayload(body, selfHuid, chatId)));
|
|
27827
|
-
const msgNonce = randomBytes3(
|
|
27828
|
-
const ciphertext =
|
|
27898
|
+
const msgNonce = randomBytes3(sodium2.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
27899
|
+
const ciphertext = sodium2.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
|
27829
27900
|
plaintext,
|
|
27830
27901
|
payloadAad(chatId, syncId),
|
|
27831
27902
|
null,
|
|
@@ -27856,6 +27927,114 @@ async function sendMessageViaWebSocket(params) {
|
|
|
27856
27927
|
timeoutMs
|
|
27857
27928
|
});
|
|
27858
27929
|
}
|
|
27930
|
+
async function buildPreview(imageBytes) {
|
|
27931
|
+
const data = await sharp(imageBytes).resize({ width: 300, height: 300, fit: "inside", withoutEnlargement: true }).jpeg({ quality: 90 }).toBuffer();
|
|
27932
|
+
const blurData = data;
|
|
27933
|
+
const { width, height } = await sharp(data).metadata();
|
|
27934
|
+
return { data, width: width ?? 0, height: height ?? 0, blurData };
|
|
27935
|
+
}
|
|
27936
|
+
async function sendImageViaWebSocket(params) {
|
|
27937
|
+
const { client, chatId, filePath, caption = "", timeoutMs = 3e4 } = params;
|
|
27938
|
+
const fileName = basename(filePath);
|
|
27939
|
+
const mimeType = detectImageMimeType(fileName);
|
|
27940
|
+
if (!mimeType) {
|
|
27941
|
+
throw new Error(
|
|
27942
|
+
`Unsupported file type for '${fileName}'. Only images are currently supported: .png, .jpg, .jpeg, .gif, .webp`
|
|
27943
|
+
);
|
|
27944
|
+
}
|
|
27945
|
+
await sodium2.ready;
|
|
27946
|
+
const apigwKeys = loadApigwKeys();
|
|
27947
|
+
if (!apigwKeys) throw new Error("No apigw keys. Run 'express auth login' first.");
|
|
27948
|
+
const config = loadConfig();
|
|
27949
|
+
const host = new URL(getBaseUrl(config)).hostname;
|
|
27950
|
+
const webOrigin = getWebOrigin(config);
|
|
27951
|
+
const ctsToken = getAuthToken();
|
|
27952
|
+
const ctsKey = apigwKeys.ctsKey ?? apigwKeys.encryptionKey;
|
|
27953
|
+
const encKeyId = ctsKey.keyId;
|
|
27954
|
+
const participantKeyIds = await getChatKeyIds(host, webOrigin, ctsToken, encKeyId, chatId);
|
|
27955
|
+
const kdcKeys = await client.get(
|
|
27956
|
+
`/api/v1/kdc/keys/?ids=${participantKeyIds.join(",")}`
|
|
27957
|
+
) ?? [];
|
|
27958
|
+
const publicKeys = kdcKeys.filter((k) => k.kind === "cts");
|
|
27959
|
+
const originalBytes = await readFile(filePath);
|
|
27960
|
+
const fileHash = createHash("sha256").update(originalBytes).digest("base64");
|
|
27961
|
+
const preview = await buildPreview(originalBytes);
|
|
27962
|
+
const blurPreviewDataUri = `data:image/jpeg;base64,${preview.blurData.toString("base64")}`;
|
|
27963
|
+
const fileKey = randomBytes3(32);
|
|
27964
|
+
const fileKeys = wrapKeyForRecipients(fileKey, publicKeys, ctsKey);
|
|
27965
|
+
const syncId = randomUUID4();
|
|
27966
|
+
const encryptedContent = await encryptFileStream(originalBytes, fileKey);
|
|
27967
|
+
const encryptedPreview = await encryptFileStream(preview.data, fileKey);
|
|
27968
|
+
const uploaded = await new FileServiceApi(client).uploadWithPreview(
|
|
27969
|
+
{
|
|
27970
|
+
subject: "groupchat_file",
|
|
27971
|
+
subject_id: chatId,
|
|
27972
|
+
visible: true,
|
|
27973
|
+
file_name: fileName,
|
|
27974
|
+
mime_type: mimeType,
|
|
27975
|
+
meta: {
|
|
27976
|
+
sync_id: syncId,
|
|
27977
|
+
kind: "media",
|
|
27978
|
+
chunk_size: FILE_CHUNK_SIZE,
|
|
27979
|
+
file_encryption_algo: "stream",
|
|
27980
|
+
sender_key_id: encKeyId,
|
|
27981
|
+
keys: fileKeys,
|
|
27982
|
+
file_hash: fileHash
|
|
27983
|
+
}
|
|
27984
|
+
},
|
|
27985
|
+
encryptedContent,
|
|
27986
|
+
{ data: encryptedPreview, mimeType: "image/jpeg" }
|
|
27987
|
+
);
|
|
27988
|
+
const selfHuid = (await new UserApi(client).getSelfProfile()).user_huid;
|
|
27989
|
+
const plaintext = new Uint8Array(Buffer.from(buildImagePayload({
|
|
27990
|
+
fromHuid: selfHuid,
|
|
27991
|
+
chatId,
|
|
27992
|
+
caption,
|
|
27993
|
+
fileName,
|
|
27994
|
+
fileSize: originalBytes.length,
|
|
27995
|
+
fileHash,
|
|
27996
|
+
mimeType,
|
|
27997
|
+
contentPath: uploaded.content,
|
|
27998
|
+
previewPath: uploaded.content_shapes.preview ?? "",
|
|
27999
|
+
previewWidth: preview.width,
|
|
28000
|
+
previewHeight: preview.height,
|
|
28001
|
+
blurPreviewDataUri,
|
|
28002
|
+
fileId: uploaded.id
|
|
28003
|
+
})));
|
|
28004
|
+
const msgKey = randomBytes3(32);
|
|
28005
|
+
const encryptedKeys = wrapKeyForRecipients(msgKey, publicKeys, ctsKey);
|
|
28006
|
+
const msgNonce = randomBytes3(sodium2.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
28007
|
+
const ciphertext = sodium2.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
|
28008
|
+
plaintext,
|
|
28009
|
+
payloadAad(chatId, syncId),
|
|
28010
|
+
null,
|
|
28011
|
+
new Uint8Array(msgNonce),
|
|
28012
|
+
new Uint8Array(msgKey)
|
|
28013
|
+
);
|
|
28014
|
+
const encryptedPayload = Buffer.concat([msgNonce, Buffer.from(ciphertext)]).toString("base64");
|
|
28015
|
+
const signingKey = apigwKeys.signingKey;
|
|
28016
|
+
const signBytes = signEd25519(
|
|
28017
|
+
signingKey.privateKey,
|
|
28018
|
+
new Uint8Array(Buffer.from(encryptedPayload, "utf8"))
|
|
28019
|
+
);
|
|
28020
|
+
const signature = {
|
|
28021
|
+
sign: Buffer.from(signBytes).toString("base64"),
|
|
28022
|
+
sign_key_id: signingKey.keyId,
|
|
28023
|
+
sign_algo: "ed25519"
|
|
28024
|
+
};
|
|
28025
|
+
return sendMessageNew({
|
|
28026
|
+
host,
|
|
28027
|
+
webOrigin,
|
|
28028
|
+
ctsToken,
|
|
28029
|
+
encKeyId,
|
|
28030
|
+
chatId,
|
|
28031
|
+
syncId,
|
|
28032
|
+
encryptedKeys,
|
|
28033
|
+
encryptedPayload,
|
|
28034
|
+
signature,
|
|
28035
|
+
timeoutMs
|
|
28036
|
+
});
|
|
28037
|
+
}
|
|
27859
28038
|
async function getChatKeyIds(host, webOrigin, ctsToken, encKeyId, chatId) {
|
|
27860
28039
|
const wsUrl = `wss://${host}/socket/user/websocket?vsn=1.0.0&auto_join=true&key_id=${encKeyId}&version=6&background=false&voex_unencrypted=true&instance_id=${randomUUID4()}`;
|
|
27861
28040
|
return new Promise((resolve, reject) => {
|
|
@@ -28024,7 +28203,6 @@ Be more specific.`);
|
|
|
28024
28203
|
}
|
|
28025
28204
|
|
|
28026
28205
|
// src/cli/send.ts
|
|
28027
|
-
import { readFileSync } from "fs";
|
|
28028
28206
|
function createSendCommand() {
|
|
28029
28207
|
const cmd = new Command8("send");
|
|
28030
28208
|
cmd.description("Send messages and files");
|
|
@@ -28039,33 +28217,11 @@ function createSendCommand() {
|
|
|
28039
28217
|
process.exit(1);
|
|
28040
28218
|
}
|
|
28041
28219
|
});
|
|
28042
|
-
cmd.command("file <chat-id> <file-path>").description("Send a file to a chat").option("--caption <caption>", "File caption").option("--host <host>", "eXpress host").option("-o, --output <format>", "Output format", "json").action(async (
|
|
28220
|
+
cmd.command("file <chat-id-or-name> <file-path>").description("Send a file to a chat (chat ID or partial name). Currently images only: .png, .jpg, .jpeg, .gif, .webp").option("--caption <caption>", "File caption").option("--host <host>", "eXpress host").option("-o, --output <format>", "Output format", "json").action(async (chatIdOrName, filePath, opts) => {
|
|
28043
28221
|
try {
|
|
28044
|
-
const data = readFileSync(filePath);
|
|
28045
|
-
const fileName = filePath.split("/").pop() ?? "file";
|
|
28046
|
-
const mimeMap = {
|
|
28047
|
-
".png": "image/png",
|
|
28048
|
-
".jpg": "image/jpeg",
|
|
28049
|
-
".jpeg": "image/jpeg",
|
|
28050
|
-
".gif": "image/gif",
|
|
28051
|
-
".pdf": "application/pdf",
|
|
28052
|
-
".txt": "text/plain",
|
|
28053
|
-
".json": "application/json",
|
|
28054
|
-
".csv": "text/csv",
|
|
28055
|
-
".zip": "application/zip"
|
|
28056
|
-
};
|
|
28057
|
-
const ext = fileName.includes(".") ? "." + fileName.split(".").pop().toLowerCase() : "";
|
|
28058
|
-
const mime = mimeMap[ext] ?? "application/octet-stream";
|
|
28059
|
-
const base64 = data.toString("base64");
|
|
28060
|
-
const dataUri = `data:${mime};base64,${base64}`;
|
|
28061
28222
|
const client = new ApiClient(opts.host ? { host: opts.host } : void 0);
|
|
28062
|
-
const
|
|
28063
|
-
const result = await
|
|
28064
|
-
groupChatId: chatId,
|
|
28065
|
-
fileName,
|
|
28066
|
-
fileData: dataUri,
|
|
28067
|
-
caption: opts.caption
|
|
28068
|
-
});
|
|
28223
|
+
const chatId = await resolveChatId(client, chatIdOrName);
|
|
28224
|
+
const result = await sendImageViaWebSocket({ client, chatId, filePath, caption: opts.caption });
|
|
28069
28225
|
console.log(formatOutput(result, opts.output));
|
|
28070
28226
|
} catch (err) {
|
|
28071
28227
|
console.error(`Error: ${err.message}`);
|
|
@@ -28191,7 +28347,7 @@ init_store();
|
|
|
28191
28347
|
// src/api/decrypt.ts
|
|
28192
28348
|
init_esm_shims();
|
|
28193
28349
|
import nacl5 from "tweetnacl";
|
|
28194
|
-
import
|
|
28350
|
+
import sodium3 from "libsodium-wrappers-sumo";
|
|
28195
28351
|
function decryptMessage(msg, ctsPrivateKey, myKeyId, keyMap, apigwKeys) {
|
|
28196
28352
|
const encKey = msg.key;
|
|
28197
28353
|
if (encKey.key_id !== myKeyId) {
|
|
@@ -28205,10 +28361,10 @@ function decryptMessage(msg, ctsPrivateKey, myKeyId, keyMap, apigwKeys) {
|
|
|
28205
28361
|
const symmetricKey = nacl5.box.open(ciphertext, nonce, senderPubKey, ctsPrivateKey);
|
|
28206
28362
|
if (!symmetricKey) throw new Error("nacl.box.open failed \u2014 wrong key pair");
|
|
28207
28363
|
const payloadRaw = Uint8Array.from(Buffer.from(msg.payload, "base64"));
|
|
28208
|
-
const msgNonce = payloadRaw.slice(0,
|
|
28209
|
-
const msgCiphertext = payloadRaw.slice(
|
|
28364
|
+
const msgNonce = payloadRaw.slice(0, sodium3.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
28365
|
+
const msgCiphertext = payloadRaw.slice(sodium3.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
28210
28366
|
const aad = new Uint8Array(Buffer.from(`${msg.group_chat_id}:${msg.sync_id}`));
|
|
28211
|
-
const plaintext =
|
|
28367
|
+
const plaintext = sodium3.crypto_aead_xchacha20poly1305_ietf_decrypt(null, msgCiphertext, aad, msgNonce, symmetricKey);
|
|
28212
28368
|
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
28213
28369
|
}
|
|
28214
28370
|
function toDecrypted(msg, payload) {
|
|
@@ -28239,7 +28395,7 @@ function getSenderPublicKey(msg, keyMap, apigwKeys) {
|
|
|
28239
28395
|
return null;
|
|
28240
28396
|
}
|
|
28241
28397
|
async function decryptMessages(events, apigwKeys, client = new ApiClient()) {
|
|
28242
|
-
await
|
|
28398
|
+
await sodium3.ready;
|
|
28243
28399
|
const ctsKey = apigwKeys.ctsKey ?? apigwKeys.encryptionKey;
|
|
28244
28400
|
const messages = events.filter((e) => e.event_type === "message_new" && e.payload && e.key);
|
|
28245
28401
|
if (messages.length === 0) return [];
|
|
@@ -38587,7 +38743,7 @@ init_esm_shims();
|
|
|
38587
38743
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
38588
38744
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
38589
38745
|
import { z as z2 } from "zod";
|
|
38590
|
-
import { writeFileSync as writeFileSync3, readFileSync as
|
|
38746
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
38591
38747
|
import { tmpdir } from "os";
|
|
38592
38748
|
import { join } from "path";
|
|
38593
38749
|
init_store();
|
|
@@ -38828,7 +38984,7 @@ Scan with the eXpress mobile app.
|
|
|
38828
38984
|
if (!material || !poll) {
|
|
38829
38985
|
let saved = null;
|
|
38830
38986
|
try {
|
|
38831
|
-
saved = JSON.parse(
|
|
38987
|
+
saved = JSON.parse(readFileSync2(QR_MATERIAL_FILE, "utf8"));
|
|
38832
38988
|
} catch {
|
|
38833
38989
|
}
|
|
38834
38990
|
if (!saved) return ok("No pending QR session. Call auth_qr_start first.");
|
|
@@ -38885,7 +39041,7 @@ function createMcpCommand() {
|
|
|
38885
39041
|
// src/cli/root.ts
|
|
38886
39042
|
function createRootCommand() {
|
|
38887
39043
|
const program2 = new Command15();
|
|
38888
|
-
program2.name("express-cli").description("CLI client for eXpress Chat").version("0.1.
|
|
39044
|
+
program2.name("express-cli").description("CLI client for eXpress Chat").version("0.1.6");
|
|
38889
39045
|
program2.addCommand(createAuthCommand());
|
|
38890
39046
|
program2.addCommand(createApiCommand());
|
|
38891
39047
|
program2.addCommand(createConfigCommand());
|