@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
@@ -1,6 +1,11 @@
1
- import type { IDevice, IPreKeysCrypto, IPreKeysSQL, ISessionCrypto } from "@vex-chat/types";
2
- import { EventEmitter } from "eventemitter3";
3
- import type { IMessage, ISession } from "./index.js";
1
+ import type { Message, Session } from "./index.js";
2
+ import type {
3
+ PreKeysCrypto,
4
+ SessionCrypto,
5
+ UnsavedPreKey,
6
+ } from "./types/index.js";
7
+ import type { Device, PreKeysSQL } from "@vex-chat/types";
8
+ import type { EventEmitter } from "eventemitter3";
4
9
  /**
5
10
  * Storage contract used by `Client` for local persistence.
6
11
  *
@@ -13,33 +18,25 @@ import type { IMessage, ISession } from "./index.js";
13
18
  * - Managing prekeys / one-time keys used for session setup
14
19
  * - Emitting lifecycle events (`ready`, `error`)
15
20
  */
16
- export interface IStorage extends EventEmitter {
17
- /**
18
- * Set this to "true" when init has complete.
19
- */
20
- ready: boolean;
21
+ export interface Storage extends EventEmitter {
21
22
  /** Closes storage resources (connections, handles, transactions, etc.). */
22
23
  close: () => Promise<void>;
23
24
  /**
24
- * Persists one chat message.
25
+ * Deletes history for a direct conversation or group channel.
25
26
  *
26
- * @example
27
- * ```ts
28
- * await storage.saveMessage(message);
29
- * ```
27
+ * @param channelOrUserID - Channel ID or user ID whose history should be deleted.
30
28
  */
31
- saveMessage: (message: IMessage) => Promise<void>;
29
+ deleteHistory: (channelOrUserID: string) => Promise<void>;
32
30
  /** Deletes one message by `mailID`. */
33
31
  deleteMessage: (mailID: string) => Promise<void>;
34
- /**
35
- * Marks an encryption session as verified.
36
- *
37
- * This usually means the user has compared safety words / fingerprint out
38
- * of band and confirmed the session.
39
- */
40
- markSessionVerified: (sessionID: string) => Promise<void>;
41
- /** Updates a session's `lastUsed` timestamp to "now". */
42
- markSessionUsed: (sessionID: string) => Promise<void>;
32
+ /** Deletes one one-time key by index. */
33
+ deleteOneTimeKey: (index: number) => Promise<void>;
34
+ /** Returns all known encryption sessions. */
35
+ getAllSessions: () => Promise<Session[]>;
36
+ /** Gets one device record by ID. */
37
+ getDevice: (deviceID: string) => Promise<Device | null>;
38
+ /** Returns group-message history for a channel. */
39
+ getGroupHistory: (channelID: string) => Promise<Message[]>;
43
40
  /**
44
41
  * Returns direct-message history for a user.
45
42
  *
@@ -48,55 +45,34 @@ export interface IStorage extends EventEmitter {
48
45
  * const history = await storage.getMessageHistory(userID);
49
46
  * ```
50
47
  */
51
- getMessageHistory: (userID: string) => Promise<IMessage[]>;
52
- /** Returns group-message history for a channel. */
53
- getGroupHistory: (channelID: string) => Promise<IMessage[]>;
54
- /**
55
- * Deletes history for a direct conversation or group channel.
56
- *
57
- * If `olderThan` is omitted, the full history for that thread is removed.
58
- *
59
- * @param channelOrUserID Channel ID or user ID whose history should be deleted.
60
- * @param olderThan Relative duration such as `1h`, `7d`, or `30m`.
61
- */
62
- deleteHistory: (channelOrUserID: string, olderThan?: string) => Promise<void>;
63
- /** Deletes all message history. */
64
- purgeHistory: () => Promise<void>;
65
- /** Deletes all local key/session state. */
66
- purgeKeyData: () => Promise<void>;
67
- /**
68
- * Saves signed prekeys.
69
- *
70
- * @param preKeys Prekeys to persist.
71
- * @param oneTime `true` for one-time keys, `false` for the long-lived signed prekey.
72
- */
73
- savePreKeys: (preKeys: IPreKeysCrypto[], oneTime: boolean) => Promise<IPreKeysSQL[]>;
48
+ getMessageHistory: (userID: string) => Promise<Message[]>;
49
+ /** Fetches one one-time key by index. */
50
+ getOneTimeKey: (index: number) => Promise<null | PreKeysCrypto>;
74
51
  /**
75
52
  * Returns the local signed prekey pair, or `null` when it has not been created yet.
76
53
  */
77
- getPreKeys: () => Promise<IPreKeysCrypto | null>;
78
- /** Fetches one one-time key by index. */
79
- getOneTimeKey: (index: number) => Promise<IPreKeysCrypto | null>;
80
- /** Deletes one one-time key by index. */
81
- deleteOneTimeKey: (index: number) => Promise<void>;
82
- /** Fetches an encryption session using the session public key bytes. */
83
- getSessionByPublicKey: (publicKey: Uint8Array) => Promise<ISessionCrypto | null>;
84
- /** Returns all known encryption sessions. */
85
- getAllSessions: () => Promise<ISession[]>;
54
+ getPreKeys: () => Promise<null | PreKeysCrypto>;
86
55
  /** Returns the active session for a device ID (typically the most recently used). */
87
- getSessionByDeviceID: (deviceID: string) => Promise<ISessionCrypto | null>;
88
- /** Persists an encryption session. */
89
- saveSession: (session: ISession) => Promise<void>;
56
+ getSessionByDeviceID: (deviceID: string) => Promise<null | SessionCrypto>;
57
+ /** Fetches an encryption session using the session public key bytes. */
58
+ getSessionByPublicKey: (
59
+ publicKey: Uint8Array,
60
+ ) => Promise<null | SessionCrypto>;
90
61
  /**
91
62
  * Performs storage initialization (schema creation, migrations, warmup, etc.).
92
63
  *
93
64
  * Implementations should set `ready = true` and emit `ready` after completion.
94
65
  */
95
66
  init: () => Promise<void>;
96
- /** Gets one device record by ID. */
97
- getDevice: (deviceID: string) => Promise<IDevice | null>;
98
- /** Saves a device record. */
99
- saveDevice: (device: IDevice) => Promise<void>;
67
+ /** Updates a session's `lastUsed` timestamp to "now". */
68
+ markSessionUsed: (sessionID: string) => Promise<void>;
69
+ /**
70
+ * Marks an encryption session as verified.
71
+ *
72
+ * This usually means the user has compared safety words / fingerprint out
73
+ * of band and confirmed the session.
74
+ */
75
+ markSessionVerified: (sessionID: string) => Promise<void>;
100
76
  /**
101
77
  * Emit this event when init has complete.
102
78
  *
@@ -109,4 +85,36 @@ export interface IStorage extends EventEmitter {
109
85
  * @event
110
86
  */
111
87
  on(event: "error", callback: (error: Error) => void): this;
88
+ /** Deletes all message history. */
89
+ purgeHistory: () => Promise<void>;
90
+ /** Deletes all local key/session state. */
91
+ purgeKeyData: () => Promise<void>;
92
+ /**
93
+ * Set this to "true" when init has complete.
94
+ */
95
+ ready: boolean;
96
+ /** Saves a device record. */
97
+ saveDevice: (device: Device) => Promise<void>;
98
+ /**
99
+ * Persists one chat message.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * await storage.saveMessage(message);
104
+ * ```
105
+ */
106
+ saveMessage: (message: Message) => Promise<void>;
107
+
108
+ /**
109
+ * Saves signed prekeys.
110
+ *
111
+ * @param preKeys - Prekeys to persist.
112
+ * @param oneTime - `true` for one-time keys, `false` for the long-lived signed prekey.
113
+ */
114
+ savePreKeys: (
115
+ preKeys: UnsavedPreKey[],
116
+ oneTime: boolean,
117
+ ) => Promise<PreKeysSQL[]>;
118
+ /** Persists an encryption session. */
119
+ saveSession: (session: Session) => Promise<void>;
112
120
  }
@@ -0,0 +1,256 @@
1
+ import fc from "fast-check";
2
+ /**
3
+ * Property-based round-trip tests for msgpack codec.
4
+ *
5
+ * Generates random valid messages matching our wire types and verifies
6
+ * they survive encode → decode through msgpack without data loss.
7
+ *
8
+ * Only uses msgpack-safe types: strings, numbers, booleans, null,
9
+ * Uint8Array, arrays, plain objects. No Date, undefined, Map, Set.
10
+ */
11
+ import { describe, expect, it } from "vitest";
12
+
13
+ import { msgpack } from "../codec.js";
14
+
15
+ // ── Helpers ──────────────────────────────────────────────────────────────────
16
+
17
+ const hex = (len: number) =>
18
+ fc.stringMatching(new RegExp(`^[0-9a-f]{${String(len)}}$`));
19
+ const hexVar = (min: number, max: number) =>
20
+ fc.stringMatching(new RegExp(`^[0-9a-f]{${String(min)},${String(max)}}$`));
21
+ const rec = (arbs: Record<string, fc.Arbitrary<unknown>>) =>
22
+ fc.record(arbs, { noNullPrototype: true });
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
+
46
+ // ── Arbitraries ──────────────────────────────────────────────────────────────
47
+
48
+ const arbBaseMsg = rec({
49
+ transmissionID: fc.uuid({ version: 4 }),
50
+ type: fc.string({ maxLength: 32, minLength: 1 }),
51
+ });
52
+
53
+ const arbSuccessMsg = rec({
54
+ data: safeJsonValue({ maxDepth: 1 }),
55
+ timestamp: fc.option(fc.string(), { nil: null }),
56
+ transmissionID: fc.uuid({ version: 4 }),
57
+ type: fc.constant("success"),
58
+ });
59
+
60
+ const arbErrMsg = rec({
61
+ data: fc.option(safeJsonValue({ maxDepth: 1 }), { nil: null }),
62
+ error: fc.string({ minLength: 1 }),
63
+ transmissionID: fc.uuid({ version: 4 }),
64
+ type: fc.constant("error"),
65
+ });
66
+
67
+ const arbResourceMsg = rec({
68
+ action: fc.constantFrom("CREATE", "RETRIEVE", "UPDATE", "DELETE"),
69
+ data: fc.option(safeJsonValue({ maxDepth: 1 }), { nil: null }),
70
+ resourceType: fc.constantFrom("mail", "preKeys", "otk"),
71
+ transmissionID: fc.uuid({ version: 4 }),
72
+ type: fc.constant("resource"),
73
+ });
74
+
75
+ const arbNotifyMsg = rec({
76
+ data: fc.option(safeJsonValue({ maxDepth: 1 }), { nil: null }),
77
+ event: fc.constantFrom("mail", "serverChange", "permission"),
78
+ transmissionID: fc.uuid({ version: 4 }),
79
+ type: fc.constant("notify"),
80
+ });
81
+
82
+ const arbMailSQL = rec({
83
+ authorID: fc.uuid({ version: 4 }),
84
+ cipher: hexVar(2, 64),
85
+ extra: hexVar(2, 64),
86
+ forward: fc.boolean(),
87
+ group: fc.option(fc.uuid({ version: 4 }), { nil: null }),
88
+ header: hex(64),
89
+ mailID: fc.uuid({ version: 4 }),
90
+ mailType: fc.constantFrom(0, 1),
91
+ nonce: hex(48),
92
+ readerID: fc.uuid({ version: 4 }),
93
+ recipient: fc.uuid({ version: 4 }),
94
+ sender: fc.uuid({ version: 4 }),
95
+ time: fc.string({ maxLength: 30, minLength: 10 }),
96
+ });
97
+
98
+ const arbServer = rec({
99
+ icon: fc.option(fc.uuid({ version: 4 }), { nil: null }),
100
+ name: fc.string({ maxLength: 64, minLength: 1 }),
101
+ serverID: fc.uuid({ version: 4 }),
102
+ });
103
+
104
+ const arbChannel = rec({
105
+ channelID: fc.uuid({ version: 4 }),
106
+ name: fc.string({ maxLength: 64, minLength: 1 }),
107
+ serverID: fc.uuid({ version: 4 }),
108
+ });
109
+
110
+ const arbPermission = rec({
111
+ permissionID: fc.uuid({ version: 4 }),
112
+ powerLevel: fc.integer({ max: 100, min: 0 }),
113
+ resourceID: fc.uuid({ version: 4 }),
114
+ resourceType: fc.constant("server"),
115
+ userID: fc.uuid({ version: 4 }),
116
+ });
117
+
118
+ const arbDevice = rec({
119
+ deleted: fc.boolean(),
120
+ deviceID: fc.uuid({ version: 4 }),
121
+ lastLogin: fc.string({ maxLength: 30, minLength: 10 }),
122
+ name: fc.string({ maxLength: 32, minLength: 1 }),
123
+ owner: fc.uuid({ version: 4 }),
124
+ signKey: hex(64),
125
+ });
126
+
127
+ const arbMailWS = rec({
128
+ authorID: fc.uuid({ version: 4 }),
129
+ cipher: fc.uint8Array({ maxLength: 128, minLength: 1 }),
130
+ extra: fc.uint8Array({ maxLength: 64 }),
131
+ forward: fc.boolean(),
132
+ group: fc.option(fc.uint8Array({ maxLength: 16, minLength: 16 }), {
133
+ nil: null,
134
+ }),
135
+ mailID: fc.uuid({ version: 4 }),
136
+ mailType: fc.constantFrom(0, 1),
137
+ nonce: fc.uint8Array({ maxLength: 24, minLength: 24 }),
138
+ readerID: fc.uuid({ version: 4 }),
139
+ recipient: fc.uuid({ version: 4 }),
140
+ sender: fc.uuid({ version: 4 }),
141
+ });
142
+
143
+ // ── Tests ────────────────────────────────────────────────────────────────────
144
+
145
+ describe("msgpack round-trip", () => {
146
+ const opts = { numRuns: 200 };
147
+
148
+ it("baseMsg", () => {
149
+ fc.assert(
150
+ fc.property(arbBaseMsg, (msg) => {
151
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
152
+ }),
153
+ opts,
154
+ );
155
+ });
156
+
157
+ it("successMsg", () => {
158
+ fc.assert(
159
+ fc.property(arbSuccessMsg, (msg) => {
160
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
161
+ }),
162
+ opts,
163
+ );
164
+ });
165
+
166
+ it("errMsg", () => {
167
+ fc.assert(
168
+ fc.property(arbErrMsg, (msg) => {
169
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
170
+ }),
171
+ opts,
172
+ );
173
+ });
174
+
175
+ it("resourceMsg", () => {
176
+ fc.assert(
177
+ fc.property(arbResourceMsg, (msg) => {
178
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
179
+ }),
180
+ opts,
181
+ );
182
+ });
183
+
184
+ it("notifyMsg", () => {
185
+ fc.assert(
186
+ fc.property(arbNotifyMsg, (msg) => {
187
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
188
+ }),
189
+ opts,
190
+ );
191
+ });
192
+
193
+ it("mailSQL (string fields)", () => {
194
+ fc.assert(
195
+ fc.property(arbMailSQL, (msg) => {
196
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
197
+ }),
198
+ opts,
199
+ );
200
+ });
201
+
202
+ it("mailWS (binary fields)", () => {
203
+ fc.assert(
204
+ fc.property(arbMailWS, (msg) => {
205
+ const decoded = msgpack.decode(msgpack.encode(msg));
206
+ // Uint8Array round-trips through msgpack as Buffer — compare contents
207
+ for (const key of Object.keys(msg) as (keyof typeof msg)[]) {
208
+ const orig = msg[key];
209
+ const dec = (decoded as Record<string, unknown>)[key];
210
+ const actual =
211
+ orig instanceof Uint8Array
212
+ ? new Uint8Array(dec as ArrayBuffer)
213
+ : dec;
214
+ expect(actual).toEqual(orig);
215
+ }
216
+ }),
217
+ opts,
218
+ );
219
+ });
220
+
221
+ it("server", () => {
222
+ fc.assert(
223
+ fc.property(arbServer, (msg) => {
224
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
225
+ }),
226
+ opts,
227
+ );
228
+ });
229
+
230
+ it("channel", () => {
231
+ fc.assert(
232
+ fc.property(arbChannel, (msg) => {
233
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
234
+ }),
235
+ opts,
236
+ );
237
+ });
238
+
239
+ it("permission", () => {
240
+ fc.assert(
241
+ fc.property(arbPermission, (msg) => {
242
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
243
+ }),
244
+ opts,
245
+ );
246
+ });
247
+
248
+ it("device", () => {
249
+ fc.assert(
250
+ fc.property(arbDevice, (msg) => {
251
+ expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
252
+ }),
253
+ opts,
254
+ );
255
+ });
256
+ });
Binary file
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Inline test fixtures — no fs needed, works on all platforms.
3
+ */
4
+
5
+ // Minimal valid 1x1 transparent PNG (67 bytes)
6
+ const TINY_PNG_B64 =
7
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQABNjN9GQAAAABJRU5ErkJggg==";
8
+
9
+ function base64ToUint8Array(b64: string): Uint8Array {
10
+ const binary = atob(b64);
11
+ const bytes = new Uint8Array(binary.length);
12
+ for (let i = 0; i < binary.length; i++) {
13
+ bytes[i] = binary.charCodeAt(i);
14
+ }
15
+ return bytes;
16
+ }
17
+
18
+ /** Valid 1x1 PNG — passes MIME type checks for avatar/emoji endpoints. */
19
+ export const testImage = base64ToUint8Array(TINY_PNG_B64);
20
+
21
+ /** Arbitrary binary data for file upload tests. */
22
+ export const testFile = new Uint8Array(1000).fill(42);