@vex-chat/libvex 1.1.0 → 4.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.
Files changed (149) hide show
  1. package/README.md +104 -41
  2. package/dist/Client.d.ts +473 -560
  3. package/dist/Client.d.ts.map +1 -0
  4. package/dist/Client.js +1486 -1551
  5. package/dist/Client.js.map +1 -1
  6. package/dist/Storage.d.ts +111 -0
  7. package/dist/Storage.d.ts.map +1 -0
  8. package/dist/Storage.js +2 -0
  9. package/dist/Storage.js.map +1 -0
  10. package/dist/__tests__/harness/memory-storage.d.ts +29 -27
  11. package/dist/__tests__/harness/memory-storage.d.ts.map +1 -0
  12. package/dist/__tests__/harness/memory-storage.js +120 -109
  13. package/dist/__tests__/harness/memory-storage.js.map +1 -1
  14. package/dist/codec.d.ts +44 -0
  15. package/dist/codec.d.ts.map +1 -0
  16. package/dist/codec.js +51 -0
  17. package/dist/codec.js.map +1 -0
  18. package/dist/codecs.d.ts +201 -0
  19. package/dist/codecs.d.ts.map +1 -0
  20. package/dist/codecs.js +67 -0
  21. package/dist/codecs.js.map +1 -0
  22. package/dist/index.d.ts +6 -5
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/keystore/memory.d.ts +5 -4
  27. package/dist/keystore/memory.d.ts.map +1 -0
  28. package/dist/keystore/memory.js +9 -7
  29. package/dist/keystore/memory.js.map +1 -1
  30. package/dist/keystore/node.d.ts +8 -6
  31. package/dist/keystore/node.d.ts.map +1 -0
  32. package/dist/keystore/node.js +47 -22
  33. package/dist/keystore/node.js.map +1 -1
  34. package/dist/preset/common.d.ts +7 -0
  35. package/dist/preset/common.d.ts.map +1 -0
  36. package/dist/preset/common.js +2 -0
  37. package/dist/preset/common.js.map +1 -0
  38. package/dist/preset/node.d.ts +4 -7
  39. package/dist/preset/node.d.ts.map +1 -0
  40. package/dist/preset/node.js +4 -11
  41. package/dist/preset/node.js.map +1 -1
  42. package/dist/preset/test.d.ts +4 -5
  43. package/dist/preset/test.d.ts.map +1 -0
  44. package/dist/preset/test.js +3 -20
  45. package/dist/preset/test.js.map +1 -1
  46. package/dist/storage/node.d.ts +3 -3
  47. package/dist/storage/node.d.ts.map +1 -0
  48. package/dist/storage/node.js +4 -10
  49. package/dist/storage/node.js.map +1 -1
  50. package/dist/storage/schema.d.ts +55 -54
  51. package/dist/storage/schema.d.ts.map +1 -0
  52. package/dist/storage/sqlite.d.ts +41 -28
  53. package/dist/storage/sqlite.d.ts.map +1 -0
  54. package/dist/storage/sqlite.js +339 -297
  55. package/dist/storage/sqlite.js.map +1 -1
  56. package/dist/transport/types.d.ts +17 -16
  57. package/dist/transport/types.d.ts.map +1 -0
  58. package/dist/transport/websocket.d.ts +26 -0
  59. package/dist/transport/websocket.d.ts.map +1 -0
  60. package/dist/transport/websocket.js +83 -0
  61. package/dist/transport/websocket.js.map +1 -0
  62. package/dist/types/crypto.d.ts +38 -0
  63. package/dist/types/crypto.d.ts.map +1 -0
  64. package/dist/types/crypto.js +9 -0
  65. package/dist/types/crypto.js.map +1 -0
  66. package/dist/types/identity.d.ts +22 -0
  67. package/dist/types/identity.d.ts.map +1 -0
  68. package/dist/types/identity.js +6 -0
  69. package/dist/types/identity.js.map +1 -0
  70. package/dist/types/index.d.ts +3 -0
  71. package/dist/types/index.d.ts.map +1 -0
  72. package/dist/types/index.js +2 -0
  73. package/dist/types/index.js.map +1 -0
  74. package/dist/utils/capitalize.d.ts +1 -0
  75. package/dist/utils/capitalize.d.ts.map +1 -0
  76. package/dist/utils/formatBytes.d.ts +1 -0
  77. package/dist/utils/formatBytes.d.ts.map +1 -0
  78. package/dist/utils/formatBytes.js +3 -1
  79. package/dist/utils/formatBytes.js.map +1 -1
  80. package/dist/utils/sqlSessionToCrypto.d.ts +4 -2
  81. package/dist/utils/sqlSessionToCrypto.d.ts.map +1 -0
  82. package/dist/utils/sqlSessionToCrypto.js +5 -5
  83. package/dist/utils/sqlSessionToCrypto.js.map +1 -1
  84. package/dist/utils/uint8uuid.d.ts +1 -4
  85. package/dist/utils/uint8uuid.d.ts.map +1 -0
  86. package/dist/utils/uint8uuid.js +1 -7
  87. package/dist/utils/uint8uuid.js.map +1 -1
  88. package/package.json +74 -91
  89. package/src/Client.ts +3086 -0
  90. package/{dist/IStorage.d.ts → src/Storage.ts} +70 -62
  91. package/src/__tests__/codec.test.ts +256 -0
  92. package/src/__tests__/ghost.png +0 -0
  93. package/src/__tests__/harness/fixtures.ts +22 -0
  94. package/src/__tests__/harness/memory-storage.ts +254 -0
  95. package/src/__tests__/harness/platform-transports.ts +4 -0
  96. package/src/__tests__/harness/poison-node-imports.ts +107 -0
  97. package/src/__tests__/harness/shared-suite.ts +426 -0
  98. package/src/__tests__/platform-browser.test.ts +14 -0
  99. package/src/__tests__/platform-node.test.ts +9 -0
  100. package/src/__tests__/triggered.png +0 -0
  101. package/src/codec.ts +68 -0
  102. package/src/codecs.ts +101 -0
  103. package/src/index.ts +40 -0
  104. package/src/keystore/memory.ts +30 -0
  105. package/src/keystore/node.ts +102 -0
  106. package/src/preset/common.ts +7 -0
  107. package/src/preset/node.ts +18 -0
  108. package/src/preset/test.ts +20 -0
  109. package/src/storage/node.ts +22 -0
  110. package/src/storage/schema.ts +94 -0
  111. package/src/storage/sqlite.ts +655 -0
  112. package/src/transport/types.ts +22 -0
  113. package/src/transport/websocket.ts +106 -0
  114. package/src/types/crypto.ts +42 -0
  115. package/src/types/identity.ts +23 -0
  116. package/src/types/index.ts +9 -0
  117. package/src/utils/capitalize.ts +6 -0
  118. package/src/utils/formatBytes.ts +15 -0
  119. package/src/utils/sqlSessionToCrypto.ts +16 -0
  120. package/src/utils/uint8uuid.ts +7 -0
  121. package/dist/IStorage.js +0 -2
  122. package/dist/IStorage.js.map +0 -1
  123. package/dist/keystore/types.d.ts +0 -4
  124. package/dist/keystore/types.js +0 -2
  125. package/dist/keystore/types.js.map +0 -1
  126. package/dist/preset/expo.d.ts +0 -2
  127. package/dist/preset/expo.js +0 -39
  128. package/dist/preset/expo.js.map +0 -1
  129. package/dist/preset/tauri.d.ts +0 -2
  130. package/dist/preset/tauri.js +0 -36
  131. package/dist/preset/tauri.js.map +0 -1
  132. package/dist/preset/types.d.ts +0 -14
  133. package/dist/preset/types.js +0 -2
  134. package/dist/preset/types.js.map +0 -1
  135. package/dist/storage/expo.d.ts +0 -3
  136. package/dist/storage/expo.js +0 -18
  137. package/dist/storage/expo.js.map +0 -1
  138. package/dist/storage/tauri.d.ts +0 -3
  139. package/dist/storage/tauri.js +0 -21
  140. package/dist/storage/tauri.js.map +0 -1
  141. package/dist/transport/browser.d.ts +0 -17
  142. package/dist/transport/browser.js +0 -56
  143. package/dist/transport/browser.js.map +0 -1
  144. package/dist/utils/constants.d.ts +0 -8
  145. package/dist/utils/constants.js +0 -9
  146. package/dist/utils/constants.js.map +0 -1
  147. package/dist/utils/createLogger.d.ts +0 -5
  148. package/dist/utils/createLogger.js +0 -27
  149. package/dist/utils/createLogger.js.map +0 -1
@@ -0,0 +1,254 @@
1
+ import type { Message } from "../../index.js";
2
+ import type { Storage } from "../../Storage.js";
3
+ import type {
4
+ PreKeysCrypto,
5
+ SessionCrypto,
6
+ UnsavedPreKey,
7
+ } from "../../types/index.js";
8
+ import type { Device, PreKeysSQL, SessionSQL } from "@vex-chat/types";
9
+
10
+ import {
11
+ type KeyPair,
12
+ xBoxKeyPairFromSecret,
13
+ XKeyConvert,
14
+ xSecretbox,
15
+ xSecretboxOpen,
16
+ xSignKeyPairFromSecret,
17
+ XUtils,
18
+ } from "@vex-chat/crypto";
19
+
20
+ /**
21
+ * Minimal in-memory Storage for browser/RN platform tests.
22
+ *
23
+ * Uses eventemitter3 (browser-safe) instead of Node's events module.
24
+ * No persistence — just enough for the register/login/connect/DM test flow.
25
+ */
26
+ import { EventEmitter } from "eventemitter3";
27
+
28
+ export class MemoryStorage extends EventEmitter implements Storage {
29
+ public ready = false;
30
+ private readonly devices: Device[] = [];
31
+ private readonly idKeys: KeyPair;
32
+ private messages: Message[] = [];
33
+ private nextOtkIndex = 1;
34
+ private nextPreKeyIndex = 1;
35
+ private oneTimeKeys: any[] = [];
36
+ private preKeys: any[] = [];
37
+ private sessions: SessionSQL[] = [];
38
+
39
+ constructor(SK: string) {
40
+ super();
41
+ const idKeys = XKeyConvert.convertKeyPair(
42
+ xSignKeyPairFromSecret(XUtils.decodeHex(SK)),
43
+ );
44
+ if (!idKeys) throw new Error("Can't convert SK!");
45
+ this.idKeys = idKeys;
46
+ }
47
+
48
+ close(): Promise<void> {
49
+ return Promise.resolve();
50
+ }
51
+
52
+ deleteHistory(channelOrUserID: string): Promise<void> {
53
+ this.messages = this.messages.filter(
54
+ (m) =>
55
+ m.group !== channelOrUserID &&
56
+ m.authorID !== channelOrUserID &&
57
+ m.readerID !== channelOrUserID,
58
+ );
59
+ return Promise.resolve();
60
+ }
61
+
62
+ deleteMessage(mailID: string): Promise<void> {
63
+ this.messages = this.messages.filter((m) => m.mailID !== mailID);
64
+ return Promise.resolve();
65
+ }
66
+
67
+ deleteOneTimeKey(index: number): Promise<void> {
68
+ this.oneTimeKeys = this.oneTimeKeys.filter((k) => k.index !== index);
69
+ return Promise.resolve();
70
+ }
71
+
72
+ getAllSessions(): Promise<SessionSQL[]> {
73
+ return Promise.resolve(
74
+ this.sessions.map((s) => ({
75
+ ...s,
76
+ verified: s.verified,
77
+ })),
78
+ );
79
+ }
80
+
81
+ getDevice(deviceID: string): Promise<Device | null> {
82
+ return Promise.resolve(
83
+ this.devices.find((d) => d.deviceID === deviceID) ?? null,
84
+ );
85
+ }
86
+
87
+ getGroupHistory(channelID: string): Promise<Message[]> {
88
+ return Promise.resolve(
89
+ this.messages
90
+ .filter((m) => m.group === channelID)
91
+ .map((m) => this.decryptMessage(m)),
92
+ );
93
+ }
94
+
95
+ getMessageHistory(userID: string): Promise<Message[]> {
96
+ return Promise.resolve(
97
+ this.messages
98
+ .filter(
99
+ (m) =>
100
+ (m.direction === "incoming" &&
101
+ m.authorID === userID &&
102
+ !m.group) ||
103
+ (m.direction === "outgoing" &&
104
+ m.readerID === userID &&
105
+ !m.group),
106
+ )
107
+ .map((m) => this.decryptMessage(m)),
108
+ );
109
+ }
110
+
111
+ getOneTimeKey(index: number): Promise<null | PreKeysCrypto> {
112
+ const otk = this.oneTimeKeys.find((k) => k.index === index);
113
+ if (!otk || !otk.privateKey) return Promise.resolve(null);
114
+ return Promise.resolve({
115
+ index: otk.index,
116
+ keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(otk.privateKey)),
117
+ signature: XUtils.decodeHex(otk.signature),
118
+ });
119
+ }
120
+
121
+ getPreKeys(): Promise<null | PreKeysCrypto> {
122
+ if (this.preKeys.length === 0) return Promise.resolve(null);
123
+ const pk = this.preKeys[0];
124
+ if (!pk.privateKey) return Promise.resolve(null);
125
+ return Promise.resolve({
126
+ index: pk.index,
127
+ keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(pk.privateKey)),
128
+ signature: XUtils.decodeHex(pk.signature),
129
+ });
130
+ }
131
+
132
+ getSessionByDeviceID(deviceID: string): Promise<null | SessionCrypto> {
133
+ const s = this.sessions.find((s) => s.deviceID === deviceID);
134
+ if (!s) return Promise.resolve(null);
135
+ return Promise.resolve(this.sqlToCrypto(s));
136
+ }
137
+
138
+ getSessionByPublicKey(
139
+ publicKey: Uint8Array,
140
+ ): Promise<null | SessionCrypto> {
141
+ const hex = XUtils.encodeHex(publicKey);
142
+ const s = this.sessions.find((s) => s.publicKey === hex);
143
+ if (!s) return Promise.resolve(null);
144
+ return Promise.resolve(this.sqlToCrypto(s));
145
+ }
146
+
147
+ init(): Promise<void> {
148
+ this.ready = true;
149
+ this.emit("ready");
150
+ return Promise.resolve();
151
+ }
152
+
153
+ markSessionUsed(sessionID: string): Promise<void> {
154
+ const s = this.sessions.find((s) => s.sessionID === sessionID);
155
+ if (s) s.lastUsed = new Date().toISOString();
156
+ return Promise.resolve();
157
+ }
158
+
159
+ markSessionVerified(sessionID: string): Promise<void> {
160
+ const s = this.sessions.find((s) => s.sessionID === sessionID);
161
+ if (s) s.verified = true;
162
+ return Promise.resolve();
163
+ }
164
+
165
+ purgeHistory(): Promise<void> {
166
+ this.messages = [];
167
+ return Promise.resolve();
168
+ }
169
+
170
+ purgeKeyData(): Promise<void> {
171
+ this.sessions = [];
172
+ this.preKeys = [];
173
+ this.oneTimeKeys = [];
174
+ this.messages = [];
175
+ return Promise.resolve();
176
+ }
177
+
178
+ saveDevice(device: Device): Promise<void> {
179
+ if (!this.devices.find((d) => d.deviceID === device.deviceID)) {
180
+ this.devices.push(device);
181
+ }
182
+ return Promise.resolve();
183
+ }
184
+
185
+ saveMessage(message: Message): Promise<void> {
186
+ const copy = { ...message };
187
+ copy.message = XUtils.encodeHex(
188
+ xSecretbox(
189
+ XUtils.decodeUTF8(message.message),
190
+ XUtils.decodeHex(message.nonce),
191
+ this.idKeys.secretKey,
192
+ ),
193
+ );
194
+ this.messages.push(copy);
195
+ return Promise.resolve();
196
+ }
197
+
198
+ savePreKeys(
199
+ preKeys: UnsavedPreKey[],
200
+ oneTime: boolean,
201
+ ): Promise<PreKeysSQL[]> {
202
+ const added: PreKeysSQL[] = [];
203
+ for (const pk of preKeys) {
204
+ const idx = oneTime ? this.nextOtkIndex++ : this.nextPreKeyIndex++;
205
+ const row = {
206
+ index: idx,
207
+ privateKey: XUtils.encodeHex(pk.keyPair.secretKey),
208
+ publicKey: XUtils.encodeHex(pk.keyPair.publicKey),
209
+ signature: XUtils.encodeHex(pk.signature),
210
+ };
211
+ if (oneTime) this.oneTimeKeys.push(row);
212
+ else this.preKeys.push(row);
213
+ // Return without privateKey (matches real Storage behavior)
214
+ added.push({
215
+ index: idx,
216
+ publicKey: row.publicKey,
217
+ signature: row.signature,
218
+ } as PreKeysSQL);
219
+ }
220
+ return Promise.resolve(added);
221
+ }
222
+
223
+ saveSession(session: SessionSQL): Promise<void> {
224
+ if (!this.sessions.find((s) => s.SK === session.SK)) {
225
+ this.sessions.push(session);
226
+ }
227
+ return Promise.resolve();
228
+ }
229
+
230
+ private decryptMessage(msg: Message): Message {
231
+ const copy = { ...msg };
232
+ if (copy.decrypted) {
233
+ const dec = xSecretboxOpen(
234
+ XUtils.decodeHex(copy.message),
235
+ XUtils.decodeHex(copy.nonce),
236
+ this.idKeys.secretKey,
237
+ );
238
+ if (dec) copy.message = XUtils.encodeUTF8(dec);
239
+ }
240
+ return copy;
241
+ }
242
+
243
+ private sqlToCrypto(s: SessionSQL): SessionCrypto {
244
+ return {
245
+ fingerprint: XUtils.decodeHex(s.fingerprint),
246
+ lastUsed: s.lastUsed,
247
+ mode: s.mode,
248
+ publicKey: XUtils.decodeHex(s.publicKey),
249
+ sessionID: s.sessionID,
250
+ SK: XUtils.decodeHex(s.SK),
251
+ userID: s.userID,
252
+ };
253
+ }
254
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared test transport utilities — kept as a module boundary for
3
+ * platform-specific test setup.
4
+ */
@@ -0,0 +1,107 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ /**
4
+ * Vite plugin that catches Node builtin imports AND Node-only globals
5
+ * in library source (src/) during transformation.
6
+ *
7
+ * Vitest runs in Node where builtins resolve natively and globals like
8
+ * Buffer/process exist — so tests pass even when the code would crash
9
+ * in a browser or Tauri webview. This plugin catches those at transform
10
+ * time, which vitest does invoke.
11
+ *
12
+ * Uses Node's own builtinModules list — no manual maintenance needed.
13
+ */
14
+ import { builtinModules } from "node:module";
15
+
16
+ const nodeBuiltins = new Set([
17
+ ...builtinModules,
18
+ ...builtinModules.map((m) => `node:${m}`),
19
+ ]);
20
+
21
+ // Node-only globals that don't exist in browsers/Tauri/RN.
22
+ // \bBuffer\b won't match ArrayBuffer or SharedArrayBuffer (no word boundary).
23
+ const NODE_GLOBALS = ["Buffer", "process", "__dirname", "__filename"];
24
+
25
+ // Matches: import ... from "events" or import ... from 'node:os'
26
+ // Also: export ... from "events"
27
+ const IMPORT_RE = /(?:import|export)\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
28
+ // Matches: await import("events")
29
+ const DYNAMIC_IMPORT_RE = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
30
+
31
+ type Violation = { kind: "global" | "import"; line: number; name: string };
32
+
33
+ export function poisonNodeImports(): Plugin {
34
+ return {
35
+ enforce: "pre",
36
+ name: "poison-node-imports",
37
+ transform(code: string, id: string) {
38
+ // Only check library source — not tests or dependencies
39
+ if (!id.includes("/src/")) return null;
40
+ if (id.includes("__tests__")) return null;
41
+ if (id.includes("node_modules")) return null;
42
+ if (isNodeOnlyFile(id)) return null;
43
+
44
+ const violations = findViolations(code);
45
+ if (violations.length === 0) return null;
46
+
47
+ const file = id.replace(/^.*\/src\//, "src/");
48
+ const msgs = violations
49
+ .map((v) => ` line ${String(v.line)}: ${v.name} (${v.kind})`)
50
+ .join("\n");
51
+ throw new Error(
52
+ `[platform-guard] Node-only code in ${file}:\n${msgs}\n` +
53
+ `These would crash in browser/RN/Tauri. Use browser-safe alternatives or dynamic imports.`,
54
+ );
55
+ },
56
+ };
57
+ }
58
+
59
+ function findViolations(code: string): Violation[] {
60
+ const results: Violation[] = [];
61
+ const lines = code.split("\n");
62
+ const strippedLines = stripComments(code).split("\n");
63
+
64
+ // Check imports against the original code (comments don't affect import syntax)
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const lineText = lines[i];
67
+ for (const re of [IMPORT_RE, DYNAMIC_IMPORT_RE]) {
68
+ re.lastIndex = 0;
69
+ let match;
70
+ while ((match = re.exec(lineText ?? "")) !== null) {
71
+ const mod = match[1]?.replace(/\.js$/, "").replace(/\.ts$/, "");
72
+ if (mod !== undefined && nodeBuiltins.has(mod)) {
73
+ results.push({
74
+ kind: "import",
75
+ line: i + 1,
76
+ name: match[1] ?? "",
77
+ });
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ // Check globals against comment-stripped code
84
+ for (let i = 0; i < strippedLines.length; i++) {
85
+ const lineText = strippedLines[i] ?? "";
86
+ for (const g of NODE_GLOBALS) {
87
+ const re = new RegExp(`\\b${g}\\b`);
88
+ if (re.test(lineText)) {
89
+ results.push({ kind: "global", line: i + 1, name: g });
90
+ }
91
+ }
92
+ }
93
+
94
+ return results;
95
+ }
96
+
97
+ function isNodeOnlyFile(id: string): boolean {
98
+ // These files are only loaded via dynamic import on the Node path.
99
+ if (id.includes("/storage/node")) return true;
100
+ return false;
101
+ }
102
+
103
+ /** Strip comments to avoid false positives on globals in JSDoc / inline comments. */
104
+ function stripComments(code: string): string {
105
+ // Remove single-line comments, but not URLs (://), and multi-line comments
106
+ return code.replace(/\/\/(?!:).*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
107
+ }