@zapo-js/fake-server 1.0.0 → 1.2.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 +180 -33
- package/dist/api/FakeMobilePrimary.d.ts +55 -0
- package/dist/api/FakeMobilePrimary.js +106 -0
- package/dist/api/FakePairingDriver.js +1 -3
- package/dist/api/FakePeer.js +7 -1
- package/dist/api/FakeServerSession.d.ts +100 -0
- package/dist/api/FakeServerSession.js +355 -0
- package/dist/api/FakeWaServer.d.ts +145 -37
- package/dist/api/FakeWaServer.js +345 -270
- package/dist/api/ServerRegistries.d.ts +7 -0
- package/dist/api/ServerRegistries.js +20 -5
- package/dist/api/iq-handlers.d.ts +20 -1
- package/dist/api/iq-handlers.js +119 -1
- package/dist/bin.js +69 -21
- package/dist/esm/api/FakeMobilePrimary.js +102 -0
- package/dist/esm/api/FakePairingDriver.js +2 -4
- package/dist/esm/api/FakePeer.js +7 -1
- package/dist/esm/api/FakeServerSession.js +349 -0
- package/dist/esm/api/FakeWaServer.js +344 -270
- package/dist/esm/api/ServerRegistries.js +20 -5
- package/dist/esm/api/iq-handlers.js +119 -1
- package/dist/esm/bin.js +67 -19
- package/dist/esm/index.js +9 -1
- package/dist/esm/infra/WaFakeConnection.js +8 -22
- package/dist/esm/infra/WaFakeConnectionPipeline.js +38 -14
- package/dist/esm/infra/WaFakeTcpServer.js +69 -0
- package/dist/esm/infra/WaFakeWsServer.js +2 -1
- package/dist/esm/infra/socket-adapters.js +79 -0
- package/dist/esm/protocol/auth/cert-chain.js +13 -2
- package/dist/esm/protocol/auth/client-payload-validate.js +77 -1
- package/dist/esm/protocol/auth/pair-device.js +16 -1
- package/dist/esm/protocol/iq/companion-host.js +104 -0
- package/dist/esm/protocol/iq/link-code.js +90 -0
- package/dist/esm/protocol/iq/router.js +9 -3
- package/dist/esm/protocol/push/mobile-notification.js +97 -0
- package/dist/esm/state/fake-companion-host.js +158 -0
- package/dist/esm/transport/auth.js +7 -0
- package/dist/esm/transport/crypto.js +1 -1
- package/dist/esm/transport/util.js +2 -1
- package/dist/index.d.ts +19 -4
- package/dist/index.js +29 -1
- package/dist/infra/WaFakeConnection.d.ts +21 -3
- package/dist/infra/WaFakeConnection.js +8 -22
- package/dist/infra/WaFakeConnectionPipeline.d.ts +11 -2
- package/dist/infra/WaFakeConnectionPipeline.js +38 -14
- package/dist/infra/WaFakeTcpServer.d.ts +29 -0
- package/dist/infra/WaFakeTcpServer.js +73 -0
- package/dist/infra/WaFakeWsServer.js +2 -1
- package/dist/infra/socket-adapters.d.ts +18 -0
- package/dist/infra/socket-adapters.js +83 -0
- package/dist/protocol/auth/cert-chain.d.ts +4 -0
- package/dist/protocol/auth/cert-chain.js +13 -2
- package/dist/protocol/auth/client-payload-validate.d.ts +24 -0
- package/dist/protocol/auth/client-payload-validate.js +77 -1
- package/dist/protocol/auth/pair-device.d.ts +15 -1
- package/dist/protocol/auth/pair-device.js +16 -0
- package/dist/protocol/iq/companion-host.d.ts +52 -0
- package/dist/protocol/iq/companion-host.js +109 -0
- package/dist/protocol/iq/link-code.d.ts +46 -0
- package/dist/protocol/iq/link-code.js +95 -0
- package/dist/protocol/iq/router.d.ts +21 -2
- package/dist/protocol/iq/router.js +9 -3
- package/dist/protocol/push/mobile-notification.d.ts +58 -0
- package/dist/protocol/push/mobile-notification.js +102 -0
- package/dist/state/fake-companion-host.d.ts +73 -0
- package/dist/state/fake-companion-host.js +163 -0
- package/dist/transport/auth.d.ts +8 -0
- package/dist/transport/auth.js +8 -0
- package/dist/transport/crypto.d.ts +1 -1
- package/dist/transport/crypto.js +2 -1
- package/dist/transport/util.d.ts +2 -1
- package/dist/transport/util.js +3 -1
- package/package.json +6 -1
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
/** IQ matcher/dispatcher used by the fake server. */
|
|
2
2
|
import type { BinaryNode } from '../../transport/codec';
|
|
3
|
+
import type { ParsedClientPayload } from '../auth/client-payload-validate';
|
|
3
4
|
export type WaFakeIqType = 'get' | 'set';
|
|
5
|
+
/**
|
|
6
|
+
* The connection an IQ arrived on. Handlers that relay between two clients
|
|
7
|
+
* (companion pairing, link-code) need to know who is asking; `WaFakeConnection
|
|
8
|
+
* Pipeline` satisfies this structurally.
|
|
9
|
+
*/
|
|
10
|
+
export interface WaFakeIqConnection {
|
|
11
|
+
sendStanza(node: BinaryNode): Promise<void>;
|
|
12
|
+
readonly clientPayload: ParsedClientPayload | null;
|
|
13
|
+
}
|
|
14
|
+
export interface WaFakeIqContext {
|
|
15
|
+
readonly connection: WaFakeIqConnection;
|
|
16
|
+
}
|
|
4
17
|
export interface WaFakeIqMatcher {
|
|
5
18
|
readonly id?: string;
|
|
6
19
|
readonly type?: WaFakeIqType;
|
|
7
20
|
readonly xmlns?: string;
|
|
8
21
|
readonly childTag?: string;
|
|
9
22
|
}
|
|
10
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Returns the response stanza, or `null` to fall through: the router keeps
|
|
25
|
+
* scanning lower-priority handlers as if this one had not matched. Lets a
|
|
26
|
+
* high-priority handler observe an IQ (capture, log, assert) while the
|
|
27
|
+
* default handler still answers it.
|
|
28
|
+
*/
|
|
29
|
+
export type WaFakeIqResponder = (iq: BinaryNode, context?: WaFakeIqContext) => BinaryNode | null | Promise<BinaryNode | null>;
|
|
11
30
|
export interface WaFakeIqHandler {
|
|
12
31
|
readonly matcher: WaFakeIqMatcher;
|
|
13
32
|
readonly respond: WaFakeIqResponder;
|
|
@@ -25,7 +44,7 @@ export declare class WaFakeIqRouter {
|
|
|
25
44
|
readonly priority?: 'high' | 'default';
|
|
26
45
|
}): () => void;
|
|
27
46
|
setEvents(events: WaFakeIqRouterEvents): void;
|
|
28
|
-
route(iq: BinaryNode): Promise<BinaryNode | null>;
|
|
47
|
+
route(iq: BinaryNode, context?: WaFakeIqContext): Promise<BinaryNode | null>;
|
|
29
48
|
}
|
|
30
49
|
export declare function buildIqResult(inbound: BinaryNode, options?: {
|
|
31
50
|
readonly from?: string;
|
|
@@ -24,18 +24,24 @@ class WaFakeIqRouter {
|
|
|
24
24
|
setEvents(events) {
|
|
25
25
|
this.events = events;
|
|
26
26
|
}
|
|
27
|
-
async route(iq) {
|
|
27
|
+
async route(iq, context) {
|
|
28
28
|
if (iq.tag !== 'iq') {
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
for (const handler of this.highPriorityHandlers) {
|
|
32
32
|
if (matches(iq, handler.matcher)) {
|
|
33
|
-
|
|
33
|
+
const response = await handler.respond(iq, context);
|
|
34
|
+
if (response !== null) {
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
for (const handler of this.handlers) {
|
|
37
40
|
if (matches(iq, handler.matcher)) {
|
|
38
|
-
|
|
41
|
+
const response = await handler.respond(iq, context);
|
|
42
|
+
if (response !== null) {
|
|
43
|
+
return response;
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
this.events.onUnhandled?.(iq);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push notifications only a phone receives.
|
|
3
|
+
*
|
|
4
|
+
* `account_sync` carries the account's live device set: the primary keys its
|
|
5
|
+
* companion bookkeeping off it, allocating a fresh key index for the next link
|
|
6
|
+
* and dropping companions the server no longer lists. The registration
|
|
7
|
+
* notifications carry the login code and the takeover warning that fire while
|
|
8
|
+
* a number is being moved to another phone.
|
|
9
|
+
*/
|
|
10
|
+
import type { BinaryNode } from '../../transport/codec';
|
|
11
|
+
export interface FakeAccountDevice {
|
|
12
|
+
readonly deviceJid: string;
|
|
13
|
+
/** ADV key index of the device; the primary keeps every listed one valid. */
|
|
14
|
+
readonly keyIndex?: number;
|
|
15
|
+
readonly deviceLid?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface BuildAccountSyncDevicesInput {
|
|
18
|
+
readonly devices: readonly FakeAccountDevice[];
|
|
19
|
+
readonly id?: string;
|
|
20
|
+
readonly from?: string;
|
|
21
|
+
readonly timestampSeconds?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Builds `<notification type="account_sync">` with the account's `<devices>`
|
|
25
|
+
* list. Every device the account still owns must be present: the primary reads
|
|
26
|
+
* this as authoritative and prunes anything missing from it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildAccountSyncDevicesNotification(input: BuildAccountSyncDevicesInput): BinaryNode;
|
|
29
|
+
export interface BuildRegistrationCodeNotificationInput {
|
|
30
|
+
/** The login code shown on the new device. */
|
|
31
|
+
readonly code: string;
|
|
32
|
+
/** Device that requested it, as reported by the server. */
|
|
33
|
+
readonly fromDeviceId: string;
|
|
34
|
+
readonly expirySeconds?: number;
|
|
35
|
+
readonly id?: string;
|
|
36
|
+
readonly from?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Builds the `<notification type="registration">` carrying a login code, which
|
|
40
|
+
* the client surfaces as `mobile_registration_code`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildRegistrationCodeNotification(input: BuildRegistrationCodeNotificationInput): BinaryNode;
|
|
43
|
+
export interface BuildAccountTakeoverNoticeInput {
|
|
44
|
+
/** Server token that identifies the takeover attempt. */
|
|
45
|
+
readonly serverToken: string;
|
|
46
|
+
readonly attemptTimestampSeconds?: number;
|
|
47
|
+
readonly newDeviceName?: string;
|
|
48
|
+
readonly newDevicePlatform?: string;
|
|
49
|
+
readonly newDeviceAppVersion?: string;
|
|
50
|
+
readonly id?: string;
|
|
51
|
+
readonly from?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Builds the `<notification type="registration">` warning that the number is
|
|
55
|
+
* being registered on another phone, surfaced as
|
|
56
|
+
* `mobile_account_takeover_notice`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildAccountTakeoverNotice(input: BuildAccountTakeoverNoticeInput): BinaryNode;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Push notifications only a phone receives.
|
|
4
|
+
*
|
|
5
|
+
* `account_sync` carries the account's live device set: the primary keys its
|
|
6
|
+
* companion bookkeeping off it, allocating a fresh key index for the next link
|
|
7
|
+
* and dropping companions the server no longer lists. The registration
|
|
8
|
+
* notifications carry the login code and the takeover warning that fire while
|
|
9
|
+
* a number is being moved to another phone.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.buildAccountSyncDevicesNotification = buildAccountSyncDevicesNotification;
|
|
13
|
+
exports.buildRegistrationCodeNotification = buildRegistrationCodeNotification;
|
|
14
|
+
exports.buildAccountTakeoverNotice = buildAccountTakeoverNotice;
|
|
15
|
+
/**
|
|
16
|
+
* Builds `<notification type="account_sync">` with the account's `<devices>`
|
|
17
|
+
* list. Every device the account still owns must be present: the primary reads
|
|
18
|
+
* this as authoritative and prunes anything missing from it.
|
|
19
|
+
*/
|
|
20
|
+
function buildAccountSyncDevicesNotification(input) {
|
|
21
|
+
return {
|
|
22
|
+
tag: 'notification',
|
|
23
|
+
attrs: {
|
|
24
|
+
id: input.id ?? `account-sync-${Math.random().toString(36).slice(2, 10)}`,
|
|
25
|
+
from: input.from ?? 's.whatsapp.net',
|
|
26
|
+
type: 'account_sync',
|
|
27
|
+
t: String(input.timestampSeconds ?? Math.floor(Date.now() / 1000))
|
|
28
|
+
},
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
tag: 'devices',
|
|
32
|
+
attrs: {},
|
|
33
|
+
content: input.devices.map((device) => ({
|
|
34
|
+
tag: 'device',
|
|
35
|
+
attrs: {
|
|
36
|
+
jid: device.deviceJid,
|
|
37
|
+
...(device.keyIndex !== undefined
|
|
38
|
+
? { 'key-index': String(device.keyIndex) }
|
|
39
|
+
: {}),
|
|
40
|
+
...(device.deviceLid !== undefined ? { lid: device.deviceLid } : {})
|
|
41
|
+
}
|
|
42
|
+
}))
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Builds the `<notification type="registration">` carrying a login code, which
|
|
49
|
+
* the client surfaces as `mobile_registration_code`.
|
|
50
|
+
*/
|
|
51
|
+
function buildRegistrationCodeNotification(input) {
|
|
52
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
53
|
+
return {
|
|
54
|
+
tag: 'notification',
|
|
55
|
+
attrs: {
|
|
56
|
+
id: input.id ?? `registration-${Math.random().toString(36).slice(2, 10)}`,
|
|
57
|
+
from: input.from ?? 's.whatsapp.net',
|
|
58
|
+
type: 'registration'
|
|
59
|
+
},
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
tag: 'wa_old_registration',
|
|
63
|
+
attrs: {
|
|
64
|
+
code: input.code,
|
|
65
|
+
expiry_t: String(input.expirySeconds ?? nowSeconds + 300),
|
|
66
|
+
device_id: input.fromDeviceId
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Builds the `<notification type="registration">` warning that the number is
|
|
74
|
+
* being registered on another phone, surfaced as
|
|
75
|
+
* `mobile_account_takeover_notice`.
|
|
76
|
+
*/
|
|
77
|
+
function buildAccountTakeoverNotice(input) {
|
|
78
|
+
return {
|
|
79
|
+
tag: 'notification',
|
|
80
|
+
attrs: {
|
|
81
|
+
id: input.id ?? `takeover-${Math.random().toString(36).slice(2, 10)}`,
|
|
82
|
+
from: input.from ?? 's.whatsapp.net',
|
|
83
|
+
type: 'registration'
|
|
84
|
+
},
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
tag: 'device_logout',
|
|
88
|
+
attrs: {
|
|
89
|
+
id: input.serverToken,
|
|
90
|
+
t: String(input.attemptTimestampSeconds ?? Math.floor(Date.now() / 1000)),
|
|
91
|
+
...(input.newDeviceName !== undefined ? { device: input.newDeviceName } : {}),
|
|
92
|
+
...(input.newDevicePlatform !== undefined
|
|
93
|
+
? { new_device_platform: input.newDevicePlatform }
|
|
94
|
+
: {}),
|
|
95
|
+
...(input.newDeviceAppVersion !== undefined
|
|
96
|
+
? { new_device_app_version: input.newDeviceAppVersion }
|
|
97
|
+
: {})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Per-session account state for a mobile primary hosting companion devices. */
|
|
2
|
+
/** Identity of the mobile primary that owns this session's account. */
|
|
3
|
+
export interface FakeMobilePrimaryIdentity {
|
|
4
|
+
/** Phone number, digits only – the user part of every device jid. */
|
|
5
|
+
readonly username: string;
|
|
6
|
+
/** `<phone>@s.whatsapp.net` – the primary itself, always device 0. */
|
|
7
|
+
readonly jid: string;
|
|
8
|
+
}
|
|
9
|
+
export interface FakeLinkedCompanion {
|
|
10
|
+
readonly deviceJid: string;
|
|
11
|
+
readonly deviceId: number;
|
|
12
|
+
/** Pairing ref the companion advertised, echoed by the primary's upload. */
|
|
13
|
+
readonly ref: string;
|
|
14
|
+
/** ADV key index the primary allocated, decoded from the signed identity. */
|
|
15
|
+
readonly keyIndex: number;
|
|
16
|
+
readonly deviceIdentityBytes: Uint8Array;
|
|
17
|
+
readonly linkedAtSeconds: number;
|
|
18
|
+
}
|
|
19
|
+
export interface FakePublishedKeyIndexList {
|
|
20
|
+
readonly bytes: Uint8Array;
|
|
21
|
+
readonly timestampSeconds: number;
|
|
22
|
+
/** Indexes the primary declared valid, decoded from the signed list. */
|
|
23
|
+
readonly validIndexes: readonly number[];
|
|
24
|
+
readonly currentIndex: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class FakeCompanionHostState {
|
|
27
|
+
private primaryIdentity;
|
|
28
|
+
private readonly companions;
|
|
29
|
+
private readonly reservedDeviceIds;
|
|
30
|
+
private keyIndexList;
|
|
31
|
+
private readonly linkListeners;
|
|
32
|
+
private readonly revokeListeners;
|
|
33
|
+
/** The mobile primary bound to this session, or `null` for a web session. */
|
|
34
|
+
get primary(): FakeMobilePrimaryIdentity | null;
|
|
35
|
+
/**
|
|
36
|
+
* Records the primary at login. The first account to bind owns the session
|
|
37
|
+
* for good: a later login under a different number would otherwise mint
|
|
38
|
+
* device jids for one account while the companions of another are still
|
|
39
|
+
* tracked here.
|
|
40
|
+
*/
|
|
41
|
+
bindPrimary(identity: FakeMobilePrimaryIdentity): void;
|
|
42
|
+
/** Companions linked from this account, in link order. */
|
|
43
|
+
linkedCompanions(): readonly FakeLinkedCompanion[];
|
|
44
|
+
/** Last key-index list the primary published, or `null` if it never did. */
|
|
45
|
+
publishedKeyIndexList(): FakePublishedKeyIndexList | null;
|
|
46
|
+
/**
|
|
47
|
+
* Reserves the next free device slot. The primary owns device 0, so
|
|
48
|
+
* companions start at 1 and a revoked slot is reused only once no live
|
|
49
|
+
* companion holds it. The slot is held from here until the link is either
|
|
50
|
+
* recorded or released, because a link spans an await and two overlapping
|
|
51
|
+
* pairings would otherwise be handed the same jid.
|
|
52
|
+
*/
|
|
53
|
+
allocateDeviceId(): number;
|
|
54
|
+
/** Frees a slot whose link never completed. */
|
|
55
|
+
releaseDeviceId(deviceId: number): void;
|
|
56
|
+
recordCompanion(companion: FakeLinkedCompanion): void;
|
|
57
|
+
/**
|
|
58
|
+
* Drops companions and notifies listeners. Returns the jids actually
|
|
59
|
+
* removed, so a revoke of an untracked device is observable as a no-op.
|
|
60
|
+
*/
|
|
61
|
+
removeCompanions(deviceJids: readonly string[]): readonly string[];
|
|
62
|
+
removeAllCompanions(): readonly string[];
|
|
63
|
+
recordKeyIndexList(bytes: Uint8Array, timestampSeconds: number): void;
|
|
64
|
+
onCompanionLinked(listener: (companion: FakeLinkedCompanion) => void): () => void;
|
|
65
|
+
onCompanionsRevoked(listener: (deviceJids: readonly string[]) => void): () => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reads the ADV key index out of the `ADVSignedDeviceIdentityHMAC` the primary
|
|
69
|
+
* uploaded. Returns 0 when the payload does not decode, which keeps a
|
|
70
|
+
* malformed upload observable in the recorded companion instead of throwing
|
|
71
|
+
* inside the IQ handler.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readCompanionKeyIndex(deviceIdentityBytes: Uint8Array): number;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Per-session account state for a mobile primary hosting companion devices. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.FakeCompanionHostState = void 0;
|
|
5
|
+
exports.readCompanionKeyIndex = readCompanionKeyIndex;
|
|
6
|
+
const protos_1 = require("../transport/protos");
|
|
7
|
+
class FakeCompanionHostState {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.primaryIdentity = null;
|
|
10
|
+
this.companions = new Map();
|
|
11
|
+
this.reservedDeviceIds = new Set();
|
|
12
|
+
this.keyIndexList = null;
|
|
13
|
+
this.linkListeners = new Set();
|
|
14
|
+
this.revokeListeners = new Set();
|
|
15
|
+
}
|
|
16
|
+
/** The mobile primary bound to this session, or `null` for a web session. */
|
|
17
|
+
get primary() {
|
|
18
|
+
return this.primaryIdentity;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Records the primary at login. The first account to bind owns the session
|
|
22
|
+
* for good: a later login under a different number would otherwise mint
|
|
23
|
+
* device jids for one account while the companions of another are still
|
|
24
|
+
* tracked here.
|
|
25
|
+
*/
|
|
26
|
+
bindPrimary(identity) {
|
|
27
|
+
if (this.primaryIdentity) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.primaryIdentity = identity;
|
|
31
|
+
}
|
|
32
|
+
/** Companions linked from this account, in link order. */
|
|
33
|
+
linkedCompanions() {
|
|
34
|
+
return [...this.companions.values()];
|
|
35
|
+
}
|
|
36
|
+
/** Last key-index list the primary published, or `null` if it never did. */
|
|
37
|
+
publishedKeyIndexList() {
|
|
38
|
+
return this.keyIndexList;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reserves the next free device slot. The primary owns device 0, so
|
|
42
|
+
* companions start at 1 and a revoked slot is reused only once no live
|
|
43
|
+
* companion holds it. The slot is held from here until the link is either
|
|
44
|
+
* recorded or released, because a link spans an await and two overlapping
|
|
45
|
+
* pairings would otherwise be handed the same jid.
|
|
46
|
+
*/
|
|
47
|
+
allocateDeviceId() {
|
|
48
|
+
const taken = new Set([0, ...this.reservedDeviceIds]);
|
|
49
|
+
for (const companion of this.companions.values()) {
|
|
50
|
+
taken.add(companion.deviceId);
|
|
51
|
+
}
|
|
52
|
+
let candidate = 1;
|
|
53
|
+
while (taken.has(candidate)) {
|
|
54
|
+
candidate += 1;
|
|
55
|
+
}
|
|
56
|
+
this.reservedDeviceIds.add(candidate);
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
/** Frees a slot whose link never completed. */
|
|
60
|
+
releaseDeviceId(deviceId) {
|
|
61
|
+
this.reservedDeviceIds.delete(deviceId);
|
|
62
|
+
}
|
|
63
|
+
recordCompanion(companion) {
|
|
64
|
+
this.reservedDeviceIds.delete(companion.deviceId);
|
|
65
|
+
this.companions.set(companion.deviceJid, companion);
|
|
66
|
+
for (const listener of this.linkListeners) {
|
|
67
|
+
try {
|
|
68
|
+
listener(companion);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
void error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Drops companions and notifies listeners. Returns the jids actually
|
|
77
|
+
* removed, so a revoke of an untracked device is observable as a no-op.
|
|
78
|
+
*/
|
|
79
|
+
removeCompanions(deviceJids) {
|
|
80
|
+
const removed = [];
|
|
81
|
+
for (const deviceJid of deviceJids) {
|
|
82
|
+
if (this.companions.delete(deviceJid)) {
|
|
83
|
+
removed.push(deviceJid);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (removed.length > 0) {
|
|
87
|
+
for (const listener of this.revokeListeners) {
|
|
88
|
+
try {
|
|
89
|
+
listener(removed);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
void error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
removeAllCompanions() {
|
|
99
|
+
return this.removeCompanions([...this.companions.keys()]);
|
|
100
|
+
}
|
|
101
|
+
recordKeyIndexList(bytes, timestampSeconds) {
|
|
102
|
+
const decoded = decodeKeyIndexList(bytes);
|
|
103
|
+
this.keyIndexList = {
|
|
104
|
+
bytes,
|
|
105
|
+
timestampSeconds,
|
|
106
|
+
validIndexes: decoded.validIndexes,
|
|
107
|
+
currentIndex: decoded.currentIndex
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
onCompanionLinked(listener) {
|
|
111
|
+
this.linkListeners.add(listener);
|
|
112
|
+
return () => {
|
|
113
|
+
this.linkListeners.delete(listener);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
onCompanionsRevoked(listener) {
|
|
117
|
+
this.revokeListeners.add(listener);
|
|
118
|
+
return () => {
|
|
119
|
+
this.revokeListeners.delete(listener);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.FakeCompanionHostState = FakeCompanionHostState;
|
|
124
|
+
/**
|
|
125
|
+
* Reads the ADV key index out of the `ADVSignedDeviceIdentityHMAC` the primary
|
|
126
|
+
* uploaded. Returns 0 when the payload does not decode, which keeps a
|
|
127
|
+
* malformed upload observable in the recorded companion instead of throwing
|
|
128
|
+
* inside the IQ handler.
|
|
129
|
+
*/
|
|
130
|
+
function readCompanionKeyIndex(deviceIdentityBytes) {
|
|
131
|
+
try {
|
|
132
|
+
const hmacWrapper = protos_1.proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityBytes);
|
|
133
|
+
if (!hmacWrapper.details) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
const signed = protos_1.proto.ADVSignedDeviceIdentity.decode(hmacWrapper.details);
|
|
137
|
+
if (!signed.details) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
return protos_1.proto.ADVDeviceIdentity.decode(signed.details).keyIndex ?? 0;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
void error;
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function decodeKeyIndexList(bytes) {
|
|
148
|
+
try {
|
|
149
|
+
const signed = protos_1.proto.ADVSignedKeyIndexList.decode(bytes);
|
|
150
|
+
if (!signed.details) {
|
|
151
|
+
return { validIndexes: [], currentIndex: 0 };
|
|
152
|
+
}
|
|
153
|
+
const details = protos_1.proto.ADVKeyIndexList.decode(signed.details);
|
|
154
|
+
return {
|
|
155
|
+
validIndexes: details.validIndexes ?? [],
|
|
156
|
+
currentIndex: details.currentIndex ?? 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
void error;
|
|
161
|
+
return { validIndexes: [], currentIndex: 0 };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1 – credential/store type wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the auth and store types from zapo-js so the fixture layer can
|
|
5
|
+
* seed a registered session without importing zapo-js directly.
|
|
6
|
+
*/
|
|
7
|
+
export type { WaAuthCredentials, WaMobileTransportDeviceInfo } from 'zapo-js/auth';
|
|
8
|
+
export type { WaStore } from 'zapo-js/store';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 1 – credential/store type wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Re-exports the auth and store types from zapo-js so the fixture layer can
|
|
6
|
+
* seed a registered session without importing zapo-js directly.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Re-exports bit-exact cryptographic primitives from zapo-js. No protocol
|
|
5
5
|
* interpretation, so reusing them does not create a test-vs-impl tautology.
|
|
6
6
|
*/
|
|
7
|
-
export { aesCbcDecrypt, aesCbcEncrypt, aesGcmDecrypt, aesGcmEncrypt, Ed25519, hkdf, hkdfSplit, hmacSha256Sign, hmacSha512Sign, prependVersion, randomBytesAsync, readVersionedContent, sha256, sha512, toRawPubKey, toSerializedPubKey, X25519, xeddsaSign, xeddsaVerify } from 'zapo-js/crypto';
|
|
7
|
+
export { aesCbcDecrypt, aesCbcEncrypt, aesGcmDecrypt, aesGcmEncrypt, Ed25519, hkdf, hkdfSplit, hmacSha256Sign, hmacSha512Sign, prependVersion, randomBytesAsync, randomIntAsync, readVersionedContent, sha256, sha512, toRawPubKey, toSerializedPubKey, X25519, xeddsaSign, xeddsaVerify } from 'zapo-js/crypto';
|
|
8
8
|
export type { SignalKeyPair } from 'zapo-js/crypto';
|
|
9
9
|
export { WaMediaCrypto } from 'zapo-js/media';
|
package/dist/transport/crypto.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* interpretation, so reusing them does not create a test-vs-impl tautology.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.WaMediaCrypto = exports.xeddsaVerify = exports.xeddsaSign = exports.X25519 = exports.toSerializedPubKey = exports.toRawPubKey = exports.sha512 = exports.sha256 = exports.readVersionedContent = exports.randomBytesAsync = exports.prependVersion = exports.hmacSha512Sign = exports.hmacSha256Sign = exports.hkdfSplit = exports.hkdf = exports.Ed25519 = exports.aesGcmEncrypt = exports.aesGcmDecrypt = exports.aesCbcEncrypt = exports.aesCbcDecrypt = void 0;
|
|
9
|
+
exports.WaMediaCrypto = exports.xeddsaVerify = exports.xeddsaSign = exports.X25519 = exports.toSerializedPubKey = exports.toRawPubKey = exports.sha512 = exports.sha256 = exports.readVersionedContent = exports.randomIntAsync = exports.randomBytesAsync = exports.prependVersion = exports.hmacSha512Sign = exports.hmacSha256Sign = exports.hkdfSplit = exports.hkdf = exports.Ed25519 = exports.aesGcmEncrypt = exports.aesGcmDecrypt = exports.aesCbcEncrypt = exports.aesCbcDecrypt = void 0;
|
|
10
10
|
var crypto_1 = require("zapo-js/crypto");
|
|
11
11
|
Object.defineProperty(exports, "aesCbcDecrypt", { enumerable: true, get: function () { return crypto_1.aesCbcDecrypt; } });
|
|
12
12
|
Object.defineProperty(exports, "aesCbcEncrypt", { enumerable: true, get: function () { return crypto_1.aesCbcEncrypt; } });
|
|
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "hmacSha256Sign", { enumerable: true, get: functi
|
|
|
19
19
|
Object.defineProperty(exports, "hmacSha512Sign", { enumerable: true, get: function () { return crypto_1.hmacSha512Sign; } });
|
|
20
20
|
Object.defineProperty(exports, "prependVersion", { enumerable: true, get: function () { return crypto_1.prependVersion; } });
|
|
21
21
|
Object.defineProperty(exports, "randomBytesAsync", { enumerable: true, get: function () { return crypto_1.randomBytesAsync; } });
|
|
22
|
+
Object.defineProperty(exports, "randomIntAsync", { enumerable: true, get: function () { return crypto_1.randomIntAsync; } });
|
|
22
23
|
Object.defineProperty(exports, "readVersionedContent", { enumerable: true, get: function () { return crypto_1.readVersionedContent; } });
|
|
23
24
|
Object.defineProperty(exports, "sha256", { enumerable: true, get: function () { return crypto_1.sha256; } });
|
|
24
25
|
Object.defineProperty(exports, "sha512", { enumerable: true, get: function () { return crypto_1.sha512; } });
|
package/dist/transport/util.d.ts
CHANGED
|
@@ -4,5 +4,6 @@
|
|
|
4
4
|
* Re-exports bit-exact byte helpers from zapo-js so Layer 2/3 of the fake
|
|
5
5
|
* server can use them without importing zapo-js directly.
|
|
6
6
|
*/
|
|
7
|
-
export { bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url } from 'zapo-js/util';
|
|
7
|
+
export { bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url, toError } from 'zapo-js/util';
|
|
8
8
|
export declare const TEXT_ENCODER: import("util").TextEncoder;
|
|
9
|
+
export declare const TEXT_DECODER: import("util").TextDecoder;
|
package/dist/transport/util.js
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* server can use them without importing zapo-js directly.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.TEXT_ENCODER = exports.decodeBase64Url = exports.bytesToHex = exports.bytesToBase64UrlSafe = exports.bytesToBase64 = void 0;
|
|
9
|
+
exports.TEXT_DECODER = exports.TEXT_ENCODER = exports.toError = exports.decodeBase64Url = exports.bytesToHex = exports.bytesToBase64UrlSafe = exports.bytesToBase64 = void 0;
|
|
10
10
|
var util_1 = require("zapo-js/util");
|
|
11
11
|
Object.defineProperty(exports, "bytesToBase64", { enumerable: true, get: function () { return util_1.bytesToBase64; } });
|
|
12
12
|
Object.defineProperty(exports, "bytesToBase64UrlSafe", { enumerable: true, get: function () { return util_1.bytesToBase64UrlSafe; } });
|
|
13
13
|
Object.defineProperty(exports, "bytesToHex", { enumerable: true, get: function () { return util_1.bytesToHex; } });
|
|
14
14
|
Object.defineProperty(exports, "decodeBase64Url", { enumerable: true, get: function () { return util_1.decodeBase64Url; } });
|
|
15
|
+
Object.defineProperty(exports, "toError", { enumerable: true, get: function () { return util_1.toError; } });
|
|
15
16
|
exports.TEXT_ENCODER = new TextEncoder();
|
|
17
|
+
exports.TEXT_DECODER = new TextDecoder();
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zapo-js/fake-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Fake WhatsApp Web server used to test zapo-js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vinikjkkj <contact@vinicius.email> (https://github.com/vinikjkkj)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/vinikjkkj/zapo.git",
|
|
10
|
+
"directory": "packages/fake-server"
|
|
11
|
+
},
|
|
7
12
|
"main": "dist/index.js",
|
|
8
13
|
"module": "dist/esm/index.js",
|
|
9
14
|
"types": "dist/index.d.ts",
|