@vex-chat/libvex 6.7.0 → 7.0.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/src/Storage.ts CHANGED
@@ -24,6 +24,11 @@ import type { EventEmitter } from "eventemitter3";
24
24
  * - Managing prekeys / one-time keys used for session setup
25
25
  * - Emitting lifecycle events (`ready`, `error`)
26
26
  */
27
+ export interface MessageUpdatePatch {
28
+ extra?: null | string | undefined;
29
+ message?: string | undefined;
30
+ }
31
+
27
32
  export interface Storage extends EventEmitter {
28
33
  /** Closes storage resources (connections, handles, transactions, etc.). */
29
34
  close: () => Promise<void>;
@@ -131,6 +136,12 @@ export interface Storage extends EventEmitter {
131
136
  preKeys: UnsavedPreKey[],
132
137
  oneTime: boolean,
133
138
  ) => Promise<PreKeysSQL[]>;
139
+
134
140
  /** Persists an encryption session. */
135
141
  saveSession: (session: Session) => Promise<void>;
142
+ /** Updates locally persisted message plaintext and/or encrypted extra by `mailID`. */
143
+ updateMessage: (
144
+ mailID: string,
145
+ patch: MessageUpdatePatch,
146
+ ) => Promise<boolean>;
136
147
  }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Message } from "../../index.js";
8
- import type { Storage } from "../../Storage.js";
8
+ import type { MessageUpdatePatch, Storage } from "../../Storage.js";
9
9
  import type {
10
10
  PreKeysCrypto,
11
11
  SessionCrypto,
@@ -276,6 +276,47 @@ export class MemoryStorage extends EventEmitter implements Storage {
276
276
  return Promise.resolve();
277
277
  }
278
278
 
279
+ async updateMessage(
280
+ mailID: string,
281
+ patch: MessageUpdatePatch,
282
+ ): Promise<boolean> {
283
+ const idx = this.messages.findIndex((m) => m.mailID === mailID);
284
+ if (idx < 0) {
285
+ return false;
286
+ }
287
+ if (
288
+ patch.message === undefined &&
289
+ !Object.prototype.hasOwnProperty.call(patch, "extra")
290
+ ) {
291
+ return false;
292
+ }
293
+ const current = this.messages[idx];
294
+ if (!current) {
295
+ return false;
296
+ }
297
+ const next = { ...current };
298
+ if (Object.prototype.hasOwnProperty.call(patch, "extra")) {
299
+ next.extra = patch.extra;
300
+ }
301
+ if (patch.message !== undefined) {
302
+ const fips = getCryptoProfile() === "fips";
303
+ const ct = fips
304
+ ? await xSecretboxAsync(
305
+ XUtils.decodeUTF8(patch.message),
306
+ XUtils.decodeHex(current.nonce),
307
+ this.atRestAesKey,
308
+ )
309
+ : xSecretbox(
310
+ XUtils.decodeUTF8(patch.message),
311
+ XUtils.decodeHex(current.nonce),
312
+ this.atRestAesKey,
313
+ );
314
+ next.message = XUtils.encodeHex(ct);
315
+ }
316
+ this.messages[idx] = next;
317
+ return true;
318
+ }
319
+
279
320
  private async decryptMessage(msg: Message): Promise<Message> {
280
321
  const copy = { ...msg };
281
322
  if (copy.decrypted) {
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ import {
9
+ createMessageDeleteBatchEventExtra,
10
+ createMessageDeleteEventExtra,
11
+ createMessageEmbedExtra,
12
+ createMessageUpdateEventExtra,
13
+ type MessageEmbed,
14
+ parseMessageExtra,
15
+ serializeMessageExtra,
16
+ } from "../messageExtra.js";
17
+
18
+ const imageAttachment = {
19
+ contentType: "image/svg+xml",
20
+ fileID: "file-1",
21
+ fileName: "summary.svg",
22
+ fileSize: 2048,
23
+ key: "secret",
24
+ };
25
+
26
+ describe("message extra", () => {
27
+ it("serializes and parses an encrypted media embed", () => {
28
+ const embed: MessageEmbed = {
29
+ blocks: [
30
+ {
31
+ attachment: imageAttachment,
32
+ mediaType: "svg",
33
+ title: "Workflow summary",
34
+ type: "media",
35
+ },
36
+ {
37
+ source: "message",
38
+ type: "markdown",
39
+ },
40
+ ],
41
+ display: "decorate",
42
+ iconAttachment: imageAttachment,
43
+ kind: "git.workflow",
44
+ title: "CI finished",
45
+ version: 1,
46
+ };
47
+
48
+ const extra = serializeMessageExtra({ embed, version: 1 });
49
+ expect(parseMessageExtra(extra).embed).toEqual(embed);
50
+ });
51
+
52
+ it("merges embed metadata with existing reactions", () => {
53
+ const current = JSON.stringify({
54
+ reactions: [
55
+ {
56
+ emoji: { kind: "unicode", value: "👍" },
57
+ userIDs: ["alice"],
58
+ },
59
+ ],
60
+ version: 1,
61
+ });
62
+ const extra = createMessageEmbedExtra(
63
+ {
64
+ display: "replace",
65
+ kind: "voice.transcript",
66
+ title: "Voice memo transcript",
67
+ version: 1,
68
+ },
69
+ current,
70
+ );
71
+ const parsed = parseMessageExtra(extra);
72
+
73
+ expect(parsed.embed?.kind).toBe("voice.transcript");
74
+ expect(parsed.reactions).toEqual([
75
+ {
76
+ emoji: { kind: "unicode", value: "👍" },
77
+ userIDs: ["alice"],
78
+ },
79
+ ]);
80
+ });
81
+
82
+ it("drops malformed known fields while preserving unknown metadata", () => {
83
+ const parsed = parseMessageExtra(
84
+ JSON.stringify({
85
+ embed: { display: "replace", kind: 123 },
86
+ messageDeleteEvent: { action: "delete" },
87
+ messageUpdateEvent: { action: "update", targetMailID: "m1" },
88
+ reactionEvent: { action: "toggle", targetMailID: "m1" },
89
+ vendor: { ok: true },
90
+ version: 999,
91
+ }),
92
+ );
93
+
94
+ expect(parsed).toEqual({
95
+ vendor: { ok: true },
96
+ version: 1,
97
+ });
98
+ });
99
+
100
+ it("serializes and parses message update and delete events", () => {
101
+ const updateExtra = createMessageUpdateEventExtra(
102
+ "m-target",
103
+ "edited text",
104
+ );
105
+ const deleteExtra = createMessageDeleteEventExtra("m-target");
106
+
107
+ expect(parseMessageExtra(updateExtra).messageUpdateEvent).toEqual({
108
+ action: "update",
109
+ message: "edited text",
110
+ targetMailID: "m-target",
111
+ });
112
+ expect(parseMessageExtra(deleteExtra).messageDeleteEvent).toEqual({
113
+ action: "delete",
114
+ targetMailID: "m-target",
115
+ });
116
+ });
117
+
118
+ it("serializes and parses batched message delete events", () => {
119
+ const extra = createMessageDeleteBatchEventExtra([
120
+ "m-first",
121
+ "m-second",
122
+ "m-first",
123
+ "",
124
+ ]);
125
+
126
+ expect(parseMessageExtra(extra).messageDeleteEvent).toEqual({
127
+ action: "delete",
128
+ targetMailIDs: ["m-first", "m-second"],
129
+ });
130
+ });
131
+
132
+ it("returns null for an empty serialized extra", () => {
133
+ expect(serializeMessageExtra({ version: 1 })).toBeNull();
134
+ });
135
+ });
package/src/index.ts CHANGED
@@ -48,12 +48,45 @@ export type {
48
48
  HttpRequestRecord,
49
49
  HttpResponse,
50
50
  } from "./http.js";
51
+ export {
52
+ createMessageDeleteBatchEventExtra,
53
+ createMessageDeleteEventExtra,
54
+ createMessageEmbedExtra,
55
+ createMessageUpdateEventExtra,
56
+ MESSAGE_EXTRA_VERSION,
57
+ parseMessageExtra,
58
+ serializeMessageExtra,
59
+ } from "./messageExtra.js";
60
+ export type {
61
+ EncryptedFileAttachmentReference,
62
+ MessageDeleteEvent,
63
+ MessageEmbed,
64
+ MessageEmbedAction,
65
+ MessageEmbedBlock,
66
+ MessageEmbedCodeBlock,
67
+ MessageEmbedDisplay,
68
+ MessageEmbedDividerBlock,
69
+ MessageEmbedField,
70
+ MessageEmbedFileBlock,
71
+ MessageEmbedGalleryBlock,
72
+ MessageEmbedMarkdownBlock,
73
+ MessageEmbedMediaBlock,
74
+ MessageEmbedMediaItem,
75
+ MessageEmbedMediaType,
76
+ MessageEmbedSource,
77
+ MessageEmbedTone,
78
+ MessageEmoji,
79
+ MessageExtra,
80
+ MessageReaction,
81
+ MessageReactionEvent,
82
+ MessageUpdateEvent,
83
+ } from "./messageExtra.js";
51
84
  export {
52
85
  clampLocalMessageRetentionDays,
53
86
  effectiveMessageRetentionHintDays,
54
87
  MAX_LOCAL_MESSAGE_RETENTION_DAYS,
55
88
  } from "./retention.js";
56
- export type { Storage } from "./Storage.js";
89
+ export type { MessageUpdatePatch, Storage } from "./Storage.js";
57
90
  export type {
58
91
  KeyPair,
59
92
  KeyStore,