zapo-js 1.8.2 → 1.10.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 +14 -13
- package/dist/auth/credentials-flow.js +2 -3
- package/dist/client/WaClient.d.ts +4 -0
- package/dist/client/WaClient.js +5 -1
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +11 -4
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +11 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +15 -5
- package/dist/client/messaging/ignore-key.d.ts +2 -1
- package/dist/client/messaging/ignore-key.js +24 -3
- package/dist/esm/auth/credentials-flow.js +3 -4
- package/dist/esm/client/WaClient.js +5 -1
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +14 -5
- package/dist/esm/client/messaging/ignore-key.js +25 -4
- package/dist/esm/message/encode/content.js +4 -1
- package/dist/esm/store/cache/identity.cache.js +30 -5
- package/dist/esm/store/cache/privacy-token.cache.js +30 -5
- package/dist/esm/store/cache/sender-key.cache.js +38 -7
- package/dist/esm/store/cache/session.cache.js +30 -4
- package/dist/esm/store/createStore.js +39 -4
- package/dist/esm/store/memory/identity.store.js +28 -4
- package/dist/esm/store/memory/privacy-token.store.js +24 -7
- package/dist/esm/store/memory/sender-key.store.js +36 -7
- package/dist/esm/store/memory/session.store.js +41 -7
- package/dist/esm/transport/node/WaMobileTcpSocket.js +69 -9
- package/dist/esm/transport/noise/WaMobileClientPayload.js +27 -7
- package/dist/esm/transport/proxy.js +83 -0
- package/dist/esm/util/collections.js +94 -0
- package/dist/esm/util/index.js +2 -2
- package/dist/message/encode/content.js +4 -1
- package/dist/message/primitives/media-retry.d.ts +11 -0
- package/dist/store/cache/identity.cache.d.ts +6 -4
- package/dist/store/cache/identity.cache.js +30 -5
- package/dist/store/cache/privacy-token.cache.d.ts +6 -4
- package/dist/store/cache/privacy-token.cache.js +30 -5
- package/dist/store/cache/sender-key.cache.d.ts +5 -3
- package/dist/store/cache/sender-key.cache.js +38 -7
- package/dist/store/cache/session.cache.d.ts +6 -3
- package/dist/store/cache/session.cache.js +30 -4
- package/dist/store/createStore.js +39 -4
- package/dist/store/memory/identity.store.d.ts +7 -1
- package/dist/store/memory/identity.store.js +28 -4
- package/dist/store/memory/privacy-token.store.d.ts +7 -1
- package/dist/store/memory/privacy-token.store.js +24 -7
- package/dist/store/memory/sender-key.store.d.ts +12 -2
- package/dist/store/memory/sender-key.store.js +36 -7
- package/dist/store/memory/session.store.d.ts +7 -1
- package/dist/store/memory/session.store.js +41 -7
- package/dist/store/types.d.ts +43 -4
- package/dist/transport/node/WaMobileTcpSocket.d.ts +3 -1
- package/dist/transport/node/WaMobileTcpSocket.js +68 -8
- package/dist/transport/noise/WaMobileClientPayload.d.ts +22 -0
- package/dist/transport/noise/WaMobileClientPayload.js +27 -7
- package/dist/transport/proxy.d.ts +24 -0
- package/dist/transport/proxy.js +85 -0
- package/dist/util/collections.d.ts +46 -0
- package/dist/util/collections.js +98 -0
- package/dist/util/index.d.ts +2 -2
- package/dist/util/index.js +3 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WaPrivacyTokenStore, WaStoredPrivacyTokenRecord } from '../contracts/privacy-token.store';
|
|
2
2
|
export declare class WaPrivacyTokenMemoryStore implements WaPrivacyTokenStore {
|
|
3
|
-
|
|
3
|
+
protected readonly records: Map<string, WaStoredPrivacyTokenRecord>;
|
|
4
4
|
private readonly maxEntries;
|
|
5
5
|
constructor(maxEntries?: number);
|
|
6
6
|
upsert(record: WaStoredPrivacyTokenRecord): Promise<void>;
|
|
@@ -9,5 +9,11 @@ export declare class WaPrivacyTokenMemoryStore implements WaPrivacyTokenStore {
|
|
|
9
9
|
deleteByJid(jid: string): Promise<number>;
|
|
10
10
|
clear(): Promise<void>;
|
|
11
11
|
destroy(): Promise<void>;
|
|
12
|
+
/** Runs with the jid of every read hit and write. No-op unless a subclass tracks access. */
|
|
13
|
+
protected onEntryAccess(jid: string): void;
|
|
14
|
+
/** Runs with the jid of every deleted or cap-evicted record. */
|
|
15
|
+
protected onEntryRemove(jid: string): void;
|
|
16
|
+
protected onEntriesClear(): void;
|
|
17
|
+
private readonly onEvict;
|
|
12
18
|
private mergeRecord;
|
|
13
19
|
}
|
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WaPrivacyTokenMemoryStore = void 0;
|
|
4
|
+
const coercion_1 = require("../../util/coercion");
|
|
4
5
|
const collections_1 = require("../../util/collections");
|
|
5
6
|
const DEFAULT_MAX_ENTRIES = 10000;
|
|
6
7
|
class WaPrivacyTokenMemoryStore {
|
|
7
|
-
constructor(maxEntries
|
|
8
|
+
constructor(maxEntries) {
|
|
9
|
+
this.onEvict = (jid) => this.onEntryRemove(jid);
|
|
8
10
|
this.records = new Map();
|
|
9
|
-
this.maxEntries = maxEntries;
|
|
11
|
+
this.maxEntries = (0, coercion_1.resolvePositive)(maxEntries, DEFAULT_MAX_ENTRIES, 'WaPrivacyTokenMemoryStore.maxEntries');
|
|
10
12
|
}
|
|
11
13
|
async upsert(record) {
|
|
12
14
|
const existing = this.records.get(record.jid);
|
|
13
15
|
const merged = existing ? this.mergeRecord(existing, record) : record;
|
|
14
|
-
(0, collections_1.setBoundedMapEntry)(this.records, record.jid, merged, this.maxEntries);
|
|
16
|
+
(0, collections_1.setBoundedMapEntry)(this.records, record.jid, merged, this.maxEntries, this.onEvict);
|
|
17
|
+
this.onEntryAccess(record.jid);
|
|
15
18
|
}
|
|
16
19
|
async upsertBatch(records) {
|
|
17
20
|
for (let i = 0; i < records.length; i += 1) {
|
|
18
21
|
const record = records[i];
|
|
19
22
|
const existing = this.records.get(record.jid);
|
|
20
23
|
const merged = existing ? this.mergeRecord(existing, record) : record;
|
|
21
|
-
(0, collections_1.setBoundedMapEntry)(this.records, record.jid, merged, this.maxEntries);
|
|
24
|
+
(0, collections_1.setBoundedMapEntry)(this.records, record.jid, merged, this.maxEntries, this.onEvict);
|
|
25
|
+
this.onEntryAccess(record.jid);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
async getByJid(jid) {
|
|
25
|
-
|
|
29
|
+
const record = this.records.get(jid);
|
|
30
|
+
if (record === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
this.onEntryAccess(jid);
|
|
33
|
+
return record;
|
|
26
34
|
}
|
|
27
35
|
async deleteByJid(jid) {
|
|
28
|
-
|
|
36
|
+
if (!this.records.delete(jid))
|
|
37
|
+
return 0;
|
|
38
|
+
this.onEntryRemove(jid);
|
|
39
|
+
return 1;
|
|
29
40
|
}
|
|
30
41
|
async clear() {
|
|
31
42
|
this.records.clear();
|
|
43
|
+
this.onEntriesClear();
|
|
32
44
|
}
|
|
33
45
|
async destroy() {
|
|
34
|
-
this.
|
|
46
|
+
await this.clear();
|
|
35
47
|
}
|
|
48
|
+
/** Runs with the jid of every read hit and write. No-op unless a subclass tracks access. */
|
|
49
|
+
onEntryAccess(jid) { }
|
|
50
|
+
/** Runs with the jid of every deleted or cap-evicted record. */
|
|
51
|
+
onEntryRemove(jid) { }
|
|
52
|
+
onEntriesClear() { }
|
|
36
53
|
mergeRecord(existing, incoming) {
|
|
37
54
|
return {
|
|
38
55
|
jid: incoming.jid,
|
|
@@ -5,8 +5,8 @@ export interface WaSenderKeyMemoryStoreOptions {
|
|
|
5
5
|
readonly maxSenderDistributions?: number;
|
|
6
6
|
}
|
|
7
7
|
export declare class SenderKeyMemoryStore implements WaSenderKeyStoreContract {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
protected readonly senderKeys: Map<string, SenderKeyRecord>;
|
|
9
|
+
protected readonly senderDistributions: Map<string, SenderKeyDistributionRecord>;
|
|
10
10
|
private readonly maxSenderKeys;
|
|
11
11
|
private readonly maxSenderDistributions;
|
|
12
12
|
constructor(options?: WaSenderKeyMemoryStoreOptions);
|
|
@@ -22,6 +22,16 @@ export declare class SenderKeyMemoryStore implements WaSenderKeyStoreContract {
|
|
|
22
22
|
deleteDeviceSenderKey(target: SignalAddress, groupId?: string): Promise<number>;
|
|
23
23
|
markForgetSenderKey(groupId: string, participants: readonly SignalAddress[]): Promise<number>;
|
|
24
24
|
clear(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Runs with the map and key of every point-read hit and write (the group
|
|
27
|
+
* scan does not count). No-op unless a subclass tracks access.
|
|
28
|
+
*/
|
|
29
|
+
protected onEntryAccess(entries: Map<string, unknown>, key: string): void;
|
|
30
|
+
/** Runs with the map and key of every deleted or cap-evicted entry. */
|
|
31
|
+
protected onEntryRemove(entries: Map<string, unknown>, key: string): void;
|
|
32
|
+
protected onEntriesClear(): void;
|
|
33
|
+
private readonly onSenderKeyEvict;
|
|
34
|
+
private readonly onDistributionEvict;
|
|
25
35
|
private deleteMatching;
|
|
26
36
|
private makeKey;
|
|
27
37
|
}
|
|
@@ -10,20 +10,28 @@ const DEFAULT_SENDER_KEY_STORE_LIMITS = Object.freeze({
|
|
|
10
10
|
});
|
|
11
11
|
class SenderKeyMemoryStore {
|
|
12
12
|
constructor(options = {}) {
|
|
13
|
+
this.onSenderKeyEvict = (key) => this.onEntryRemove(this.senderKeys, key);
|
|
14
|
+
this.onDistributionEvict = (key) => this.onEntryRemove(this.senderDistributions, key);
|
|
13
15
|
this.senderKeys = new Map();
|
|
14
16
|
this.senderDistributions = new Map();
|
|
15
17
|
this.maxSenderKeys = (0, coercion_1.resolvePositive)(options.maxSenderKeys, DEFAULT_SENDER_KEY_STORE_LIMITS.senderKeys, 'WaSenderKeyMemoryStoreOptions.maxSenderKeys');
|
|
16
18
|
this.maxSenderDistributions = (0, coercion_1.resolvePositive)(options.maxSenderDistributions, DEFAULT_SENDER_KEY_STORE_LIMITS.senderDistributions, 'WaSenderKeyMemoryStoreOptions.maxSenderDistributions');
|
|
17
19
|
}
|
|
18
20
|
async upsertSenderKey(record) {
|
|
19
|
-
|
|
21
|
+
const key = this.makeKey(record.groupId, record.sender);
|
|
22
|
+
(0, collections_1.setBoundedMapEntry)(this.senderKeys, key, record, this.maxSenderKeys, this.onSenderKeyEvict);
|
|
23
|
+
this.onEntryAccess(this.senderKeys, key);
|
|
20
24
|
}
|
|
21
25
|
async upsertSenderKeyDistribution(record) {
|
|
22
|
-
|
|
26
|
+
const key = this.makeKey(record.groupId, record.sender);
|
|
27
|
+
(0, collections_1.setBoundedMapEntry)(this.senderDistributions, key, record, this.maxSenderDistributions, this.onDistributionEvict);
|
|
28
|
+
this.onEntryAccess(this.senderDistributions, key);
|
|
23
29
|
}
|
|
24
30
|
async upsertSenderKeyDistributions(records) {
|
|
25
31
|
for (const record of records) {
|
|
26
|
-
|
|
32
|
+
const key = this.makeKey(record.groupId, record.sender);
|
|
33
|
+
(0, collections_1.setBoundedMapEntry)(this.senderDistributions, key, record, this.maxSenderDistributions, this.onDistributionEvict);
|
|
34
|
+
this.onEntryAccess(this.senderDistributions, key);
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
37
|
async getGroupSenderKeyList(groupId) {
|
|
@@ -45,14 +53,25 @@ class SenderKeyMemoryStore {
|
|
|
45
53
|
};
|
|
46
54
|
}
|
|
47
55
|
async getDeviceSenderKey(groupId, sender) {
|
|
48
|
-
const
|
|
49
|
-
|
|
56
|
+
const key = this.makeKey(groupId, sender);
|
|
57
|
+
const record = this.senderKeys.get(key);
|
|
58
|
+
if (record === undefined)
|
|
59
|
+
return null;
|
|
60
|
+
this.onEntryAccess(this.senderKeys, key);
|
|
61
|
+
return record;
|
|
50
62
|
}
|
|
51
63
|
async getDeviceSenderKeyDistributions(groupId, senders) {
|
|
52
64
|
const records = new Array(senders.length);
|
|
53
65
|
for (let index = 0; index < senders.length; index += 1) {
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
const key = this.makeKey(groupId, senders[index]);
|
|
67
|
+
const record = this.senderDistributions.get(key);
|
|
68
|
+
if (record === undefined) {
|
|
69
|
+
records[index] = null;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.onEntryAccess(this.senderDistributions, key);
|
|
73
|
+
records[index] = record;
|
|
74
|
+
}
|
|
56
75
|
}
|
|
57
76
|
return records;
|
|
58
77
|
}
|
|
@@ -74,7 +93,16 @@ class SenderKeyMemoryStore {
|
|
|
74
93
|
async clear() {
|
|
75
94
|
this.senderKeys.clear();
|
|
76
95
|
this.senderDistributions.clear();
|
|
96
|
+
this.onEntriesClear();
|
|
77
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Runs with the map and key of every point-read hit and write (the group
|
|
100
|
+
* scan does not count). No-op unless a subclass tracks access.
|
|
101
|
+
*/
|
|
102
|
+
onEntryAccess(entries, key) { }
|
|
103
|
+
/** Runs with the map and key of every deleted or cap-evicted entry. */
|
|
104
|
+
onEntryRemove(entries, key) { }
|
|
105
|
+
onEntriesClear() { }
|
|
78
106
|
deleteMatching(map, target, groupId) {
|
|
79
107
|
let deleted = 0;
|
|
80
108
|
const targetAddressKey = (0, jid_1.signalAddressKey)(target);
|
|
@@ -83,6 +111,7 @@ class SenderKeyMemoryStore {
|
|
|
83
111
|
const sameAddress = (0, jid_1.signalAddressKey)(record.sender) === targetAddressKey;
|
|
84
112
|
if (sameGroup && sameAddress) {
|
|
85
113
|
map.delete(key);
|
|
114
|
+
this.onEntryRemove(map, key);
|
|
86
115
|
deleted += 1;
|
|
87
116
|
}
|
|
88
117
|
}
|
|
@@ -4,7 +4,7 @@ export interface WaSessionMemoryStoreOptions {
|
|
|
4
4
|
readonly maxSessions?: number;
|
|
5
5
|
}
|
|
6
6
|
export declare class WaSessionMemoryStore implements WaSessionStoreContract {
|
|
7
|
-
|
|
7
|
+
protected readonly signalSessions: Map<string, SignalSessionRecord>;
|
|
8
8
|
private readonly maxSessions;
|
|
9
9
|
constructor(options?: WaSessionMemoryStoreOptions);
|
|
10
10
|
hasSession(address: SignalAddress): Promise<boolean>;
|
|
@@ -18,4 +18,10 @@ export declare class WaSessionMemoryStore implements WaSessionStoreContract {
|
|
|
18
18
|
}[]): Promise<void>;
|
|
19
19
|
deleteSession(address: SignalAddress): Promise<void>;
|
|
20
20
|
clear(): Promise<void>;
|
|
21
|
+
/** Runs with the key of every read hit and write. No-op unless a subclass tracks access. */
|
|
22
|
+
protected onEntryAccess(key: string): void;
|
|
23
|
+
/** Runs with the key of every deleted or cap-evicted entry. */
|
|
24
|
+
protected onEntryRemove(key: string): void;
|
|
25
|
+
protected onEntriesClear(): void;
|
|
26
|
+
private readonly onEvict;
|
|
21
27
|
}
|
|
@@ -7,43 +7,77 @@ const collections_1 = require("../../util/collections");
|
|
|
7
7
|
const DEFAULT_MAX_SESSIONS = 8192;
|
|
8
8
|
class WaSessionMemoryStore {
|
|
9
9
|
constructor(options = {}) {
|
|
10
|
+
this.onEvict = (key) => this.onEntryRemove(key);
|
|
10
11
|
this.signalSessions = new Map();
|
|
11
12
|
this.maxSessions = (0, coercion_1.resolvePositive)(options.maxSessions, DEFAULT_MAX_SESSIONS, 'WaSessionMemoryStoreOptions.maxSessions');
|
|
12
13
|
}
|
|
13
14
|
async hasSession(address) {
|
|
14
|
-
|
|
15
|
+
const key = (0, jid_1.signalAddressKey)(address);
|
|
16
|
+
if (!this.signalSessions.has(key))
|
|
17
|
+
return false;
|
|
18
|
+
this.onEntryAccess(key);
|
|
19
|
+
return true;
|
|
15
20
|
}
|
|
16
21
|
async hasSessions(addresses) {
|
|
17
22
|
const result = new Array(addresses.length);
|
|
18
23
|
for (let i = 0; i < addresses.length; i += 1) {
|
|
19
|
-
|
|
24
|
+
const key = (0, jid_1.signalAddressKey)(addresses[i]);
|
|
25
|
+
const found = this.signalSessions.has(key);
|
|
26
|
+
if (found)
|
|
27
|
+
this.onEntryAccess(key);
|
|
28
|
+
result[i] = found;
|
|
20
29
|
}
|
|
21
30
|
return result;
|
|
22
31
|
}
|
|
23
32
|
async getSession(address) {
|
|
24
|
-
|
|
33
|
+
const key = (0, jid_1.signalAddressKey)(address);
|
|
34
|
+
const session = this.signalSessions.get(key);
|
|
35
|
+
if (session === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
this.onEntryAccess(key);
|
|
38
|
+
return session;
|
|
25
39
|
}
|
|
26
40
|
async getSessionsBatch(addresses) {
|
|
27
41
|
const result = new Array(addresses.length);
|
|
28
42
|
for (let i = 0; i < addresses.length; i += 1) {
|
|
29
|
-
|
|
43
|
+
const key = (0, jid_1.signalAddressKey)(addresses[i]);
|
|
44
|
+
const session = this.signalSessions.get(key);
|
|
45
|
+
if (session === undefined) {
|
|
46
|
+
result[i] = null;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.onEntryAccess(key);
|
|
50
|
+
result[i] = session;
|
|
51
|
+
}
|
|
30
52
|
}
|
|
31
53
|
return result;
|
|
32
54
|
}
|
|
33
55
|
async setSession(address, session) {
|
|
34
|
-
|
|
56
|
+
const key = (0, jid_1.signalAddressKey)(address);
|
|
57
|
+
(0, collections_1.setBoundedMapEntry)(this.signalSessions, key, session, this.maxSessions, this.onEvict);
|
|
58
|
+
this.onEntryAccess(key);
|
|
35
59
|
}
|
|
36
60
|
async setSessionsBatch(entries) {
|
|
37
61
|
for (let index = 0; index < entries.length; index += 1) {
|
|
38
62
|
const entry = entries[index];
|
|
39
|
-
|
|
63
|
+
const key = (0, jid_1.signalAddressKey)(entry.address);
|
|
64
|
+
(0, collections_1.setBoundedMapEntry)(this.signalSessions, key, entry.session, this.maxSessions, this.onEvict);
|
|
65
|
+
this.onEntryAccess(key);
|
|
40
66
|
}
|
|
41
67
|
}
|
|
42
68
|
async deleteSession(address) {
|
|
43
|
-
|
|
69
|
+
const key = (0, jid_1.signalAddressKey)(address);
|
|
70
|
+
if (this.signalSessions.delete(key))
|
|
71
|
+
this.onEntryRemove(key);
|
|
44
72
|
}
|
|
45
73
|
async clear() {
|
|
46
74
|
this.signalSessions.clear();
|
|
75
|
+
this.onEntriesClear();
|
|
47
76
|
}
|
|
77
|
+
/** Runs with the key of every read hit and write. No-op unless a subclass tracks access. */
|
|
78
|
+
onEntryAccess(key) { }
|
|
79
|
+
/** Runs with the key of every deleted or cap-evicted entry. */
|
|
80
|
+
onEntryRemove(key) { }
|
|
81
|
+
onEntriesClear() { }
|
|
48
82
|
}
|
|
49
83
|
exports.WaSessionMemoryStore = WaSessionMemoryStore;
|
package/dist/store/types.d.ts
CHANGED
|
@@ -364,10 +364,11 @@ export interface WaCreateStoreOptions<B extends string = string, TBackends exten
|
|
|
364
364
|
/**
|
|
365
365
|
* Opt-in in-process read-through cache in front of a *persistent* backend
|
|
366
366
|
* for the hot signal domains. Each enabled domain wraps its backend store
|
|
367
|
-
* with a bounded
|
|
367
|
+
* with a bounded L1 (the in-tree memory provider) so repeated reads of
|
|
368
368
|
* the same peer on the send/recv path skip the backend round-trip; writes
|
|
369
369
|
* stay write-through (or invalidate-on-write for `privacyToken`) so the
|
|
370
|
-
* backend remains authoritative.
|
|
370
|
+
* backend remains authoritative. Entries leave the L1 through the
|
|
371
|
+
* `limits` cap and, when set, the `ttlMs` idle TTL.
|
|
371
372
|
*
|
|
372
373
|
* A domain flag is a no-op unless that domain resolves to a real backend
|
|
373
374
|
* in {@link providers} - caching a `'memory'`/`'none'` provider in front of
|
|
@@ -417,8 +418,10 @@ export interface WaCreateStoreOptions<B extends string = string, TBackends exten
|
|
|
417
418
|
/** Cache trusted-contact tokens (jid→record). Invalidate-on-write. */
|
|
418
419
|
readonly privacyToken?: boolean;
|
|
419
420
|
/**
|
|
420
|
-
* Per-domain L1 entry caps
|
|
421
|
-
* the
|
|
421
|
+
* Per-domain L1 entry caps. Past the cap the least recently written
|
|
422
|
+
* entry goes or, when the domain has a `ttlMs`, the least recently
|
|
423
|
+
* used one (recency counted in sweep ticks). Defaults to the matching
|
|
424
|
+
* memory-provider default when unset.
|
|
422
425
|
*/
|
|
423
426
|
readonly limits?: {
|
|
424
427
|
readonly session?: number;
|
|
@@ -426,6 +429,42 @@ export interface WaCreateStoreOptions<B extends string = string, TBackends exten
|
|
|
426
429
|
readonly senderKey?: number;
|
|
427
430
|
readonly privacyToken?: number;
|
|
428
431
|
};
|
|
432
|
+
/**
|
|
433
|
+
* Per-domain L1 idle TTLs in milliseconds, each a safe integer of at
|
|
434
|
+
* least 1_000. An entry neither read nor written for this long leaves
|
|
435
|
+
* the L1, so a long-lived process keeps the peers it is actively
|
|
436
|
+
* talking to instead of every peer seen since boot. Unset (the
|
|
437
|
+
* default) keeps entries until `limits` evicts them. A TTL on a
|
|
438
|
+
* domain that gets no L1 (flag off, or a `'memory'`/`'none'`
|
|
439
|
+
* provider) is ignored with a warning on the store `logger`.
|
|
440
|
+
*
|
|
441
|
+
* Idle time is counted in ticks of one sweep timer per store, whose
|
|
442
|
+
* period is the smallest `ttlMs / 2` clamped to [1 s, 60 s], so
|
|
443
|
+
* wall-clock steps never expire or keep an entry. An entry goes after
|
|
444
|
+
* at least `ttlMs` idle and less than `ttlMs` plus two periods.
|
|
445
|
+
*
|
|
446
|
+
* The TTL bounds residency, not freshness. The L1 stays coherent with
|
|
447
|
+
* the backend (write-through, or invalidate-on-write), so expiry never
|
|
448
|
+
* changes what a read returns: an expired entry is re-read from the
|
|
449
|
+
* backend on its next use, and the backend rows are never touched.
|
|
450
|
+
* Size it against that miss cost - each expiry turns the next lookup
|
|
451
|
+
* of that peer into one backend read.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```ts
|
|
455
|
+
* cacheLayer: {
|
|
456
|
+
* session: true,
|
|
457
|
+
* identity: true,
|
|
458
|
+
* ttlMs: { sessionMs: 30 * 60_000, identityMs: 30 * 60_000 }
|
|
459
|
+
* }
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
readonly ttlMs?: {
|
|
463
|
+
readonly sessionMs?: number;
|
|
464
|
+
readonly identityMs?: number;
|
|
465
|
+
readonly senderKeyMs?: number;
|
|
466
|
+
readonly privacyTokenMs?: number;
|
|
467
|
+
};
|
|
429
468
|
};
|
|
430
469
|
/**
|
|
431
470
|
* Memory-store tuning for the built-in `'memory'` providers. `limits`
|
|
@@ -16,7 +16,9 @@ export declare class WaMobileTcpSocket implements RawWebSocket {
|
|
|
16
16
|
private closedReason;
|
|
17
17
|
private closedClean;
|
|
18
18
|
private forceCloseTimer;
|
|
19
|
-
constructor(url: string, _protocols?: unknown,
|
|
19
|
+
constructor(url: string, _protocols?: unknown, options?: WaRawWebSocketInit);
|
|
20
|
+
private markOpen;
|
|
21
|
+
private emitMessage;
|
|
20
22
|
send(data: string | ArrayBuffer | Uint8Array): void;
|
|
21
23
|
close(code?: number, reason?: string): void;
|
|
22
24
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WaMobileTcpSocketCtor = exports.WaMobileTcpSocket = void 0;
|
|
4
4
|
const node_net_1 = require("node:net");
|
|
5
5
|
const constants_1 = require("../../protocol/constants");
|
|
6
|
+
const proxy_1 = require("../proxy");
|
|
6
7
|
const bytes_1 = require("../../util/bytes");
|
|
7
8
|
/**
|
|
8
9
|
* `RawWebSocket`-shaped adapter over a raw Node TCP socket. Used by the
|
|
@@ -10,7 +11,7 @@ const bytes_1 = require("../../util/bytes");
|
|
|
10
11
|
* `tcp://host:port` URLs.
|
|
11
12
|
*/
|
|
12
13
|
class WaMobileTcpSocket {
|
|
13
|
-
constructor(url, _protocols,
|
|
14
|
+
constructor(url, _protocols, options) {
|
|
14
15
|
this.binaryType = 'arraybuffer';
|
|
15
16
|
this.readyState = constants_1.WA_READY_STATES.CONNECTING;
|
|
16
17
|
this.onopen = null;
|
|
@@ -22,19 +23,55 @@ class WaMobileTcpSocket {
|
|
|
22
23
|
this.closedClean = true;
|
|
23
24
|
this.forceCloseTimer = null;
|
|
24
25
|
const { host, port } = parseTcpUrl(url);
|
|
25
|
-
|
|
26
|
+
const proxy = (0, proxy_1.toTcpProxyEndpoint)(options?.agent);
|
|
27
|
+
this.socket = proxy
|
|
28
|
+
? (0, node_net_1.connect)({ host: proxy.hostname, port: proxy.port })
|
|
29
|
+
: (0, node_net_1.connect)({ host, port });
|
|
30
|
+
let tunnelReady = !proxy;
|
|
31
|
+
let proxyResponse = new Uint8Array(0);
|
|
26
32
|
this.socket.on('connect', () => {
|
|
27
33
|
if (this.readyState !== constants_1.WA_READY_STATES.CONNECTING)
|
|
28
34
|
return;
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
if (proxy) {
|
|
36
|
+
const authority = `${host}:${port}`;
|
|
37
|
+
const lines = [
|
|
38
|
+
`CONNECT ${authority} HTTP/1.1`,
|
|
39
|
+
`Host: ${authority}`,
|
|
40
|
+
'Proxy-Connection: Keep-Alive'
|
|
41
|
+
];
|
|
42
|
+
if (proxy.authorization)
|
|
43
|
+
lines.push(`Proxy-Authorization: ${proxy.authorization}`);
|
|
44
|
+
this.socket.write(bytes_1.TEXT_ENCODER.encode(`${lines.join('\r\n')}\r\n\r\n`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.markOpen();
|
|
31
48
|
});
|
|
32
49
|
this.socket.on('data', (chunk) => {
|
|
33
|
-
if (!
|
|
50
|
+
if (!tunnelReady && proxy) {
|
|
51
|
+
proxyResponse = (0, bytes_1.concatBytes)([proxyResponse, chunk]);
|
|
52
|
+
const headerEnd = findHttpHeaderEnd(proxyResponse);
|
|
53
|
+
const scanned = headerEnd === -1 ? proxyResponse.byteLength : headerEnd;
|
|
54
|
+
if (scanned > MAX_PROXY_HEADER_BYTES) {
|
|
55
|
+
this.socket.destroy(new Error('WaMobileTcpSocket: proxy response headers too large'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (headerEnd === -1) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const statusLine = bytes_1.TEXT_DECODER.decode(proxyResponse.subarray(0, headerEnd)).split('\r\n')[0];
|
|
62
|
+
if (!/^HTTP\/1\.[01] 2\d\d(?:\s|$)/.test(statusLine)) {
|
|
63
|
+
this.socket.destroy(new Error(`WaMobileTcpSocket: proxy CONNECT failed (${statusLine})`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const remaining = proxyResponse.subarray(headerEnd + 4);
|
|
67
|
+
proxyResponse = new Uint8Array(0);
|
|
68
|
+
tunnelReady = true;
|
|
69
|
+
this.markOpen();
|
|
70
|
+
if (remaining.byteLength > 0)
|
|
71
|
+
this.emitMessage(remaining);
|
|
34
72
|
return;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.onmessage({ data: copy });
|
|
73
|
+
}
|
|
74
|
+
this.emitMessage(chunk);
|
|
38
75
|
});
|
|
39
76
|
this.socket.on('error', (err) => {
|
|
40
77
|
this.closedClean = false;
|
|
@@ -56,6 +93,19 @@ class WaMobileTcpSocket {
|
|
|
56
93
|
});
|
|
57
94
|
});
|
|
58
95
|
}
|
|
96
|
+
markOpen() {
|
|
97
|
+
if (this.readyState !== constants_1.WA_READY_STATES.CONNECTING)
|
|
98
|
+
return;
|
|
99
|
+
this.readyState = constants_1.WA_READY_STATES.OPEN;
|
|
100
|
+
this.onopen?.({});
|
|
101
|
+
}
|
|
102
|
+
emitMessage(chunk) {
|
|
103
|
+
if (!this.onmessage || this.readyState !== constants_1.WA_READY_STATES.OPEN)
|
|
104
|
+
return;
|
|
105
|
+
const copy = new Uint8Array(chunk.byteLength);
|
|
106
|
+
copy.set(chunk);
|
|
107
|
+
this.onmessage({ data: copy });
|
|
108
|
+
}
|
|
59
109
|
send(data) {
|
|
60
110
|
if (this.readyState !== constants_1.WA_READY_STATES.OPEN) {
|
|
61
111
|
throw new Error('WaMobileTcpSocket: send() called on non-OPEN socket');
|
|
@@ -89,6 +139,16 @@ class WaMobileTcpSocket {
|
|
|
89
139
|
}
|
|
90
140
|
}
|
|
91
141
|
exports.WaMobileTcpSocket = WaMobileTcpSocket;
|
|
142
|
+
/** Cap on the CONNECT response headers buffered before the tunnel is rejected. */
|
|
143
|
+
const MAX_PROXY_HEADER_BYTES = 65536;
|
|
144
|
+
function findHttpHeaderEnd(bytes) {
|
|
145
|
+
for (let i = 3; i < bytes.byteLength; i += 1) {
|
|
146
|
+
if (bytes[i - 3] === 13 && bytes[i - 2] === 10 && bytes[i - 1] === 13 && bytes[i] === 10) {
|
|
147
|
+
return i - 3;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
92
152
|
function parseTcpUrl(url) {
|
|
93
153
|
let work = url;
|
|
94
154
|
if (work.startsWith('tcp://')) {
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { type Proto } from '../../proto';
|
|
2
|
+
/**
|
|
3
|
+
* Distribution channel advertised by an iOS login (`UserAgent.distributionChannel`).
|
|
4
|
+
* iOS-only – Android leaves the field unset. `appstore` is the normal
|
|
5
|
+
* production install; `testflight`/`internal` mark beta builds.
|
|
6
|
+
*/
|
|
7
|
+
export type WaMobileDistributionChannel = 'appstore' | 'website' | 'testflight' | 'internal';
|
|
2
8
|
export interface WaMobileTransportDeviceInfo {
|
|
9
|
+
/**
|
|
10
|
+
* Operating system the login payload impersonates. `android` (the default,
|
|
11
|
+
* for backwards compatibility) advertises the `ANDROID`/`SMB_ANDROID`
|
|
12
|
+
* platform and fills the Android-only `mcc`/`mnc`/`deviceBoard` fields.
|
|
13
|
+
* `ios` advertises `IOS`/`SMB_IOS`, omits those Android-only fields (the
|
|
14
|
+
* real iPhone client never sends them) and carries `distributionChannel`
|
|
15
|
+
* instead.
|
|
16
|
+
*
|
|
17
|
+
* Field mapping differs by OS and must match the real client:
|
|
18
|
+
* - **Android** `device` = a device codename (e.g. `moto_g52`).
|
|
19
|
+
* - **iOS** `device` = the marketing name (e.g. `iPhone 15 Pro`) while
|
|
20
|
+
* {@link deviceModelType} carries the raw machine id (`iPhone16,1`).
|
|
21
|
+
*/
|
|
22
|
+
readonly os?: 'android' | 'ios';
|
|
3
23
|
readonly manufacturer: string;
|
|
4
24
|
readonly device: string;
|
|
5
25
|
readonly osVersion: string;
|
|
@@ -12,6 +32,8 @@ export interface WaMobileTransportDeviceInfo {
|
|
|
12
32
|
readonly phoneId?: string;
|
|
13
33
|
readonly deviceBoard?: string;
|
|
14
34
|
readonly deviceModelType?: string;
|
|
35
|
+
/** iOS distribution channel; ignored for Android. Defaults to `appstore`. */
|
|
36
|
+
readonly distributionChannel?: WaMobileDistributionChannel;
|
|
15
37
|
readonly business?: boolean;
|
|
16
38
|
}
|
|
17
39
|
export interface WaMobileLoginPayloadConfig {
|
|
@@ -16,6 +16,20 @@ function parseAppVersion(version) {
|
|
|
16
16
|
quaternary: at(3)
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
function distributionChannelId(channel) {
|
|
20
|
+
const { DistributionChannel } = _proto_1.proto.ClientPayload.UserAgent;
|
|
21
|
+
switch (channel) {
|
|
22
|
+
case 'website':
|
|
23
|
+
return DistributionChannel.WEBSITE;
|
|
24
|
+
case 'testflight':
|
|
25
|
+
return DistributionChannel.TESTFLIGHT;
|
|
26
|
+
case 'internal':
|
|
27
|
+
return DistributionChannel.INTERNAL;
|
|
28
|
+
case 'appstore':
|
|
29
|
+
case undefined:
|
|
30
|
+
return DistributionChannel.APPSTORE;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
/**
|
|
20
34
|
* Builds the encoded {@link Proto.ClientPayload} bytes the WhatsApp Mobile
|
|
21
35
|
* transport sends after the noise login handshake. Throws when
|
|
@@ -27,14 +41,18 @@ function buildMobileLoginPayload(config) {
|
|
|
27
41
|
}
|
|
28
42
|
const info = config.deviceInfo;
|
|
29
43
|
const version = parseAppVersion(info.appVersion);
|
|
30
|
-
const
|
|
31
|
-
|
|
44
|
+
const isIos = info.os === 'ios';
|
|
45
|
+
const platform = isIos
|
|
46
|
+
? info.business
|
|
47
|
+
? _proto_1.proto.ClientPayload.UserAgent.Platform.SMB_IOS
|
|
48
|
+
: _proto_1.proto.ClientPayload.UserAgent.Platform.IOS
|
|
49
|
+
: info.business
|
|
32
50
|
? _proto_1.proto.ClientPayload.UserAgent.Platform.SMB_ANDROID
|
|
33
|
-
: _proto_1.proto.ClientPayload.UserAgent.Platform.ANDROID
|
|
51
|
+
: _proto_1.proto.ClientPayload.UserAgent.Platform.ANDROID;
|
|
52
|
+
const userAgent = {
|
|
53
|
+
platform,
|
|
34
54
|
releaseChannel: _proto_1.proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
|
35
55
|
appVersion: version,
|
|
36
|
-
mcc: info.mcc ?? '000',
|
|
37
|
-
mnc: info.mnc ?? '000',
|
|
38
56
|
osVersion: info.osVersion,
|
|
39
57
|
manufacturer: info.manufacturer,
|
|
40
58
|
device: info.device,
|
|
@@ -43,8 +61,10 @@ function buildMobileLoginPayload(config) {
|
|
|
43
61
|
localeLanguageIso6391: info.localeLanguageIso6391 ?? 'en',
|
|
44
62
|
localeCountryIso31661Alpha2: info.localeCountryIso31661Alpha2 ?? 'US',
|
|
45
63
|
deviceType: _proto_1.proto.ClientPayload.UserAgent.DeviceType.PHONE,
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
deviceModelType: info.deviceModelType,
|
|
65
|
+
...(isIos
|
|
66
|
+
? { distributionChannel: distributionChannelId(info.distributionChannel) }
|
|
67
|
+
: { mcc: info.mcc ?? '000', mnc: info.mnc ?? '000', deviceBoard: info.deviceBoard })
|
|
48
68
|
};
|
|
49
69
|
return _proto_1.proto.ClientPayload.encode({
|
|
50
70
|
passive: config.passive === true,
|
|
@@ -9,3 +9,27 @@ export declare function isProxyTransport(value: unknown): value is WaProxyTransp
|
|
|
9
9
|
export declare function toProxyDispatcher(proxy: WaProxyTransport | undefined): WaProxyDispatcher | undefined;
|
|
10
10
|
/** Narrows `proxy` to {@link WaProxyAgent} or returns `undefined`. */
|
|
11
11
|
export declare function toProxyAgent(proxy: WaProxyTransport | undefined): WaProxyAgent | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* HTTP CONNECT endpoint a raw TCP transport dials to reach its destination.
|
|
14
|
+
*
|
|
15
|
+
* @sensitive Contains proxy credentials (`authorization`). Never log, serialize
|
|
16
|
+
* via `JSON.stringify`, or transmit unencrypted. Persist with encryption-at-rest.
|
|
17
|
+
*/
|
|
18
|
+
export interface WaTcpProxyEndpoint {
|
|
19
|
+
readonly hostname: string;
|
|
20
|
+
readonly port: number;
|
|
21
|
+
/** Ready-to-send `Proxy-Authorization` value when the proxy url carries credentials. */
|
|
22
|
+
readonly authorization?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the proxy endpoint a raw TCP transport tunnels through with HTTP
|
|
26
|
+
* CONNECT.
|
|
27
|
+
*
|
|
28
|
+
* Returns `undefined` only when no proxy is configured. Proxy shapes the tunnel
|
|
29
|
+
* cannot honour throw instead of resolving to `undefined`, because dropping one
|
|
30
|
+
* silently would dial the destination directly – the opposite of what a
|
|
31
|
+
* deployment that pins its egress to a proxy asked for.
|
|
32
|
+
*/
|
|
33
|
+
export declare function toTcpProxyEndpoint(proxy: WaProxyTransport | undefined): WaTcpProxyEndpoint | undefined;
|
|
34
|
+
/** Throws when {@link toTcpProxyEndpoint} cannot honour `proxy`. */
|
|
35
|
+
export declare function assertTcpProxySupported(proxy: WaProxyTransport | undefined): void;
|