@vex-chat/libvex 2.0.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 (68) hide show
  1. package/README.md +3 -2
  2. package/dist/Client.d.ts +83 -59
  3. package/dist/Client.d.ts.map +1 -1
  4. package/dist/Client.js +143 -272
  5. package/dist/Client.js.map +1 -1
  6. package/dist/Storage.d.ts +3 -3
  7. package/dist/codec.d.ts +4 -4
  8. package/dist/codec.d.ts.map +1 -1
  9. package/dist/codec.js +4 -4
  10. package/dist/codec.js.map +1 -1
  11. package/dist/index.d.ts +2 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/keystore/node.d.ts +2 -1
  15. package/dist/keystore/node.d.ts.map +1 -1
  16. package/dist/keystore/node.js +9 -3
  17. package/dist/keystore/node.js.map +1 -1
  18. package/dist/preset/common.d.ts +1 -3
  19. package/dist/preset/common.d.ts.map +1 -1
  20. package/dist/preset/node.d.ts +1 -2
  21. package/dist/preset/node.d.ts.map +1 -1
  22. package/dist/preset/node.js +3 -7
  23. package/dist/preset/node.js.map +1 -1
  24. package/dist/preset/test.d.ts +0 -1
  25. package/dist/preset/test.d.ts.map +1 -1
  26. package/dist/preset/test.js +1 -15
  27. package/dist/preset/test.js.map +1 -1
  28. package/dist/storage/node.d.ts +1 -2
  29. package/dist/storage/node.d.ts.map +1 -1
  30. package/dist/storage/node.js +2 -8
  31. package/dist/storage/node.js.map +1 -1
  32. package/dist/storage/sqlite.d.ts +11 -3
  33. package/dist/storage/sqlite.d.ts.map +1 -1
  34. package/dist/storage/sqlite.js +36 -33
  35. package/dist/storage/sqlite.js.map +1 -1
  36. package/dist/transport/types.d.ts +0 -6
  37. package/dist/transport/types.d.ts.map +1 -1
  38. package/dist/types/crypto.d.ts +5 -2
  39. package/dist/types/crypto.d.ts.map +1 -1
  40. package/dist/types/crypto.js +2 -2
  41. package/dist/types/identity.d.ts +6 -1
  42. package/dist/types/identity.d.ts.map +1 -1
  43. package/dist/types/identity.js +1 -1
  44. package/package.json +20 -12
  45. package/src/Client.ts +206 -424
  46. package/src/Storage.ts +3 -3
  47. package/src/__tests__/codec.test.ts +26 -21
  48. package/src/__tests__/harness/platform-transports.ts +2 -15
  49. package/src/__tests__/harness/poison-node-imports.ts +0 -1
  50. package/src/__tests__/harness/shared-suite.ts +0 -20
  51. package/src/__tests__/platform-browser.test.ts +5 -10
  52. package/src/__tests__/platform-node.test.ts +1 -2
  53. package/src/codec.ts +4 -4
  54. package/src/index.ts +9 -2
  55. package/src/keystore/node.ts +14 -3
  56. package/src/preset/common.ts +1 -7
  57. package/src/preset/node.ts +3 -19
  58. package/src/preset/test.ts +1 -18
  59. package/src/storage/node.ts +2 -13
  60. package/src/storage/sqlite.ts +44 -65
  61. package/src/transport/types.ts +0 -7
  62. package/src/types/crypto.ts +5 -2
  63. package/src/types/identity.ts +6 -1
  64. package/dist/utils/createLogger.d.ts +0 -6
  65. package/dist/utils/createLogger.d.ts.map +0 -1
  66. package/dist/utils/createLogger.js +0 -27
  67. package/dist/utils/createLogger.js.map +0 -1
  68. package/src/utils/createLogger.ts +0 -37
package/src/Storage.ts CHANGED
@@ -24,7 +24,7 @@ export interface Storage extends EventEmitter {
24
24
  /**
25
25
  * Deletes history for a direct conversation or group channel.
26
26
  *
27
- * @param channelOrUserID Channel ID or user ID whose history should be deleted.
27
+ * @param channelOrUserID - Channel ID or user ID whose history should be deleted.
28
28
  */
29
29
  deleteHistory: (channelOrUserID: string) => Promise<void>;
30
30
  /** Deletes one message by `mailID`. */
@@ -108,8 +108,8 @@ export interface Storage extends EventEmitter {
108
108
  /**
109
109
  * Saves signed prekeys.
110
110
  *
111
- * @param preKeys Prekeys to persist.
112
- * @param oneTime `true` for one-time keys, `false` for the long-lived signed prekey.
111
+ * @param preKeys - Prekeys to persist.
112
+ * @param oneTime - `true` for one-time keys, `false` for the long-lived signed prekey.
113
113
  */
114
114
  savePreKeys: (
115
115
  preKeys: UnsavedPreKey[],
@@ -21,6 +21,28 @@ const hexVar = (min: number, max: number) =>
21
21
  const rec = (arbs: Record<string, fc.Arbitrary<unknown>>) =>
22
22
  fc.record(arbs, { noNullPrototype: true });
23
23
 
24
+ /**
25
+ * Strip keys that are magic in JS (`__proto__`, `constructor`, `prototype`)
26
+ * from generated JSON values. These can't round-trip through msgpack because
27
+ * the JS runtime intercepts them on plain objects.
28
+ */
29
+ function stripProtoKeys(val: unknown): unknown {
30
+ if (val === null || typeof val !== "object") return val;
31
+ if (Array.isArray(val)) return val.map(stripProtoKeys);
32
+ const out: Record<string, unknown> = {};
33
+ for (const [k, v] of Object.entries(val)) {
34
+ if (k === "__proto__" || k === "constructor" || k === "prototype")
35
+ continue;
36
+ out[k] = stripProtoKeys(v);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ const safeJsonValue = (opts?: { maxDepth?: number }) =>
42
+ fc
43
+ .jsonValue(opts)
44
+ .map((v) => stripProtoKeys(JSON.parse(JSON.stringify(v))));
45
+
24
46
  // ── Arbitraries ──────────────────────────────────────────────────────────────
25
47
 
26
48
  const arbBaseMsg = rec({
@@ -29,21 +51,14 @@ const arbBaseMsg = rec({
29
51
  });
30
52
 
31
53
  const arbSuccessMsg = rec({
32
- data: fc
33
- .jsonValue({ maxDepth: 1 })
34
- .map((v) => JSON.parse(JSON.stringify(v)) as unknown),
54
+ data: safeJsonValue({ maxDepth: 1 }),
35
55
  timestamp: fc.option(fc.string(), { nil: null }),
36
56
  transmissionID: fc.uuid({ version: 4 }),
37
57
  type: fc.constant("success"),
38
58
  });
39
59
 
40
60
  const arbErrMsg = rec({
41
- data: fc.option(
42
- fc
43
- .jsonValue({ maxDepth: 1 })
44
- .map((v) => JSON.parse(JSON.stringify(v)) as unknown),
45
- { nil: null },
46
- ),
61
+ data: fc.option(safeJsonValue({ maxDepth: 1 }), { nil: null }),
47
62
  error: fc.string({ minLength: 1 }),
48
63
  transmissionID: fc.uuid({ version: 4 }),
49
64
  type: fc.constant("error"),
@@ -51,24 +66,14 @@ const arbErrMsg = rec({
51
66
 
52
67
  const arbResourceMsg = rec({
53
68
  action: fc.constantFrom("CREATE", "RETRIEVE", "UPDATE", "DELETE"),
54
- data: fc.option(
55
- fc
56
- .jsonValue({ maxDepth: 1 })
57
- .map((v) => JSON.parse(JSON.stringify(v)) as unknown),
58
- { nil: null },
59
- ),
69
+ data: fc.option(safeJsonValue({ maxDepth: 1 }), { nil: null }),
60
70
  resourceType: fc.constantFrom("mail", "preKeys", "otk"),
61
71
  transmissionID: fc.uuid({ version: 4 }),
62
72
  type: fc.constant("resource"),
63
73
  });
64
74
 
65
75
  const arbNotifyMsg = rec({
66
- data: fc.option(
67
- fc
68
- .jsonValue({ maxDepth: 1 })
69
- .map((v) => JSON.parse(JSON.stringify(v)) as unknown),
70
- { nil: null },
71
- ),
76
+ data: fc.option(safeJsonValue({ maxDepth: 1 }), { nil: null }),
72
77
  event: fc.constantFrom("mail", "serverChange", "permission"),
73
78
  transmissionID: fc.uuid({ version: 4 }),
74
79
  type: fc.constant("notify"),
@@ -1,17 +1,4 @@
1
1
  /**
2
- * Test logger for integration tests.
2
+ * Shared test transport utilities — kept as a module boundary for
3
+ * platform-specific test setup.
3
4
  */
4
- import type { Logger } from "../../transport/types.js";
5
-
6
- export const testLogger: Logger = {
7
- debug(_m: string) {},
8
- error(m: string) {
9
- console.error(`[test] ${m}`);
10
- },
11
- info(m: string) {
12
- console.log(`[test] ${m}`);
13
- },
14
- warn(m: string) {
15
- console.warn(`[test] ${m}`);
16
- },
17
- };
@@ -97,7 +97,6 @@ function findViolations(code: string): Violation[] {
97
97
  function isNodeOnlyFile(id: string): boolean {
98
98
  // These files are only loaded via dynamic import on the Node path.
99
99
  if (id.includes("/storage/node")) return true;
100
- if (id.includes("/utils/createLogger")) return true;
101
100
  return false;
102
101
  }
103
102
 
@@ -7,7 +7,6 @@
7
7
 
8
8
  import type { ClientOptions, Message } from "../../index.js";
9
9
  import type { Storage } from "../../Storage.js";
10
- import type { Logger } from "../../transport/types.js";
11
10
 
12
11
  import { Client } from "../../index.js";
13
12
 
@@ -15,7 +14,6 @@ import { testFile, testImage } from "./fixtures.js";
15
14
 
16
15
  export function platformSuite(
17
16
  platformName: string,
18
- logger: Logger,
19
17
  makeStorage: (SK: string, opts: ClientOptions) => Promise<Storage>,
20
18
  ) {
21
19
  describe.sequential(`platform: ${platformName}`, () => {
@@ -26,10 +24,7 @@ export function platformSuite(
26
24
  beforeAll(async () => {
27
25
  const SK = Client.generateSecretKey();
28
26
  const opts: ClientOptions = {
29
- dbLogLevel: "error",
30
27
  inMemoryDb: true,
31
- logger,
32
- logLevel: "error",
33
28
  ...apiUrlOverrideFromEnv(),
34
29
  };
35
30
  const storage = await makeStorage(SK, opts);
@@ -73,10 +68,7 @@ export function platformSuite(
73
68
  test("two-user DM", async () => {
74
69
  const SK2 = Client.generateSecretKey();
75
70
  const opts2: ClientOptions = {
76
- dbLogLevel: "error",
77
71
  inMemoryDb: true,
78
- logger,
79
- logLevel: "error",
80
72
  ...apiUrlOverrideFromEnv(),
81
73
  };
82
74
  const storage2 = await makeStorage(SK2, opts2);
@@ -113,10 +105,7 @@ export function platformSuite(
113
105
  test("group messaging in channel", async () => {
114
106
  const SK2 = Client.generateSecretKey();
115
107
  const opts2: ClientOptions = {
116
- dbLogLevel: "error",
117
108
  inMemoryDb: true,
118
- logger,
119
- logLevel: "error",
120
109
  ...apiUrlOverrideFromEnv(),
121
110
  };
122
111
  const storage2 = await makeStorage(SK2, opts2);
@@ -174,10 +163,7 @@ export function platformSuite(
174
163
  const deviceKey = client.getKeys().private;
175
164
  const deviceID = client.me.device().deviceID;
176
165
  const opts2: ClientOptions = {
177
- dbLogLevel: "error",
178
166
  inMemoryDb: true,
179
- logger,
180
- logLevel: "error",
181
167
  ...apiUrlOverrideFromEnv(),
182
168
  };
183
169
  const storage2 = await makeStorage(deviceKey, opts2);
@@ -274,10 +260,7 @@ export function platformSuite(
274
260
  test.todo("multi-device message sync", async () => {
275
261
  const SK2 = Client.generateSecretKey();
276
262
  const opts2: ClientOptions = {
277
- dbLogLevel: "error",
278
263
  inMemoryDb: true,
279
- logger,
280
- logLevel: "error",
281
264
  ...apiUrlOverrideFromEnv(),
282
265
  };
283
266
  const storage2 = await makeStorage(SK2, opts2);
@@ -286,10 +269,7 @@ export function platformSuite(
286
269
  // Sender: separate user
287
270
  const SK3 = Client.generateSecretKey();
288
271
  const opts3: ClientOptions = {
289
- dbLogLevel: "error",
290
272
  inMemoryDb: true,
291
- logger,
292
- logLevel: "error",
293
273
  ...apiUrlOverrideFromEnv(),
294
274
  };
295
275
  const storage3 = await makeStorage(SK3, opts3);
@@ -1,19 +1,14 @@
1
1
  import type { ClientOptions } from "../index.js";
2
2
 
3
3
  import { MemoryStorage } from "./harness/memory-storage.js";
4
- import { browserTestAdapters } from "./harness/platform-transports.js";
5
4
  // Browser platform test — covers Tauri, Expo/RN, and web.
6
5
  // Runs with the poison plugin (vitest.config.browser.ts) which catches
7
6
  // Node builtins and globals at compile time. Uses MemoryStorage (no
8
7
  // Node SQLite) and BrowserTestWS (Uint8Array binary).
9
8
  import { platformSuite } from "./harness/shared-suite.js";
10
9
 
11
- platformSuite(
12
- "browser",
13
- browserTestAdapters,
14
- async (SK: string, _opts: ClientOptions) => {
15
- const storage = new MemoryStorage(SK);
16
- await storage.init();
17
- return storage;
18
- },
19
- );
10
+ platformSuite("browser", async (SK: string, _opts: ClientOptions) => {
11
+ const storage = new MemoryStorage(SK);
12
+ await storage.init();
13
+ return storage;
14
+ });
@@ -2,9 +2,8 @@ import type { ClientOptions } from "../index.js";
2
2
 
3
3
  import { createNodeStorage } from "../storage/node.js";
4
4
 
5
- import { testLogger } from "./harness/platform-transports.js";
6
5
  import { platformSuite } from "./harness/shared-suite.js";
7
6
 
8
- platformSuite("node", testLogger, (SK: string, _opts: ClientOptions) =>
7
+ platformSuite("node", (SK: string, _opts: ClientOptions) =>
9
8
  Promise.resolve(createNodeStorage(":memory:", SK)),
10
9
  );
package/src/codec.ts CHANGED
@@ -27,17 +27,17 @@ const _packr = new Packr({ moreTypes: false, useRecords: false });
27
27
  export function createCodec<T extends z.ZodType>(schema: T) {
28
28
  type Msg = z.infer<T>;
29
29
  return {
30
- /** Decode msgpack data typed but not validated. Fast path for SDK. */
30
+ /** Decode msgpack data and validate against the schema. */
31
31
  decode: (data: Uint8Array): Msg => schema.parse(decode(data)) as Msg,
32
32
 
33
- /** Decode + validate with Zod. Safe path for trust boundaries (Spire). */
33
+ /** Alias for decode both paths validate. Kept for API compat. */
34
34
  decodeSafe: (data: Uint8Array): Msg =>
35
35
  schema.parse(decode(data)) as Msg,
36
36
 
37
- /** Encode to msgpack — typed but not validated. */
37
+ /** Encode to msgpack. */
38
38
  encode: (msg: Msg): Uint8Array => encode(msg),
39
39
 
40
- /** Validate + encode. Ensures outgoing messages match the schema. */
40
+ /** Validate against the schema, then encode to msgpack. */
41
41
  encodeSafe: (msg: Msg): Uint8Array => {
42
42
  schema.parse(msg);
43
43
  return encode(msg);
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { Client } from "./Client.js";
2
2
  export type {
3
3
  Channel,
4
4
  Channels,
5
+ ClientEvents,
5
6
  ClientOptions,
6
7
  Device,
7
8
  Devices,
@@ -27,7 +28,13 @@ export type {
27
28
  } from "./Client.js";
28
29
  export { createCodec, msgpack } from "./codec.js";
29
30
  export type { Storage } from "./Storage.js";
30
- export type { Logger } from "./transport/types.js";
31
- export type { KeyStore, StoredCredentials } from "./types/index.js";
31
+ export type {
32
+ KeyPair,
33
+ KeyStore,
34
+ PreKeysCrypto,
35
+ SessionCrypto,
36
+ StoredCredentials,
37
+ UnsavedPreKey,
38
+ } from "./types/index.js";
32
39
  // Re-export app-facing types
33
40
  export type { Invite } from "@vex-chat/types";
@@ -13,8 +13,16 @@ import { XUtils } from "@vex-chat/crypto";
13
13
 
14
14
  export class NodeKeyStore implements KeyStore {
15
15
  private readonly dir: string;
16
+ private readonly passphrase: string;
16
17
 
17
- constructor(dir: string = ".") {
18
+ constructor(passphrase: string, dir: string = ".") {
19
+ if (!passphrase) {
20
+ throw new Error(
21
+ "NodeKeyStore requires a non-empty passphrase. " +
22
+ "The caller must supply a passphrase sourced from user input, OS keychain, or similar.",
23
+ );
24
+ }
25
+ this.passphrase = passphrase;
18
26
  this.dir = dir;
19
27
  }
20
28
 
@@ -54,7 +62,7 @@ export class NodeKeyStore implements KeyStore {
54
62
 
55
63
  save(creds: StoredCredentials): Promise<void> {
56
64
  const data = JSON.stringify(creds);
57
- const encrypted = XUtils.encryptKeyData("", data);
65
+ const encrypted = XUtils.encryptKeyData(this.passphrase, data);
58
66
  fs.writeFileSync(this.filePath(creds.username), encrypted);
59
67
  return Promise.resolve();
60
68
  }
@@ -66,7 +74,10 @@ export class NodeKeyStore implements KeyStore {
66
74
  private readFile(filePath: string): null | StoredCredentials {
67
75
  try {
68
76
  const data = fs.readFileSync(filePath);
69
- const decrypted = XUtils.decryptKeyData(new Uint8Array(data), "");
77
+ const decrypted = XUtils.decryptKeyData(
78
+ new Uint8Array(data),
79
+ this.passphrase,
80
+ );
70
81
  const parsed: unknown = JSON.parse(decrypted);
71
82
  if (isStoredCredentials(parsed)) {
72
83
  return parsed;
@@ -1,13 +1,7 @@
1
1
  import type { Storage } from "../Storage.js";
2
- import type { Logger } from "../transport/types.js";
3
2
 
4
3
  /** Internal preset interface used by nodePreset and testPreset. */
5
4
  export interface PlatformPreset {
6
- createStorage(
7
- dbName: string,
8
- privateKey: string,
9
- logger: Logger,
10
- ): Promise<Storage>;
5
+ createStorage(dbName: string, privateKey: string): Promise<Storage>;
11
6
  deviceName: string;
12
- logger: Logger;
13
7
  }
@@ -1,34 +1,18 @@
1
1
  import type { Storage } from "../Storage.js";
2
- import type { Logger } from "../transport/types.js";
3
2
  /**
4
3
  * Platform preset for Node.js (CLI tools, bots, tests).
5
4
  *
6
5
  * - WebSocket: native global (Node 22+)
7
6
  * - Storage: Kysely + better-sqlite3
8
- * - Logger: winston (loaded dynamically)
9
7
  */
10
8
  import type { PlatformPreset } from "./common.js";
11
9
 
12
- export async function nodePreset(logLevel?: string): Promise<PlatformPreset> {
13
- const { createLogger } = await import("../utils/createLogger.js");
14
- const logger: Logger = createLogger("libvex", logLevel);
15
-
10
+ export function nodePreset(): PlatformPreset {
16
11
  return {
17
- async createStorage(
18
- dbName,
19
- privateKey,
20
- storageLogger,
21
- ): Promise<Storage> {
12
+ async createStorage(dbName, privateKey): Promise<Storage> {
22
13
  const { createNodeStorage } = await import("../storage/node.js");
23
-
24
- const storage: Storage = createNodeStorage(
25
- dbName,
26
- privateKey,
27
- storageLogger,
28
- );
29
- return storage;
14
+ return createNodeStorage(dbName, privateKey);
30
15
  },
31
16
  deviceName: process.platform,
32
- logger,
33
17
  };
34
18
  }
@@ -1,30 +1,14 @@
1
- import type { Logger } from "../transport/types.js";
2
1
  /**
3
2
  * Platform preset for tests — no I/O, no platform dependencies.
4
3
  *
5
4
  * - WebSocket: native global (Node 22+)
6
5
  * - Storage: in-memory (no persistence)
7
- * - Logger: console
8
6
  */
9
7
  import type { PlatformPreset } from "./common.js";
10
8
 
11
- const logger: Logger = {
12
- debug() {},
13
- error(m: string) {
14
- console.error(`[test] ${m}`);
15
- },
16
- info(m: string) {
17
- console.log(`[test] ${m}`);
18
- },
19
- warn(m: string) {
20
- console.warn(`[test] ${m}`);
21
- },
22
- };
23
-
24
9
  export function testPreset(): PlatformPreset {
25
10
  return {
26
- async createStorage(_dbName, privateKey, _logger) {
27
- // Lazy import to avoid pulling eventemitter3 into the type graph
11
+ async createStorage(_dbName, privateKey) {
28
12
  const { MemoryStorage } =
29
13
  await import("../__tests__/harness/memory-storage.js");
30
14
  const storage = new MemoryStorage(privateKey);
@@ -32,6 +16,5 @@ export function testPreset(): PlatformPreset {
32
16
  return storage;
33
17
  },
34
18
  deviceName: "test",
35
- logger,
36
19
  };
37
20
  }
@@ -1,5 +1,4 @@
1
1
  import type { Storage } from "../Storage.js";
2
- import type { Logger } from "../transport/types.js";
3
2
  import type { ClientDatabase } from "./schema.js";
4
3
 
5
4
  import BetterSqlite3 from "better-sqlite3";
@@ -11,23 +10,13 @@ import { Kysely, SqliteDialect } from "kysely";
11
10
 
12
11
  import { SqliteStorage } from "./sqlite.js";
13
12
 
14
- export function createNodeStorage(
15
- dbPath: string,
16
- SK: string,
17
- logger?: Logger,
18
- ): Storage {
13
+ export function createNodeStorage(dbPath: string, SK: string): Storage {
19
14
  const db = new Kysely<ClientDatabase>({
20
15
  dialect: new SqliteDialect({
21
16
  database: new BetterSqlite3(dbPath),
22
17
  }),
23
18
  });
24
- const log: Logger = logger ?? {
25
- debug() {},
26
- error() {},
27
- info() {},
28
- warn() {},
29
- };
30
- const storage = new SqliteStorage(db, SK, log);
19
+ const storage = new SqliteStorage(db, SK);
31
20
  void storage.init();
32
21
  return storage;
33
22
  }