@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,426 @@
1
+ /**
2
+ * Shared integration test body. Called by each platform entry file
3
+ * with a different adapter factory.
4
+ *
5
+ * Runs register → login → connect → send/receive DM against a real spire.
6
+ */
7
+
8
+ import type { ClientOptions, Message } from "../../index.js";
9
+ import type { Storage } from "../../Storage.js";
10
+
11
+ import { Client } from "../../index.js";
12
+
13
+ import { testFile, testImage } from "./fixtures.js";
14
+
15
+ export function platformSuite(
16
+ platformName: string,
17
+ makeStorage: (SK: string, opts: ClientOptions) => Promise<Storage>,
18
+ ) {
19
+ describe.sequential(`platform: ${platformName}`, () => {
20
+ let client: Client;
21
+ const username = Client.randomUsername();
22
+ const password = "platform-test-pw";
23
+
24
+ beforeAll(async () => {
25
+ const SK = Client.generateSecretKey();
26
+ const opts: ClientOptions = {
27
+ inMemoryDb: true,
28
+ ...apiUrlOverrideFromEnv(),
29
+ };
30
+ const storage = await makeStorage(SK, opts);
31
+ client = await Client.create(SK, opts, storage);
32
+ });
33
+
34
+ afterAll(async () => {
35
+ try {
36
+ await client.close();
37
+ } catch {}
38
+ });
39
+
40
+ test("register", async () => {
41
+ const [user, err] = await client.register(username, password);
42
+ expect(err).toBeNull();
43
+ expect(user!.username).toBe(username);
44
+ });
45
+
46
+ test("login", async () => {
47
+ const result = await client.login(username, password);
48
+ expect(result.ok).toBe(true);
49
+ });
50
+
51
+ test("connect (websocket auth)", async () => {
52
+ await connectAndWait(client, `[${platformName}] WS auth`);
53
+ expect(true).toBe(true);
54
+ });
55
+
56
+ test("send and receive DM (self)", async () => {
57
+ const me = client.me.user();
58
+ const msgPromise = waitForMessage(
59
+ client,
60
+ (m) => m.direction === "incoming" && m.decrypted,
61
+ `[${platformName}] self-DM`,
62
+ );
63
+ void client.messages.send(me.userID, "platform-test");
64
+ const msg = await msgPromise;
65
+ expect(msg.message).toBe("platform-test");
66
+ });
67
+
68
+ test("two-user DM", async () => {
69
+ const SK2 = Client.generateSecretKey();
70
+ const opts2: ClientOptions = {
71
+ inMemoryDb: true,
72
+ ...apiUrlOverrideFromEnv(),
73
+ };
74
+ const storage2 = await makeStorage(SK2, opts2);
75
+ const client2 = await Client.create(SK2, opts2, storage2);
76
+ const username2 = Client.randomUsername();
77
+
78
+ try {
79
+ const [user2, regErr] = await client2.register(
80
+ username2,
81
+ "test-pw-2",
82
+ );
83
+ expect(regErr).toBeNull();
84
+
85
+ const loginErr = await client2.login(username2, "test-pw-2");
86
+ expect(loginErr.ok).toBe(true);
87
+
88
+ await connectAndWait(client2, "client2");
89
+
90
+ // client sends to client2, client2 receives
91
+ const msgPromise = waitForMessage(
92
+ client2,
93
+ (m) => m.direction === "incoming" && m.decrypted,
94
+ `[${platformName}] two-user DM`,
95
+ 15_000,
96
+ );
97
+ void client.messages.send(user2!.userID, "hello from user 1");
98
+ const msg = await msgPromise;
99
+ expect(msg.message).toBe("hello from user 1");
100
+ } finally {
101
+ await client2.close().catch(() => {});
102
+ }
103
+ });
104
+
105
+ test("group messaging in channel", async () => {
106
+ const SK2 = Client.generateSecretKey();
107
+ const opts2: ClientOptions = {
108
+ inMemoryDb: true,
109
+ ...apiUrlOverrideFromEnv(),
110
+ };
111
+ const storage2 = await makeStorage(SK2, opts2);
112
+ const client2 = await Client.create(SK2, opts2, storage2);
113
+ const username2 = Client.randomUsername();
114
+
115
+ try {
116
+ // Register + login + connect user2
117
+ await client2.register(username2, "test-pw-2");
118
+ await client2.login(username2, "test-pw-2");
119
+ await connectAndWait(client2, "client2");
120
+
121
+ // user1 creates server + channel
122
+ const server = await client.servers.create("test-server");
123
+ expect(server).toBeTruthy();
124
+ const channels = await client.channels.retrieve(
125
+ server.serverID,
126
+ );
127
+ expect(channels.length).toBeGreaterThan(0);
128
+ const channel = channels[0];
129
+ if (!channel) throw new Error("No channel found");
130
+
131
+ // user1 creates invite, user2 redeems it
132
+ const invite = await client.invites.create(
133
+ server.serverID,
134
+ "1h",
135
+ );
136
+ expect(invite).toBeTruthy();
137
+ await client2.invites.redeem(invite.inviteID);
138
+
139
+ // user1 sends group message, user2 receives it
140
+ const msgPromise = waitForMessage(
141
+ client2,
142
+ (m) =>
143
+ m.direction === "incoming" &&
144
+ m.decrypted &&
145
+ m.group === channel.channelID,
146
+ "group message receive",
147
+ 15_000,
148
+ );
149
+ void client.messages.group(channel.channelID, "hello channel");
150
+ const msg = await msgPromise;
151
+ expect(msg.message).toBe("hello channel");
152
+
153
+ // Cleanup
154
+ await client.servers.delete(server.serverID);
155
+ } finally {
156
+ await client2.close().catch(() => {});
157
+ }
158
+ });
159
+
160
+ test("loginWithDeviceKey (auto-login)", async () => {
161
+ // Simulate app restart: create a new Client with the same
162
+ // device key, authenticate without password.
163
+ const deviceKey = client.getKeys().private;
164
+ const deviceID = client.me.device().deviceID;
165
+ const opts2: ClientOptions = {
166
+ inMemoryDb: true,
167
+ ...apiUrlOverrideFromEnv(),
168
+ };
169
+ const storage2 = await makeStorage(deviceKey, opts2);
170
+ const client2 = await Client.create(deviceKey, opts2, storage2);
171
+
172
+ try {
173
+ const authErr = await client2.loginWithDeviceKey(deviceID);
174
+ expect(authErr).toBeNull();
175
+
176
+ await connectAndWait(client2, "device-key");
177
+
178
+ // Same user, same identity
179
+ expect(client2.me.user().userID).toBe(client.me.user().userID);
180
+ expect(client2.me.user().username).toBe(username);
181
+ } finally {
182
+ await client2.close().catch(() => {});
183
+ }
184
+ });
185
+
186
+ test("server CRUD", async () => {
187
+ const permissions = await client.permissions.retrieve();
188
+ expect(permissions).toEqual([]);
189
+
190
+ const server = await client.servers.create("Test Server");
191
+ const serverList = await client.servers.retrieve();
192
+ expect(serverList.some((s) => s.serverID === server.serverID)).toBe(
193
+ true,
194
+ );
195
+
196
+ const byID = await client.servers.retrieveByID(server.serverID);
197
+ expect(byID?.serverID).toBe(server.serverID);
198
+
199
+ await client.servers.delete(server.serverID);
200
+ const afterDelete = await client.servers.retrieve();
201
+ expect(
202
+ afterDelete.some((s) => s.serverID === server.serverID),
203
+ ).toBe(false);
204
+ });
205
+
206
+ test("channel CRUD", async () => {
207
+ const server = await client.servers.create("Channel Test Server");
208
+ const channel = await client.channels.create(
209
+ "Test Channel",
210
+ server.serverID,
211
+ );
212
+
213
+ const byID = await client.channels.retrieveByID(channel.channelID);
214
+ expect(byID?.channelID).toBe(channel.channelID);
215
+
216
+ await client.channels.delete(channel.channelID);
217
+ const channels = await client.channels.retrieve(server.serverID);
218
+ expect(
219
+ channels.some((c) => c.channelID === channel.channelID),
220
+ ).toBe(false);
221
+ // Default channel still exists
222
+ expect(channels.length).toBe(1);
223
+
224
+ await client.servers.delete(server.serverID);
225
+ });
226
+
227
+ test("invite create + redeem", async () => {
228
+ const server = await client.servers.create("Invite Test Server");
229
+ const invite = await client.invites.create(server.serverID, "1h");
230
+ expect(invite).toBeTruthy();
231
+ expect(invite.serverID).toBe(server.serverID);
232
+
233
+ await client.invites.redeem(invite.inviteID);
234
+ await client.servers.delete(server.serverID);
235
+ });
236
+
237
+ test("message history retrieve + delete", async () => {
238
+ const me = client.me.user();
239
+
240
+ // Send a message and wait for it
241
+ const msgPromise = waitForMessage(
242
+ client,
243
+ (m) => m.direction === "incoming" && m.decrypted,
244
+ "history DM",
245
+ );
246
+ void client.messages.send(me.userID, "history-test");
247
+ await msgPromise;
248
+
249
+ const history = await client.messages.retrieve(me.userID);
250
+ expect(history.length).toBeGreaterThan(0);
251
+
252
+ await client.messages.delete(me.userID);
253
+ const afterDelete = await client.messages.retrieve(me.userID);
254
+ expect(afterDelete.length).toBe(0);
255
+ });
256
+
257
+ // TODO: multi-device fan-out requires sender to query fresh device
258
+ // list before sending. Currently the sender caches one device and
259
+ // the message only reaches device1.
260
+ test.todo("multi-device message sync", async () => {
261
+ const SK2 = Client.generateSecretKey();
262
+ const opts2: ClientOptions = {
263
+ inMemoryDb: true,
264
+ ...apiUrlOverrideFromEnv(),
265
+ };
266
+ const storage2 = await makeStorage(SK2, opts2);
267
+ const device2 = await Client.create(SK2, opts2, storage2);
268
+
269
+ // Sender: separate user
270
+ const SK3 = Client.generateSecretKey();
271
+ const opts3: ClientOptions = {
272
+ inMemoryDb: true,
273
+ ...apiUrlOverrideFromEnv(),
274
+ };
275
+ const storage3 = await makeStorage(SK3, opts3);
276
+ const sender = await Client.create(SK3, opts3, storage3);
277
+ const senderName = Client.randomUsername();
278
+
279
+ try {
280
+ // Register device2 under same account
281
+ await device2.login(username, password);
282
+ await connectAndWait(device2, "device2");
283
+
284
+ // Register + connect sender
285
+ await sender.register(senderName, "sender-pw");
286
+ await sender.login(senderName, "sender-pw");
287
+ await connectAndWait(sender, "sender");
288
+
289
+ const targetUserID = client.me.user().userID;
290
+
291
+ // Both devices listen for the incoming DM
292
+ const received = { device1: false, device2: false };
293
+
294
+ const waitForBoth = new Promise<void>((resolve, reject) => {
295
+ const timer = setTimeout(() => {
296
+ reject(
297
+ new Error(
298
+ `multi-device sync timed out (d1=${String(received.device1)}, d2=${String(received.device2)})`,
299
+ ),
300
+ );
301
+ }, 15_000);
302
+ const check = () => {
303
+ if (received.device1 && received.device2) {
304
+ clearTimeout(timer);
305
+ resolve();
306
+ }
307
+ };
308
+
309
+ client.on("message", (msg: Message) => {
310
+ if (
311
+ msg.direction === "incoming" &&
312
+ msg.decrypted &&
313
+ msg.message === "sync-test"
314
+ ) {
315
+ received.device1 = true;
316
+ check();
317
+ }
318
+ });
319
+ device2.on("message", (msg: Message) => {
320
+ if (
321
+ msg.direction === "incoming" &&
322
+ msg.decrypted &&
323
+ msg.message === "sync-test"
324
+ ) {
325
+ received.device2 = true;
326
+ check();
327
+ }
328
+ });
329
+ });
330
+
331
+ void sender.messages.send(targetUserID, "sync-test");
332
+ await waitForBoth;
333
+
334
+ expect(received.device1).toBe(true);
335
+ expect(received.device2).toBe(true);
336
+ } finally {
337
+ await device2.close().catch(() => {});
338
+ await sender.close().catch(() => {});
339
+ }
340
+ });
341
+
342
+ test("file upload + download", async () => {
343
+ const [details, key] = await client.files.create(testFile);
344
+ expect(details.fileID).toBeTruthy();
345
+
346
+ const fetched = await client.files.retrieve(details.fileID, key);
347
+ expect(fetched).toBeTruthy();
348
+ expect(new Uint8Array(fetched!.data)).toEqual(testFile);
349
+ });
350
+
351
+ test("emoji upload", async () => {
352
+ const server = await client.servers.create("Emoji Test Server");
353
+ const emoji = await client.emoji.create(
354
+ testImage,
355
+ "testmoji",
356
+ server.serverID,
357
+ );
358
+ expect(emoji).toBeTruthy();
359
+
360
+ const list = await client.emoji.retrieveList(server.serverID);
361
+ expect(list.some((e) => e.emojiID === emoji!.emojiID)).toBe(true);
362
+
363
+ await client.servers.delete(server.serverID);
364
+ });
365
+
366
+ test("avatar upload", async () => {
367
+ await client.me.setAvatar(testImage);
368
+ expect(true).toBe(true);
369
+ });
370
+ });
371
+ }
372
+
373
+ function apiUrlOverrideFromEnv():
374
+ | Pick<ClientOptions, "host" | "unsafeHttp">
375
+ | undefined {
376
+ const raw = process.env["API_URL"]?.trim();
377
+ if (!raw) return undefined;
378
+ if (/^https?:\/\//i.test(raw)) {
379
+ const u = new URL(raw);
380
+ return { host: u.host, unsafeHttp: u.protocol === "http:" };
381
+ }
382
+ return { host: raw, unsafeHttp: true };
383
+ }
384
+
385
+ function connectAndWait(
386
+ c: Client,
387
+ label: string,
388
+ timeout = 10_000,
389
+ ): Promise<void> {
390
+ return new Promise((resolve, reject) => {
391
+ const timer = setTimeout(() => {
392
+ reject(new Error(`${label} connect timed out`));
393
+ }, timeout);
394
+ const onConnected = () => {
395
+ clearTimeout(timer);
396
+ c.off("connected", onConnected);
397
+ resolve();
398
+ };
399
+ c.on("connected", onConnected);
400
+ c.connect().catch((err: unknown) => {
401
+ clearTimeout(timer);
402
+ reject(err instanceof Error ? err : new Error(String(err)));
403
+ });
404
+ });
405
+ }
406
+
407
+ async function waitForMessage(
408
+ c: Client,
409
+ predicate: (m: Message) => boolean,
410
+ label: string,
411
+ timeout = 10_000,
412
+ ): Promise<Message> {
413
+ return new Promise((resolve, reject) => {
414
+ const timer = setTimeout(() => {
415
+ reject(new Error(`${label} message timed out`));
416
+ }, timeout);
417
+ const onMsg = (msg: Message) => {
418
+ if (predicate(msg)) {
419
+ clearTimeout(timer);
420
+ c.off("message", onMsg);
421
+ resolve(msg);
422
+ }
423
+ };
424
+ c.on("message", onMsg);
425
+ });
426
+ }
@@ -0,0 +1,14 @@
1
+ import type { ClientOptions } from "../index.js";
2
+
3
+ import { MemoryStorage } from "./harness/memory-storage.js";
4
+ // Browser platform test — covers Tauri, Expo/RN, and web.
5
+ // Runs with the poison plugin (vitest.config.browser.ts) which catches
6
+ // Node builtins and globals at compile time. Uses MemoryStorage (no
7
+ // Node SQLite) and BrowserTestWS (Uint8Array binary).
8
+ import { platformSuite } from "./harness/shared-suite.js";
9
+
10
+ platformSuite("browser", async (SK: string, _opts: ClientOptions) => {
11
+ const storage = new MemoryStorage(SK);
12
+ await storage.init();
13
+ return storage;
14
+ });
@@ -0,0 +1,9 @@
1
+ import type { ClientOptions } from "../index.js";
2
+
3
+ import { createNodeStorage } from "../storage/node.js";
4
+
5
+ import { platformSuite } from "./harness/shared-suite.js";
6
+
7
+ platformSuite("node", (SK: string, _opts: ClientOptions) =>
8
+ Promise.resolve(createNodeStorage(":memory:", SK)),
9
+ );
Binary file
package/src/codec.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Type-safe codec factory for msgpack encode/decode with optional Zod validation.
3
+ *
4
+ * Usage:
5
+ * import { MailWSSchema } from "@vex-chat/types";
6
+ * const MailCodec = createCodec(MailWSSchema);
7
+ *
8
+ * // SDK/Apps (trusted internal) — fast, typed, no Zod overhead:
9
+ * const msg = MailCodec.decode(data);
10
+ *
11
+ * // Spire (trust boundary) — validates at runtime:
12
+ * const msg = MailCodec.decodeSafe(data);
13
+ */
14
+
15
+ import type { z } from "zod/v4";
16
+
17
+ import { Packr } from "msgpackr";
18
+
19
+ const _packr = new Packr({ moreTypes: false, useRecords: false });
20
+
21
+ /**
22
+ * Creates a type-safe codec for msgpack encode/decode.
23
+ *
24
+ * @param schema - A Zod schema to validate against
25
+ * @returns An object with encode, decode, encodeSafe, and decodeSafe methods
26
+ */
27
+ export function createCodec<T extends z.ZodType>(schema: T) {
28
+ type Msg = z.infer<T>;
29
+ return {
30
+ /** Decode msgpack data and validate against the schema. */
31
+ decode: (data: Uint8Array): Msg => schema.parse(decode(data)) as Msg,
32
+
33
+ /** Alias for decode — both paths validate. Kept for API compat. */
34
+ decodeSafe: (data: Uint8Array): Msg =>
35
+ schema.parse(decode(data)) as Msg,
36
+
37
+ /** Encode to msgpack. */
38
+ encode: (msg: Msg): Uint8Array => encode(msg),
39
+
40
+ /** Validate against the schema, then encode to msgpack. */
41
+ encodeSafe: (msg: Msg): Uint8Array => {
42
+ schema.parse(msg);
43
+ return encode(msg);
44
+ },
45
+ };
46
+ }
47
+
48
+ function decode(data: Uint8Array): unknown {
49
+ return _packr.decode(data) as unknown;
50
+ }
51
+
52
+ /**
53
+ * Encode a value to msgpack. Returns a fresh Uint8Array copy
54
+ * (not a subarray of the internal pool buffer) to avoid browser
55
+ * XMLHttpRequest.send() corruption (axios issue #4068).
56
+ */
57
+ function encode(value: unknown): Uint8Array {
58
+ const packed = _packr.encode(value);
59
+ return new Uint8Array(
60
+ packed.buffer.slice(
61
+ packed.byteOffset,
62
+ packed.byteOffset + packed.byteLength,
63
+ ),
64
+ );
65
+ }
66
+
67
+ /** Raw msgpack encode/decode without schema validation. */
68
+ export const msgpack = { decode, encode };
package/src/codecs.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Pre-built codec instances for every HTTP response type.
3
+ *
4
+ * Usage: import { UserCodec } from "./codecs.js";
5
+ * const data = decodeAxios(UserCodec, res.data);
6
+ *
7
+ * decode() returns typed data without runtime validation (SDK trusts server).
8
+ * For trust boundary validation, use codec.decodeSafe() directly.
9
+ */
10
+ import {
11
+ ActionTokenSchema,
12
+ ChannelSchema,
13
+ DeviceSchema,
14
+ EmojiSchema,
15
+ FileSQLSchema,
16
+ InviteSchema,
17
+ KeyBundleSchema,
18
+ PermissionSchema,
19
+ ServerSchema,
20
+ UserSchema,
21
+ } from "@vex-chat/types";
22
+
23
+ import { z } from "zod/v4";
24
+
25
+ import { createCodec } from "./codec.js";
26
+
27
+ // ── Named schema codecs ─────────────────────────────────────────────────────
28
+
29
+ export const UserCodec = createCodec(UserSchema);
30
+ export const DeviceCodec = createCodec(DeviceSchema);
31
+ export const ServerCodec = createCodec(ServerSchema);
32
+ export const ChannelCodec = createCodec(ChannelSchema);
33
+ export const PermissionCodec = createCodec(PermissionSchema);
34
+ export const InviteCodec = createCodec(InviteSchema);
35
+ export const EmojiCodec = createCodec(EmojiSchema);
36
+ export const FileSQLCodec = createCodec(FileSQLSchema);
37
+ export const ActionTokenCodec = createCodec(ActionTokenSchema);
38
+ export const KeyBundleCodec = createCodec(KeyBundleSchema);
39
+
40
+ // ── Array codecs ────────────────────────────────────────────────────────────
41
+
42
+ export const UserArrayCodec = createCodec(z.array(UserSchema));
43
+ export const DeviceArrayCodec = createCodec(z.array(DeviceSchema));
44
+ export const ServerArrayCodec = createCodec(z.array(ServerSchema));
45
+ export const ChannelArrayCodec = createCodec(z.array(ChannelSchema));
46
+ export const PermissionArrayCodec = createCodec(z.array(PermissionSchema));
47
+ export const InviteArrayCodec = createCodec(z.array(InviteSchema));
48
+ export const EmojiArrayCodec = createCodec(z.array(EmojiSchema));
49
+
50
+ // ── Inline ad-hoc response codecs ───────────────────────────────────────────
51
+
52
+ export const ConnectResponseCodec = createCodec(
53
+ z.object({ deviceToken: z.string() }),
54
+ );
55
+
56
+ export const AuthResponseCodec = createCodec(
57
+ z.object({
58
+ token: z.string(),
59
+ user: UserSchema,
60
+ }),
61
+ );
62
+
63
+ export const DeviceChallengeCodec = createCodec(
64
+ z.object({
65
+ challenge: z.string(),
66
+ challengeID: z.string(),
67
+ }),
68
+ );
69
+
70
+ export const WhoamiCodec = createCodec(
71
+ z.object({
72
+ exp: z.number(),
73
+ token: z.string(),
74
+ user: UserSchema,
75
+ }),
76
+ );
77
+
78
+ export const OtkCountCodec = createCodec(z.object({ count: z.number() }));
79
+
80
+ // ── Helper: decode axios response buffer ────────────────────────────────────
81
+
82
+ /**
83
+ * Decode an axios arraybuffer response with a typed codec.
84
+ * Uses decodeSafe (Zod-validated) so schema mismatches surface immediately.
85
+ */
86
+ export function decodeAxios<T>(
87
+ codec: { decodeSafe: (data: Uint8Array) => T },
88
+ /**
89
+ * Accepts `unknown` because axios types its `responseType: 'arraybuffer'`
90
+ * responses as `any`. At runtime this is always an `ArrayBuffer`.
91
+ */
92
+ data: unknown,
93
+ ): T {
94
+ if (data instanceof Uint8Array) {
95
+ return codec.decodeSafe(data);
96
+ }
97
+ if (data instanceof ArrayBuffer) {
98
+ return codec.decodeSafe(new Uint8Array(data));
99
+ }
100
+ throw new Error("Expected Uint8Array or ArrayBuffer from axios response");
101
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ export { Client } from "./Client.js";
2
+ export type {
3
+ Channel,
4
+ Channels,
5
+ ClientEvents,
6
+ ClientOptions,
7
+ Device,
8
+ Devices,
9
+ Emojis,
10
+ FileProgress,
11
+ FileRes,
12
+ Files,
13
+ Invites,
14
+ Keys,
15
+ Me,
16
+ Message,
17
+ Messages,
18
+ Moderation,
19
+ Permission,
20
+ Permissions,
21
+ Server,
22
+ Servers,
23
+ Session,
24
+ Sessions,
25
+ User,
26
+ Users,
27
+ VexFile,
28
+ } from "./Client.js";
29
+ export { createCodec, msgpack } from "./codec.js";
30
+ export type { Storage } from "./Storage.js";
31
+ export type {
32
+ KeyPair,
33
+ KeyStore,
34
+ PreKeysCrypto,
35
+ SessionCrypto,
36
+ StoredCredentials,
37
+ UnsavedPreKey,
38
+ } from "./types/index.js";
39
+ // Re-export app-facing types
40
+ export type { Invite } from "@vex-chat/types";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * In-memory KeyStore for testing and ephemeral sessions.
3
+ * No persistence — credentials are lost when the process exits.
4
+ */
5
+ import type { KeyStore, StoredCredentials } from "../types/index.js";
6
+
7
+ export class MemoryKeyStore implements KeyStore {
8
+ private readonly store = new Map<string, StoredCredentials>();
9
+
10
+ clear(username: string): Promise<void> {
11
+ this.store.delete(username);
12
+ return Promise.resolve();
13
+ }
14
+
15
+ load(username?: string): Promise<null | StoredCredentials> {
16
+ if (username) {
17
+ return Promise.resolve(this.store.get(username) ?? null);
18
+ }
19
+ // Return the most recently saved credentials
20
+ const entries = [...this.store.values()];
21
+ return Promise.resolve(
22
+ entries.length > 0 ? (entries[entries.length - 1] ?? null) : null,
23
+ );
24
+ }
25
+
26
+ save(creds: StoredCredentials): Promise<void> {
27
+ this.store.set(creds.username, creds);
28
+ return Promise.resolve();
29
+ }
30
+ }