@vex-chat/libvex 1.1.0 → 2.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/README.md +103 -41
- package/dist/Client.d.ts +440 -551
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +1566 -1502
- package/dist/Client.js.map +1 -1
- package/dist/Storage.d.ts +111 -0
- package/dist/Storage.d.ts.map +1 -0
- package/dist/Storage.js +2 -0
- package/dist/Storage.js.map +1 -0
- package/dist/__tests__/harness/memory-storage.d.ts +29 -27
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -0
- package/dist/__tests__/harness/memory-storage.js +120 -109
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/codec.d.ts +44 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +51 -0
- package/dist/codec.js.map +1 -0
- package/dist/codecs.d.ts +201 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +67 -0
- package/dist/codecs.js.map +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keystore/memory.d.ts +5 -4
- package/dist/keystore/memory.d.ts.map +1 -0
- package/dist/keystore/memory.js +9 -7
- package/dist/keystore/memory.js.map +1 -1
- package/dist/keystore/node.d.ts +6 -5
- package/dist/keystore/node.d.ts.map +1 -0
- package/dist/keystore/node.js +38 -19
- package/dist/keystore/node.js.map +1 -1
- package/dist/preset/common.d.ts +9 -0
- package/dist/preset/common.d.ts.map +1 -0
- package/dist/preset/common.js +2 -0
- package/dist/preset/common.js.map +1 -0
- package/dist/preset/node.d.ts +3 -5
- package/dist/preset/node.d.ts.map +1 -0
- package/dist/preset/node.js +5 -8
- package/dist/preset/node.js.map +1 -1
- package/dist/preset/test.d.ts +4 -4
- package/dist/preset/test.d.ts.map +1 -0
- package/dist/preset/test.js +8 -11
- package/dist/preset/test.js.map +1 -1
- package/dist/storage/node.d.ts +4 -3
- package/dist/storage/node.d.ts.map +1 -0
- package/dist/storage/node.js +4 -4
- package/dist/storage/node.js.map +1 -1
- package/dist/storage/schema.d.ts +55 -54
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/sqlite.d.ts +33 -28
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +330 -291
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transport/types.d.ts +23 -16
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/websocket.d.ts +26 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +83 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types/crypto.d.ts +35 -0
- package/dist/types/crypto.d.ts.map +1 -0
- package/dist/types/crypto.js +9 -0
- package/dist/types/crypto.js.map +1 -0
- package/dist/types/identity.d.ts +17 -0
- package/dist/types/identity.d.ts.map +1 -0
- package/dist/types/identity.js +6 -0
- package/dist/types/identity.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/capitalize.d.ts +1 -0
- package/dist/utils/capitalize.d.ts.map +1 -0
- package/dist/utils/createLogger.d.ts +1 -0
- package/dist/utils/createLogger.d.ts.map +1 -0
- package/dist/utils/createLogger.js +3 -3
- package/dist/utils/createLogger.js.map +1 -1
- package/dist/utils/formatBytes.d.ts +1 -0
- package/dist/utils/formatBytes.d.ts.map +1 -0
- package/dist/utils/formatBytes.js +3 -1
- package/dist/utils/formatBytes.js.map +1 -1
- package/dist/utils/sqlSessionToCrypto.d.ts +4 -2
- package/dist/utils/sqlSessionToCrypto.d.ts.map +1 -0
- package/dist/utils/sqlSessionToCrypto.js +5 -5
- package/dist/utils/sqlSessionToCrypto.js.map +1 -1
- package/dist/utils/uint8uuid.d.ts +1 -4
- package/dist/utils/uint8uuid.d.ts.map +1 -0
- package/dist/utils/uint8uuid.js +1 -7
- package/dist/utils/uint8uuid.js.map +1 -1
- package/package.json +58 -83
- package/src/Client.ts +3304 -0
- package/{dist/IStorage.d.ts → src/Storage.ts} +70 -62
- package/src/__tests__/codec.test.ts +251 -0
- package/src/__tests__/ghost.png +0 -0
- package/src/__tests__/harness/fixtures.ts +22 -0
- package/src/__tests__/harness/memory-storage.ts +254 -0
- package/src/__tests__/harness/platform-transports.ts +17 -0
- package/src/__tests__/harness/poison-node-imports.ts +108 -0
- package/src/__tests__/harness/shared-suite.ts +446 -0
- package/src/__tests__/platform-browser.test.ts +19 -0
- package/src/__tests__/platform-node.test.ts +10 -0
- package/src/__tests__/triggered.png +0 -0
- package/src/codec.ts +68 -0
- package/src/codecs.ts +101 -0
- package/src/index.ts +33 -0
- package/src/keystore/memory.ts +30 -0
- package/src/keystore/node.ts +91 -0
- package/src/preset/common.ts +13 -0
- package/src/preset/node.ts +34 -0
- package/src/preset/test.ts +37 -0
- package/src/storage/node.ts +33 -0
- package/src/storage/schema.ts +94 -0
- package/src/storage/sqlite.ts +676 -0
- package/src/transport/types.ts +29 -0
- package/src/transport/websocket.ts +106 -0
- package/src/types/crypto.ts +39 -0
- package/src/types/identity.ts +18 -0
- package/src/types/index.ts +9 -0
- package/src/utils/capitalize.ts +6 -0
- package/src/utils/createLogger.ts +37 -0
- package/src/utils/formatBytes.ts +15 -0
- package/src/utils/sqlSessionToCrypto.ts +16 -0
- package/src/utils/uint8uuid.ts +7 -0
- package/dist/IStorage.js +0 -2
- package/dist/IStorage.js.map +0 -1
- package/dist/keystore/types.d.ts +0 -4
- package/dist/keystore/types.js +0 -2
- package/dist/keystore/types.js.map +0 -1
- package/dist/preset/expo.d.ts +0 -2
- package/dist/preset/expo.js +0 -39
- package/dist/preset/expo.js.map +0 -1
- package/dist/preset/tauri.d.ts +0 -2
- package/dist/preset/tauri.js +0 -36
- package/dist/preset/tauri.js.map +0 -1
- package/dist/preset/types.d.ts +0 -14
- package/dist/preset/types.js +0 -2
- package/dist/preset/types.js.map +0 -1
- package/dist/storage/expo.d.ts +0 -3
- package/dist/storage/expo.js +0 -18
- package/dist/storage/expo.js.map +0 -1
- package/dist/storage/tauri.d.ts +0 -3
- package/dist/storage/tauri.js +0 -21
- package/dist/storage/tauri.js.map +0 -1
- package/dist/transport/browser.d.ts +0 -17
- package/dist/transport/browser.js +0 -56
- package/dist/transport/browser.js.map +0 -1
- package/dist/utils/constants.d.ts +0 -8
- package/dist/utils/constants.js +0 -9
- package/dist/utils/constants.js.map +0 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
|
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
|
-
*
|
|
25
|
+
* Deletes history for a direct conversation or group channel.
|
|
25
26
|
*
|
|
26
|
-
* @
|
|
27
|
-
* ```ts
|
|
28
|
-
* await storage.saveMessage(message);
|
|
29
|
-
* ```
|
|
27
|
+
* @param channelOrUserID Channel ID or user ID whose history should be deleted.
|
|
30
28
|
*/
|
|
31
|
-
|
|
29
|
+
deleteHistory: (channelOrUserID: string) => Promise<void>;
|
|
32
30
|
/** Deletes one message by `mailID`. */
|
|
33
31
|
deleteMessage: (mailID: string) => Promise<void>;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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<
|
|
52
|
-
/**
|
|
53
|
-
|
|
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<
|
|
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<
|
|
88
|
-
/**
|
|
89
|
-
|
|
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
|
-
/**
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
|
|
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,251 @@
|
|
|
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
|
+
// ── Arbitraries ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const arbBaseMsg = rec({
|
|
27
|
+
transmissionID: fc.uuid({ version: 4 }),
|
|
28
|
+
type: fc.string({ maxLength: 32, minLength: 1 }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const arbSuccessMsg = rec({
|
|
32
|
+
data: fc
|
|
33
|
+
.jsonValue({ maxDepth: 1 })
|
|
34
|
+
.map((v) => JSON.parse(JSON.stringify(v)) as unknown),
|
|
35
|
+
timestamp: fc.option(fc.string(), { nil: null }),
|
|
36
|
+
transmissionID: fc.uuid({ version: 4 }),
|
|
37
|
+
type: fc.constant("success"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
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
|
+
),
|
|
47
|
+
error: fc.string({ minLength: 1 }),
|
|
48
|
+
transmissionID: fc.uuid({ version: 4 }),
|
|
49
|
+
type: fc.constant("error"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const arbResourceMsg = rec({
|
|
53
|
+
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
|
+
),
|
|
60
|
+
resourceType: fc.constantFrom("mail", "preKeys", "otk"),
|
|
61
|
+
transmissionID: fc.uuid({ version: 4 }),
|
|
62
|
+
type: fc.constant("resource"),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
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
|
+
),
|
|
72
|
+
event: fc.constantFrom("mail", "serverChange", "permission"),
|
|
73
|
+
transmissionID: fc.uuid({ version: 4 }),
|
|
74
|
+
type: fc.constant("notify"),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const arbMailSQL = rec({
|
|
78
|
+
authorID: fc.uuid({ version: 4 }),
|
|
79
|
+
cipher: hexVar(2, 64),
|
|
80
|
+
extra: hexVar(2, 64),
|
|
81
|
+
forward: fc.boolean(),
|
|
82
|
+
group: fc.option(fc.uuid({ version: 4 }), { nil: null }),
|
|
83
|
+
header: hex(64),
|
|
84
|
+
mailID: fc.uuid({ version: 4 }),
|
|
85
|
+
mailType: fc.constantFrom(0, 1),
|
|
86
|
+
nonce: hex(48),
|
|
87
|
+
readerID: fc.uuid({ version: 4 }),
|
|
88
|
+
recipient: fc.uuid({ version: 4 }),
|
|
89
|
+
sender: fc.uuid({ version: 4 }),
|
|
90
|
+
time: fc.string({ maxLength: 30, minLength: 10 }),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const arbServer = rec({
|
|
94
|
+
icon: fc.option(fc.uuid({ version: 4 }), { nil: null }),
|
|
95
|
+
name: fc.string({ maxLength: 64, minLength: 1 }),
|
|
96
|
+
serverID: fc.uuid({ version: 4 }),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const arbChannel = rec({
|
|
100
|
+
channelID: fc.uuid({ version: 4 }),
|
|
101
|
+
name: fc.string({ maxLength: 64, minLength: 1 }),
|
|
102
|
+
serverID: fc.uuid({ version: 4 }),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const arbPermission = rec({
|
|
106
|
+
permissionID: fc.uuid({ version: 4 }),
|
|
107
|
+
powerLevel: fc.integer({ max: 100, min: 0 }),
|
|
108
|
+
resourceID: fc.uuid({ version: 4 }),
|
|
109
|
+
resourceType: fc.constant("server"),
|
|
110
|
+
userID: fc.uuid({ version: 4 }),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const arbDevice = rec({
|
|
114
|
+
deleted: fc.boolean(),
|
|
115
|
+
deviceID: fc.uuid({ version: 4 }),
|
|
116
|
+
lastLogin: fc.string({ maxLength: 30, minLength: 10 }),
|
|
117
|
+
name: fc.string({ maxLength: 32, minLength: 1 }),
|
|
118
|
+
owner: fc.uuid({ version: 4 }),
|
|
119
|
+
signKey: hex(64),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const arbMailWS = rec({
|
|
123
|
+
authorID: fc.uuid({ version: 4 }),
|
|
124
|
+
cipher: fc.uint8Array({ maxLength: 128, minLength: 1 }),
|
|
125
|
+
extra: fc.uint8Array({ maxLength: 64 }),
|
|
126
|
+
forward: fc.boolean(),
|
|
127
|
+
group: fc.option(fc.uint8Array({ maxLength: 16, minLength: 16 }), {
|
|
128
|
+
nil: null,
|
|
129
|
+
}),
|
|
130
|
+
mailID: fc.uuid({ version: 4 }),
|
|
131
|
+
mailType: fc.constantFrom(0, 1),
|
|
132
|
+
nonce: fc.uint8Array({ maxLength: 24, minLength: 24 }),
|
|
133
|
+
readerID: fc.uuid({ version: 4 }),
|
|
134
|
+
recipient: fc.uuid({ version: 4 }),
|
|
135
|
+
sender: fc.uuid({ version: 4 }),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
describe("msgpack round-trip", () => {
|
|
141
|
+
const opts = { numRuns: 200 };
|
|
142
|
+
|
|
143
|
+
it("baseMsg", () => {
|
|
144
|
+
fc.assert(
|
|
145
|
+
fc.property(arbBaseMsg, (msg) => {
|
|
146
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
147
|
+
}),
|
|
148
|
+
opts,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("successMsg", () => {
|
|
153
|
+
fc.assert(
|
|
154
|
+
fc.property(arbSuccessMsg, (msg) => {
|
|
155
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
156
|
+
}),
|
|
157
|
+
opts,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("errMsg", () => {
|
|
162
|
+
fc.assert(
|
|
163
|
+
fc.property(arbErrMsg, (msg) => {
|
|
164
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
165
|
+
}),
|
|
166
|
+
opts,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("resourceMsg", () => {
|
|
171
|
+
fc.assert(
|
|
172
|
+
fc.property(arbResourceMsg, (msg) => {
|
|
173
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
174
|
+
}),
|
|
175
|
+
opts,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("notifyMsg", () => {
|
|
180
|
+
fc.assert(
|
|
181
|
+
fc.property(arbNotifyMsg, (msg) => {
|
|
182
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
183
|
+
}),
|
|
184
|
+
opts,
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("mailSQL (string fields)", () => {
|
|
189
|
+
fc.assert(
|
|
190
|
+
fc.property(arbMailSQL, (msg) => {
|
|
191
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
192
|
+
}),
|
|
193
|
+
opts,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("mailWS (binary fields)", () => {
|
|
198
|
+
fc.assert(
|
|
199
|
+
fc.property(arbMailWS, (msg) => {
|
|
200
|
+
const decoded = msgpack.decode(msgpack.encode(msg));
|
|
201
|
+
// Uint8Array round-trips through msgpack as Buffer — compare contents
|
|
202
|
+
for (const key of Object.keys(msg) as (keyof typeof msg)[]) {
|
|
203
|
+
const orig = msg[key];
|
|
204
|
+
const dec = (decoded as Record<string, unknown>)[key];
|
|
205
|
+
const actual =
|
|
206
|
+
orig instanceof Uint8Array
|
|
207
|
+
? new Uint8Array(dec as ArrayBuffer)
|
|
208
|
+
: dec;
|
|
209
|
+
expect(actual).toEqual(orig);
|
|
210
|
+
}
|
|
211
|
+
}),
|
|
212
|
+
opts,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("server", () => {
|
|
217
|
+
fc.assert(
|
|
218
|
+
fc.property(arbServer, (msg) => {
|
|
219
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
220
|
+
}),
|
|
221
|
+
opts,
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("channel", () => {
|
|
226
|
+
fc.assert(
|
|
227
|
+
fc.property(arbChannel, (msg) => {
|
|
228
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
229
|
+
}),
|
|
230
|
+
opts,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("permission", () => {
|
|
235
|
+
fc.assert(
|
|
236
|
+
fc.property(arbPermission, (msg) => {
|
|
237
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
238
|
+
}),
|
|
239
|
+
opts,
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("device", () => {
|
|
244
|
+
fc.assert(
|
|
245
|
+
fc.property(arbDevice, (msg) => {
|
|
246
|
+
expect(msgpack.decode(msgpack.encode(msg))).toEqual(msg);
|
|
247
|
+
}),
|
|
248
|
+
opts,
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
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);
|
|
@@ -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
|
+
}
|