zapo-js 1.9.0 → 1.11.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/WaAuthClient.d.ts +5 -0
- package/dist/auth/WaAuthClient.js +5 -0
- package/dist/auth/pairing/WaPairingFlow.d.ts +11 -0
- package/dist/auth/pairing/WaPairingFlow.js +97 -40
- package/dist/client/WaClient.d.ts +4 -0
- package/dist/client/WaClient.js +7 -1
- package/dist/client/WaClientFactory.js +6 -5
- package/dist/client/connection/WaConnectionManager.d.ts +14 -2
- package/dist/client/connection/WaConnectionManager.js +55 -22
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +27 -8
- package/dist/client/coordinators/WaMessageCoordinator.js +4 -8
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +13 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +19 -6
- package/dist/client/coordinators/WaProfileCoordinator.d.ts +5 -1
- package/dist/client/coordinators/WaStreamControlCoordinator.d.ts +2 -0
- package/dist/client/coordinators/WaStreamControlCoordinator.js +6 -1
- package/dist/client/messaging/ignore-key.d.ts +2 -1
- package/dist/client/messaging/ignore-key.js +24 -3
- package/dist/client/messaging/messages.js +24 -8
- package/dist/client/persistence/history-sync.d.ts +5 -1
- package/dist/client/persistence/history-sync.js +172 -38
- package/dist/client/types.d.ts +65 -10
- package/dist/esm/auth/WaAuthClient.js +5 -0
- package/dist/esm/auth/pairing/WaPairingFlow.js +97 -40
- package/dist/esm/client/WaClient.js +7 -1
- package/dist/esm/client/WaClientFactory.js +6 -5
- package/dist/esm/client/connection/WaConnectionManager.js +55 -22
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +4 -8
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +18 -6
- package/dist/esm/client/coordinators/WaStreamControlCoordinator.js +6 -1
- package/dist/esm/client/messaging/ignore-key.js +25 -4
- package/dist/esm/client/messaging/messages.js +25 -9
- package/dist/esm/client/persistence/history-sync.js +172 -38
- package/dist/esm/infra/log/types.js +2 -1
- package/dist/esm/message/context-info.js +2 -0
- package/dist/esm/message/encode/content.js +16 -0
- package/dist/esm/message/primitives/incoming.js +12 -5
- package/dist/esm/protocol/defaults.js +6 -0
- package/dist/esm/protocol/message.js +2 -1
- package/dist/esm/signal/api/SignalDeviceSyncApi.js +64 -32
- 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/WaComms.js +94 -16
- package/dist/esm/transport/WaWebSocket.js +79 -32
- package/dist/esm/transport/node/builders/message.js +1 -1
- package/dist/esm/transport/noise/WaNoiseSession.js +6 -5
- package/dist/esm/util/collections.js +94 -0
- package/dist/esm/util/index.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/infra/log/types.d.ts +1 -0
- package/dist/infra/log/types.js +2 -1
- package/dist/message/context-info.js +2 -0
- package/dist/message/encode/content.d.ts +3 -2
- package/dist/message/encode/content.js +17 -0
- package/dist/message/primitives/incoming.js +12 -5
- package/dist/message/primitives/media-retry.d.ts +11 -0
- package/dist/message/types.d.ts +21 -12
- package/dist/protocol/defaults.d.ts +6 -0
- package/dist/protocol/defaults.js +6 -0
- package/dist/protocol/message.d.ts +1 -0
- package/dist/protocol/message.js +2 -1
- package/dist/signal/api/SignalDeviceSyncApi.d.ts +27 -5
- package/dist/signal/api/SignalDeviceSyncApi.js +64 -32
- 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/WaComms.d.ts +9 -1
- package/dist/transport/WaComms.js +94 -16
- package/dist/transport/WaWebSocket.d.ts +8 -2
- package/dist/transport/WaWebSocket.js +79 -32
- package/dist/transport/node/builders/message.js +1 -1
- package/dist/transport/noise/WaNoiseSession.js +6 -5
- 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
package/README.md
CHANGED
|
@@ -127,19 +127,20 @@ The core lives at the repo root (`zapo-js`). Optional packages live in
|
|
|
127
127
|
[`packages/`](packages/) and ship under the `@zapo-js/*` scope. Install
|
|
128
128
|
only what you need.
|
|
129
129
|
|
|
130
|
-
| Package | Peer dependency
|
|
131
|
-
| ---------------------------------------------------- |
|
|
132
|
-
| [`@zapo-js/store-sqlite`](packages/store-sqlite) | `better-sqlite3`
|
|
133
|
-
| [`@zapo-js/store-redis`](packages/store-redis) | `ioredis`
|
|
134
|
-
| [`@zapo-js/store-mongo`](packages/store-mongo) | `mongodb`
|
|
135
|
-
| [`@zapo-js/store-mysql`](packages/store-mysql) | `mysql2`
|
|
136
|
-
| [`@zapo-js/store-postgres`](packages/store-postgres) | `pg`
|
|
137
|
-
| [`@zapo-js/media-utils`](packages/media-utils) | `sharp` + `file-type` + `ffmpeg`
|
|
138
|
-
| [`@zapo-js/native`](packages/native) | (none)
|
|
139
|
-
| [`@zapo-js/voip`](packages/voip) | `@roamhq/wrtc` + `libmlow-wasm`
|
|
140
|
-
| [`@zapo-js/
|
|
141
|
-
| [`@zapo-js/
|
|
142
|
-
| [`@zapo-js/
|
|
130
|
+
| Package | Peer dependency | Purpose |
|
|
131
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
132
|
+
| [`@zapo-js/store-sqlite`](packages/store-sqlite) | `better-sqlite3` | SQLite persistent store (single-process bots, dev sessions, small-to-medium prod). |
|
|
133
|
+
| [`@zapo-js/store-redis`](packages/store-redis) | `ioredis` | Redis-backed store with native TTL eviction. |
|
|
134
|
+
| [`@zapo-js/store-mongo`](packages/store-mongo) | `mongodb` | MongoDB-backed store with TTL-index eviction. |
|
|
135
|
+
| [`@zapo-js/store-mysql`](packages/store-mysql) | `mysql2` | MySQL / MariaDB-backed store with background cleanup poller. |
|
|
136
|
+
| [`@zapo-js/store-postgres`](packages/store-postgres) | `pg` | PostgreSQL-backed store with background cleanup poller. |
|
|
137
|
+
| [`@zapo-js/media-utils`](packages/media-utils) | `sharp` + `file-type` + `ffmpeg` | `WaMediaProcessor`: thumbnails, waveforms, voice-note normalization. |
|
|
138
|
+
| [`@zapo-js/native`](packages/native) | (none) | Rust crypto accelerator for the messaging hot path (X25519 ECDH, XEdDSA sign / verify). Auto-detected once installed; ships as a WASM build. |
|
|
139
|
+
| [`@zapo-js/voip`](packages/voip) | `@roamhq/wrtc` + `libmlow-wasm-fork` | `client.voip` plugin for calls: MLow/WASM codec, SRTP, STUN, WebRTC/SCTP relay transport. |
|
|
140
|
+
| [`@zapo-js/voip-media`](packages/voip-media) | `libmlow-wasm-fork` (optional, to carry media) + `@roamhq/wrtc` (optional, Node host) | Call media plane with no Node or `zapo-js` dependency, so it runs in the browser: relay transport, STUN, SRTP, RTP/RTCP, MLow codec. |
|
|
141
|
+
| [`@zapo-js/wam`](packages/wam) | (none) | `client.wam` plugin emitting the client-side WAM telemetry batches WA Web sends, for wire parity and anti-fingerprinting. |
|
|
142
|
+
| [`@zapo-js/fake-server`](packages/fake-server) | (none) | In-process fake WhatsApp Web server for end-to-end testing. |
|
|
143
|
+
| [`@zapo-js/mcp-server`](packages/mcp-server) | `@modelcontextprotocol/sdk` | **Dev-only.** MCP server exposing multi-session `WaClient`s as dynamic tools for an LLM agent (Claude Code / Cursor / etc.). Not for production. |
|
|
143
144
|
|
|
144
145
|
Each package's README has the install + config + integration notes.
|
|
145
146
|
|
|
@@ -146,6 +146,11 @@ export declare class WaAuthClient {
|
|
|
146
146
|
* call this. The user then enters the returned code in WhatsApp on their
|
|
147
147
|
* phone under *Linked Devices → Link with phone number instead*.
|
|
148
148
|
*
|
|
149
|
+
* The server binds the code to the connection that requested it. If the
|
|
150
|
+
* socket drops and reconnects first, the client registers the same code on
|
|
151
|
+
* the new connection; if that fails, `auth_pairing_required` fires with
|
|
152
|
+
* `forceManual: true` and a new code must be requested.
|
|
153
|
+
*
|
|
149
154
|
* @example
|
|
150
155
|
* ```ts
|
|
151
156
|
* // Start the handshake; do NOT await – connect() resolves only after pairing
|
|
@@ -307,6 +307,11 @@ class WaAuthClient {
|
|
|
307
307
|
* call this. The user then enters the returned code in WhatsApp on their
|
|
308
308
|
* phone under *Linked Devices → Link with phone number instead*.
|
|
309
309
|
*
|
|
310
|
+
* The server binds the code to the connection that requested it. If the
|
|
311
|
+
* socket drops and reconnects first, the client registers the same code on
|
|
312
|
+
* the new connection; if that fails, `auth_pairing_required` fires with
|
|
313
|
+
* `forceManual: true` and a new code must be requested.
|
|
314
|
+
*
|
|
310
315
|
* @example
|
|
311
316
|
* ```ts
|
|
312
317
|
* // Start the handshake; do NOT await – connect() resolves only after pairing
|
|
@@ -31,6 +31,7 @@ interface WaPairingFlowOptions {
|
|
|
31
31
|
export declare class WaPairingFlow {
|
|
32
32
|
private readonly opts;
|
|
33
33
|
private pairingSession;
|
|
34
|
+
private rearmGeneration;
|
|
34
35
|
private pairSuccessInFlight;
|
|
35
36
|
constructor(options: WaPairingFlowOptions);
|
|
36
37
|
hasPairingSession(): boolean;
|
|
@@ -48,6 +49,16 @@ export declare class WaPairingFlow {
|
|
|
48
49
|
/** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
|
|
49
50
|
private sendPairSuccessValidationError;
|
|
50
51
|
private handlePrimaryHello;
|
|
52
|
+
private sendCompanionHello;
|
|
53
|
+
/**
|
|
54
|
+
* The server delivers `primary_hello` only to the connection that sent the
|
|
55
|
+
* `companion_hello`, so a code requested on a dropped connection is dead.
|
|
56
|
+
* Registers the same code again, without a push, like WA Web's code refresh.
|
|
57
|
+
* The ADV secret is not rotated: the QR just emitted for this connection carries it.
|
|
58
|
+
* Only the latest pair-device may settle the session, since an older attempt can
|
|
59
|
+
* outlive its dropped connection until the IQ times out.
|
|
60
|
+
*/
|
|
61
|
+
private rearmPairingCode;
|
|
51
62
|
private rotateAdvSecret;
|
|
52
63
|
private requireCredentials;
|
|
53
64
|
}
|
|
@@ -19,6 +19,7 @@ const PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES = 1;
|
|
|
19
19
|
const PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES = 500;
|
|
20
20
|
class WaPairingFlow {
|
|
21
21
|
constructor(options) {
|
|
22
|
+
this.rearmGeneration = 0;
|
|
22
23
|
this.pairSuccessInFlight = false;
|
|
23
24
|
this.opts = options;
|
|
24
25
|
this.pairingSession = null;
|
|
@@ -38,49 +39,17 @@ class WaPairingFlow {
|
|
|
38
39
|
const normalizedCustomCode = customCode !== undefined ? (0, pairing_code_crypto_1.normalizeCustomPairingCode)(customCode) : undefined;
|
|
39
40
|
const credentials = this.requireCredentials();
|
|
40
41
|
const phoneJid = (0, jid_1.parsePhoneJid)(phoneNumber);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
(0, pairing_code_crypto_1.createCompanionHello)({ customCode: normalizedCustomCode }),
|
|
42
|
+
const [session] = await Promise.all([
|
|
43
|
+
this.sendCompanionHello(phoneJid, shouldShowPushNotification, normalizedCustomCode),
|
|
44
44
|
this.rotateAdvSecret(credentials)
|
|
45
45
|
]);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
phoneJid,
|
|
49
|
-
shouldShowPushNotification,
|
|
50
|
-
wrappedCompanionEphemeralPub: companionHello.wrappedCompanionEphemeralPub,
|
|
51
|
-
companionServerAuthKeyPub: refreshedCredentials.noiseKeyPair.pubKey,
|
|
52
|
-
companionPlatformId: this.opts.device.platform,
|
|
53
|
-
companionPlatformDisplay: `${browserDisplayName} (${this.opts.device.osDisplayName})`
|
|
54
|
-
}), constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS);
|
|
55
|
-
this.opts.logger.debug('pairing code request response received', {
|
|
56
|
-
responseTag: response.tag,
|
|
57
|
-
responseType: response.attrs.type
|
|
58
|
-
});
|
|
59
|
-
(0, query_1.assertIqResult)(response, 'companion hello');
|
|
60
|
-
const linkCodeNode = (0, helpers_1.findNodeChild)(response, constants_1.WA_NODE_TAGS.LINK_CODE_COMPANION_REG);
|
|
61
|
-
if (!linkCodeNode) {
|
|
62
|
-
throw new Error('companion hello response missing link_code_companion_reg');
|
|
63
|
-
}
|
|
64
|
-
const refNode = (0, helpers_1.findNodeChild)(linkCodeNode, constants_1.WA_NODE_TAGS.LINK_CODE_PAIRING_REF);
|
|
65
|
-
if (!refNode) {
|
|
66
|
-
throw new Error('companion hello response missing link_code_pairing_ref');
|
|
67
|
-
}
|
|
68
|
-
const ref = (0, helpers_1.decodeNodeContentUtf8OrBytes)(refNode.content, 'link_code_pairing_ref');
|
|
69
|
-
this.pairingSession = {
|
|
70
|
-
pairingCode: companionHello.pairingCode,
|
|
71
|
-
phoneJid,
|
|
72
|
-
ref,
|
|
73
|
-
createdAtSeconds,
|
|
74
|
-
companionEphemeralKeyPair: companionHello.companionEphemeralKeyPair,
|
|
75
|
-
attempts: 0,
|
|
76
|
-
finished: false
|
|
77
|
-
};
|
|
78
|
-
this.opts.callbacks.emitPairingCode(companionHello.pairingCode);
|
|
46
|
+
this.pairingSession = session;
|
|
47
|
+
this.opts.callbacks.emitPairingCode(session.pairingCode);
|
|
79
48
|
this.opts.logger.debug('pairing code emitted', {
|
|
80
49
|
phoneJid,
|
|
81
|
-
createdAtSeconds:
|
|
50
|
+
createdAtSeconds: session.createdAtSeconds
|
|
82
51
|
});
|
|
83
|
-
return
|
|
52
|
+
return session.pairingCode;
|
|
84
53
|
}
|
|
85
54
|
async fetchPairingCountryCodeIso() {
|
|
86
55
|
this.opts.logger.trace('fetching pairing country code ISO');
|
|
@@ -170,6 +139,15 @@ class WaPairingFlow {
|
|
|
170
139
|
await this.opts.socket.sendNode((0, global_1.buildIqResultNode)(iqNode));
|
|
171
140
|
this.opts.qrFlow.setRefs(refs);
|
|
172
141
|
this.opts.logger.debug('pair-device refs updated', { refsCount: refs.length });
|
|
142
|
+
// The server sends pair-device once per connection: a pending code belongs to an older one.
|
|
143
|
+
if (this.pairingSession) {
|
|
144
|
+
const generation = ++this.rearmGeneration;
|
|
145
|
+
void this.rearmPairingCode(this.pairingSession, generation).catch((error) => {
|
|
146
|
+
this.opts.logger.warn('pairing code re-registration failed unexpectedly', {
|
|
147
|
+
message: (0, primitives_1.toError)(error).message
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
173
151
|
}
|
|
174
152
|
async handlePairSuccess(iqNode, pairSuccessNode) {
|
|
175
153
|
if (this.pairSuccessInFlight) {
|
|
@@ -231,7 +209,13 @@ class WaPairingFlow {
|
|
|
231
209
|
await this.sendPairSuccessValidationError(iqNode);
|
|
232
210
|
throw new Error('pair-success account signature validation failed');
|
|
233
211
|
}
|
|
234
|
-
const { signedIdentity, keyIndex, responseIdentityBytes } = built;
|
|
212
|
+
const { signedIdentity, keyIndex, responseIdentityBytes, advDeviceIdentity } = built;
|
|
213
|
+
this.opts.logger.debug('pair-success adv identity', {
|
|
214
|
+
envelopeAccountType: wrappedIdentity.accountType ?? null,
|
|
215
|
+
accountType: advDeviceIdentity.accountType ?? null,
|
|
216
|
+
deviceType: advDeviceIdentity.deviceType ?? null,
|
|
217
|
+
keyIndex: advDeviceIdentity.keyIndex ?? null
|
|
218
|
+
});
|
|
235
219
|
const nextCredentials = {
|
|
236
220
|
...credentials,
|
|
237
221
|
signedIdentity,
|
|
@@ -301,7 +285,8 @@ class WaPairingFlow {
|
|
|
301
285
|
return {
|
|
302
286
|
signedIdentity,
|
|
303
287
|
keyIndex: advDeviceIdentity.keyIndex ?? 0,
|
|
304
|
-
responseIdentityBytes
|
|
288
|
+
responseIdentityBytes,
|
|
289
|
+
advDeviceIdentity
|
|
305
290
|
};
|
|
306
291
|
}
|
|
307
292
|
/** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
|
|
@@ -377,6 +362,78 @@ class WaPairingFlow {
|
|
|
377
362
|
pairingSession.finished = true;
|
|
378
363
|
this.opts.logger.debug('primary_hello completed with companion_finish success');
|
|
379
364
|
}
|
|
365
|
+
async sendCompanionHello(phoneJid, shouldShowPushNotification, customCode) {
|
|
366
|
+
const credentials = this.requireCredentials();
|
|
367
|
+
const createdAtSeconds = Math.floor(Date.now() / 1000);
|
|
368
|
+
const companionHello = await (0, pairing_code_crypto_1.createCompanionHello)({ customCode });
|
|
369
|
+
const browserDisplayName = (0, browser_1.getWaBrowserDisplayName)(this.opts.device.browser);
|
|
370
|
+
const response = await this.opts.socket.query((0, pairing_1.buildCompanionHelloRequestNode)({
|
|
371
|
+
phoneJid,
|
|
372
|
+
shouldShowPushNotification,
|
|
373
|
+
wrappedCompanionEphemeralPub: companionHello.wrappedCompanionEphemeralPub,
|
|
374
|
+
companionServerAuthKeyPub: credentials.noiseKeyPair.pubKey,
|
|
375
|
+
companionPlatformId: this.opts.device.platform,
|
|
376
|
+
companionPlatformDisplay: `${browserDisplayName} (${this.opts.device.osDisplayName})`
|
|
377
|
+
}), constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS);
|
|
378
|
+
this.opts.logger.debug('pairing code request response received', {
|
|
379
|
+
responseTag: response.tag,
|
|
380
|
+
responseType: response.attrs.type
|
|
381
|
+
});
|
|
382
|
+
(0, query_1.assertIqResult)(response, 'companion hello');
|
|
383
|
+
const linkCodeNode = (0, helpers_1.findNodeChild)(response, constants_1.WA_NODE_TAGS.LINK_CODE_COMPANION_REG);
|
|
384
|
+
if (!linkCodeNode) {
|
|
385
|
+
throw new Error('companion hello response missing link_code_companion_reg');
|
|
386
|
+
}
|
|
387
|
+
const refNode = (0, helpers_1.findNodeChild)(linkCodeNode, constants_1.WA_NODE_TAGS.LINK_CODE_PAIRING_REF);
|
|
388
|
+
if (!refNode) {
|
|
389
|
+
throw new Error('companion hello response missing link_code_pairing_ref');
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
pairingCode: companionHello.pairingCode,
|
|
393
|
+
phoneJid,
|
|
394
|
+
ref: (0, helpers_1.decodeNodeContentUtf8OrBytes)(refNode.content, 'link_code_pairing_ref'),
|
|
395
|
+
createdAtSeconds,
|
|
396
|
+
companionEphemeralKeyPair: companionHello.companionEphemeralKeyPair,
|
|
397
|
+
attempts: 0,
|
|
398
|
+
finished: false
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The server delivers `primary_hello` only to the connection that sent the
|
|
403
|
+
* `companion_hello`, so a code requested on a dropped connection is dead.
|
|
404
|
+
* Registers the same code again, without a push, like WA Web's code refresh.
|
|
405
|
+
* The ADV secret is not rotated: the QR just emitted for this connection carries it.
|
|
406
|
+
* Only the latest pair-device may settle the session, since an older attempt can
|
|
407
|
+
* outlive its dropped connection until the IQ times out.
|
|
408
|
+
*/
|
|
409
|
+
async rearmPairingCode(previous, generation) {
|
|
410
|
+
const isCurrent = () => this.pairingSession === previous && this.rearmGeneration === generation;
|
|
411
|
+
try {
|
|
412
|
+
const session = await this.sendCompanionHello(previous.phoneJid, false, previous.pairingCode);
|
|
413
|
+
if (!isCurrent()) {
|
|
414
|
+
this.opts.logger.debug('pairing code re-registration superseded');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.pairingSession = session;
|
|
418
|
+
this.opts.logger.info('pairing code re-registered on the new connection', {
|
|
419
|
+
phoneJid: session.phoneJid
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
const message = (0, primitives_1.toError)(error).message;
|
|
424
|
+
if (!isCurrent()) {
|
|
425
|
+
this.opts.logger.debug('superseded pairing code re-registration failed', {
|
|
426
|
+
message
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
this.pairingSession = null;
|
|
431
|
+
this.opts.logger.warn('failed to re-register pairing code on the new connection', {
|
|
432
|
+
message
|
|
433
|
+
});
|
|
434
|
+
this.opts.callbacks.emitPairingRefresh(true);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
380
437
|
async rotateAdvSecret(credentials) {
|
|
381
438
|
const nextCredentials = {
|
|
382
439
|
...credentials,
|
|
@@ -132,6 +132,10 @@ declare class WaClientImpl extends EventEmitter {
|
|
|
132
132
|
* arrays are empty. The predicate receives a parsed
|
|
133
133
|
* {@link WaIgnoreKeyContext} and returns `true` to drop the stanza.
|
|
134
134
|
*
|
|
135
|
+
* `category='peer'` messages from our own account (app-state key share,
|
|
136
|
+
* history sync, PDO responses) are never dropped: they are protocol
|
|
137
|
+
* traffic between our own devices, not chat messages.
|
|
138
|
+
*
|
|
135
139
|
* @example
|
|
136
140
|
* ```ts
|
|
137
141
|
* client.ignoreKey({ remoteJid: spammerJid })
|
package/dist/client/WaClient.js
CHANGED
|
@@ -275,6 +275,8 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
275
275
|
mediaTransfer: this.mediaTransfer,
|
|
276
276
|
chatMetadataStore: this.stores.chatMetadata,
|
|
277
277
|
writeBehind: this.writeBehind,
|
|
278
|
+
chunkId: event.key.id,
|
|
279
|
+
sink: this.options.history?.sink,
|
|
278
280
|
emitEvent: this.emit.bind(this),
|
|
279
281
|
onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
|
|
280
282
|
onNctSalt: (salt) => this.deps.trustedContactToken.hydrateNctSaltFromHistorySync(salt),
|
|
@@ -458,6 +460,10 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
458
460
|
* arrays are empty. The predicate receives a parsed
|
|
459
461
|
* {@link WaIgnoreKeyContext} and returns `true` to drop the stanza.
|
|
460
462
|
*
|
|
463
|
+
* `category='peer'` messages from our own account (app-state key share,
|
|
464
|
+
* history sync, PDO responses) are never dropped: they are protocol
|
|
465
|
+
* traffic between our own devices, not chat messages.
|
|
466
|
+
*
|
|
461
467
|
* @example
|
|
462
468
|
* ```ts
|
|
463
469
|
* client.ignoreKey({ remoteJid: spammerJid })
|
|
@@ -469,7 +475,7 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
469
475
|
if (typeof input !== 'function') {
|
|
470
476
|
(0, ignore_key_1.validateIgnoreKey)(input);
|
|
471
477
|
}
|
|
472
|
-
const filter = (0, ignore_key_1.createIgnoreKeyFilter)(input, () => this.deps.authClient.getCurrentCredentials()
|
|
478
|
+
const filter = (0, ignore_key_1.createIgnoreKeyFilter)(input, () => this.deps.authClient.getCurrentCredentials());
|
|
473
479
|
return this.deps.incomingNode.registerIncomingStanzaFilter(filter);
|
|
474
480
|
}
|
|
475
481
|
/** Auth client: pairing, credentials, registration state. */
|
|
@@ -211,7 +211,7 @@ function buildWaClientDependencies(input) {
|
|
|
211
211
|
let connectionManager = null;
|
|
212
212
|
let passiveTasks = null;
|
|
213
213
|
let mediaConnCacheFallback = null;
|
|
214
|
-
let
|
|
214
|
+
let awaitServerRestartAfterPairing = () => undefined;
|
|
215
215
|
const serverClock = (0, clock_1.createServerClock)(() => connectionManager?.getClockSkewMs() ?? null);
|
|
216
216
|
const nodeTransport = new WaNodeTransport_1.WaNodeTransport(logger);
|
|
217
217
|
const nodeOrchestrator = new WaNodeOrchestrator_1.WaNodeOrchestrator({
|
|
@@ -338,7 +338,7 @@ function buildWaClientDependencies(input) {
|
|
|
338
338
|
deviceOsDisplayName: options.deviceOsDisplayName,
|
|
339
339
|
deviceOsVersion: options.deviceOsVersion,
|
|
340
340
|
devicePlatform: options.devicePlatform,
|
|
341
|
-
requireFullSync: options.requireFullSync,
|
|
341
|
+
requireFullSync: options.history?.requireFullSync ?? options.requireFullSync,
|
|
342
342
|
version: options.version,
|
|
343
343
|
dangerous: options.dangerous,
|
|
344
344
|
mobileTransport: options.mobileTransport
|
|
@@ -358,7 +358,7 @@ function buildWaClientDependencies(input) {
|
|
|
358
358
|
onPairingRefresh: (forceManual) => runtime.emitEvent('auth_pairing_required', { forceManual }),
|
|
359
359
|
onPaired: (credentials) => {
|
|
360
360
|
runtime.emitEvent('auth_paired', { credentials });
|
|
361
|
-
|
|
361
|
+
awaitServerRestartAfterPairing();
|
|
362
362
|
},
|
|
363
363
|
onError: (error) => runtime.handleError(error),
|
|
364
364
|
onPasskeyRequired: (hasSigner) => runtime.emitEvent('auth_passkey_required', { hasSigner }),
|
|
@@ -671,7 +671,7 @@ function buildWaClientDependencies(input) {
|
|
|
671
671
|
if (mediaConnCacheFallback !== null) {
|
|
672
672
|
connectionManager.setMediaConnCache(mediaConnCacheFallback);
|
|
673
673
|
}
|
|
674
|
-
|
|
674
|
+
awaitServerRestartAfterPairing = () => connectionManager?.awaitServerRestartAfterPairing();
|
|
675
675
|
const disconnectWithClientSideEffects = async (reason, isLogout, code) => {
|
|
676
676
|
abPropsCoordinator.reset();
|
|
677
677
|
offlineResume.reset();
|
|
@@ -712,7 +712,8 @@ function buildWaClientDependencies(input) {
|
|
|
712
712
|
},
|
|
713
713
|
disconnect: disconnectWithClientSideEffects,
|
|
714
714
|
clearStoredCredentials: clearStoredCredentialsWithClientSideEffects,
|
|
715
|
-
connect: connectWithClientSideEffects
|
|
715
|
+
connect: connectWithClientSideEffects,
|
|
716
|
+
handlePairingRestart: () => connectionManager?.handleServerRestartRequest() ?? false
|
|
716
717
|
});
|
|
717
718
|
const incomingMessageAckOptions = {
|
|
718
719
|
logger,
|
|
@@ -33,14 +33,24 @@ export declare class WaConnectionManager {
|
|
|
33
33
|
private mediaConnCache;
|
|
34
34
|
private connectPromise;
|
|
35
35
|
private pairingReconnectPromise;
|
|
36
|
-
private
|
|
36
|
+
private pairRestartTimer;
|
|
37
37
|
private frameHandler;
|
|
38
38
|
private pendingComms;
|
|
39
39
|
private lifecycleGeneration;
|
|
40
40
|
private lifecycleQueue;
|
|
41
41
|
constructor(options: WaConnectionManagerOptions);
|
|
42
42
|
connect(frameHandler: (frame: Uint8Array) => Promise<void>): Promise<void>;
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Starts the post-pairing restart window: the server ends the unregistered connection
|
|
45
|
+
* itself (`stream:error` 515, as WA Web waits for), so the socket stays open until that
|
|
46
|
+
* signal, a server close, or the fallback timer, whichever comes first.
|
|
47
|
+
*/
|
|
48
|
+
awaitServerRestartAfterPairing(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Claims a `stream:error` 515 for the post-pairing restart. Returns `true` while the
|
|
51
|
+
* window is open or its restart is running, so the generic force-login path must skip it.
|
|
52
|
+
*/
|
|
53
|
+
handleServerRestartRequest(): boolean;
|
|
44
54
|
disconnect(): Promise<void>;
|
|
45
55
|
clearStoredCredentials(): Promise<void>;
|
|
46
56
|
isConnected(): boolean;
|
|
@@ -51,6 +61,8 @@ export declare class WaConnectionManager {
|
|
|
51
61
|
updateClockSkewFromSuccess(serverUnixSeconds: number): void;
|
|
52
62
|
setClockSkewMs(clockSkewMs: number, source: string): void;
|
|
53
63
|
private connectInternal;
|
|
64
|
+
private startPairRestart;
|
|
65
|
+
private clearPairRestartTimer;
|
|
54
66
|
private reconnectAsRegisteredAfterPairing;
|
|
55
67
|
private reconnectAsRegisteredAfterPairingInternal;
|
|
56
68
|
private disconnectInternal;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WaConnectionManager = void 0;
|
|
4
|
+
const constants_1 = require("../../protocol/constants");
|
|
4
5
|
const WaComms_1 = require("../../transport/WaComms");
|
|
5
6
|
const primitives_1 = require("../../util/primitives");
|
|
6
7
|
class WaConnectionManager {
|
|
@@ -19,7 +20,7 @@ class WaConnectionManager {
|
|
|
19
20
|
this.mediaConnCache = null;
|
|
20
21
|
this.connectPromise = null;
|
|
21
22
|
this.pairingReconnectPromise = null;
|
|
22
|
-
this.
|
|
23
|
+
this.pairRestartTimer = null;
|
|
23
24
|
this.frameHandler = null;
|
|
24
25
|
this.pendingComms = null;
|
|
25
26
|
this.lifecycleGeneration = 0;
|
|
@@ -43,26 +44,37 @@ class WaConnectionManager {
|
|
|
43
44
|
this.connectPromise = promise;
|
|
44
45
|
return this.connectPromise;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Starts the post-pairing restart window: the server ends the unregistered connection
|
|
49
|
+
* itself (`stream:error` 515, as WA Web waits for), so the socket stays open until that
|
|
50
|
+
* signal, a server close, or the fallback timer, whichever comes first.
|
|
51
|
+
*/
|
|
52
|
+
awaitServerRestartAfterPairing() {
|
|
53
|
+
const comms = this.comms;
|
|
54
|
+
if (!comms) {
|
|
55
|
+
this.logger.trace('pairing restart wait skipped: no active comms');
|
|
56
|
+
return;
|
|
50
57
|
}
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
this.clearPairRestartTimer();
|
|
59
|
+
this.logger.debug('wa client waiting for server restart after pairing', {
|
|
60
|
+
fallbackMs: constants_1.WA_DEFAULTS.PAIRING_RESTART_WAIT_MS
|
|
61
|
+
});
|
|
62
|
+
this.pairRestartTimer = setTimeout(() => this.startPairRestart('fallback_timeout'), constants_1.WA_DEFAULTS.PAIRING_RESTART_WAIT_MS);
|
|
63
|
+
comms.holdForServerRestart(() => this.startPairRestart('socket_closed'));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Claims a `stream:error` 515 for the post-pairing restart. Returns `true` while the
|
|
67
|
+
* window is open or its restart is running, so the generic force-login path must skip it.
|
|
68
|
+
*/
|
|
69
|
+
handleServerRestartRequest() {
|
|
70
|
+
if (this.pairRestartTimer) {
|
|
71
|
+
this.startPairRestart('stream_error_515');
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return this.pairingReconnectPromise !== null;
|
|
64
75
|
}
|
|
65
76
|
async disconnect() {
|
|
77
|
+
this.clearPairRestartTimer();
|
|
66
78
|
const lifecycleGeneration = this.nextLifecycleGeneration();
|
|
67
79
|
this.invalidatePendingComms('failed to stop pending comms during disconnect');
|
|
68
80
|
await this.runLifecycleOperation(async () => {
|
|
@@ -140,6 +152,30 @@ class WaConnectionManager {
|
|
|
140
152
|
this.assertLifecycleCurrent(lifecycleGeneration, 'connect');
|
|
141
153
|
this.logger.info('wa client connected');
|
|
142
154
|
}
|
|
155
|
+
startPairRestart(trigger) {
|
|
156
|
+
if (!this.pairRestartTimer) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.clearPairRestartTimer();
|
|
160
|
+
this.logger.debug('wa client restarting after pairing', { trigger });
|
|
161
|
+
void this.reconnectAsRegisteredAfterPairing().catch((error) => {
|
|
162
|
+
if (this.isLifecycleSupersededError(error)) {
|
|
163
|
+
this.logger.trace('pairing reconnect canceled by newer lifecycle');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.logger.warn('failed to reconnect after pairing', {
|
|
167
|
+
meJid: this.authClient.getCurrentCredentials()?.meJid,
|
|
168
|
+
message: (0, primitives_1.toError)(error).message
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
clearPairRestartTimer() {
|
|
173
|
+
if (!this.pairRestartTimer) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
clearTimeout(this.pairRestartTimer);
|
|
177
|
+
this.pairRestartTimer = null;
|
|
178
|
+
}
|
|
143
179
|
async reconnectAsRegisteredAfterPairing() {
|
|
144
180
|
if (this.pairingReconnectPromise) {
|
|
145
181
|
this.logger.trace('pairing reconnect already in-flight');
|
|
@@ -188,10 +224,7 @@ class WaConnectionManager {
|
|
|
188
224
|
return;
|
|
189
225
|
}
|
|
190
226
|
this.logger.debug('wa client disconnect start');
|
|
191
|
-
|
|
192
|
-
clearTimeout(this.pairingReconnectTimeout);
|
|
193
|
-
this.pairingReconnectTimeout = null;
|
|
194
|
-
}
|
|
227
|
+
this.clearPairRestartTimer();
|
|
195
228
|
this.keepAlive.stop();
|
|
196
229
|
this.nodeOrchestrator.clearPending(new Error('client disconnected'));
|
|
197
230
|
this.clockSkewMs = null;
|
|
@@ -110,6 +110,13 @@ export interface WaRequestHistorySyncInput {
|
|
|
110
110
|
*/
|
|
111
111
|
readonly count?: number;
|
|
112
112
|
}
|
|
113
|
+
/** Options for {@link WaMessageCoordinator.syncSignalSession}. */
|
|
114
|
+
export interface WaSyncSignalSessionOptions {
|
|
115
|
+
/** Drops the session, keeping the identity, and fetches a fresh one. */
|
|
116
|
+
readonly force?: boolean;
|
|
117
|
+
/** The identity changed: drops the session and fetches with `reason="identity"`. */
|
|
118
|
+
readonly reasonIdentity?: boolean;
|
|
119
|
+
}
|
|
113
120
|
export interface WaReachoutTimelock {
|
|
114
121
|
readonly isActive: boolean;
|
|
115
122
|
readonly enforcementType: string | null;
|
|
@@ -239,11 +246,16 @@ export declare class WaMessageCoordinator {
|
|
|
239
246
|
*/
|
|
240
247
|
getNewChatMessageCapping(type?: WaMessageCappingType): Promise<WaMessageCappingInfo>;
|
|
241
248
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
249
|
+
* Syncs the Signal session of the device `jid` resolves to (a user JID is
|
|
250
|
+
* device 0). With no options it only ensures a session exists. `force`
|
|
251
|
+
* rebuilds it, keeping the identity. `reasonIdentity` rebuilds it as an
|
|
252
|
+
* identity change and reissues the trusted-contact token; it wins over
|
|
253
|
+
* `force`. Throws for group JIDs and when the key-bundle fetch fails, in
|
|
254
|
+
* which case the old session is already gone.
|
|
245
255
|
*/
|
|
246
|
-
syncSignalSession(jid: string,
|
|
256
|
+
syncSignalSession(jid: string, options?: WaSyncSignalSessionOptions): Promise<void>;
|
|
257
|
+
/** @deprecated Pass `{ reasonIdentity: true }` instead. */
|
|
258
|
+
syncSignalSession(jid: string, reasonIdentity: boolean): Promise<void>;
|
|
247
259
|
/**
|
|
248
260
|
* Sends a message (any {@link WaSendMessageContent} kind – text, media,
|
|
249
261
|
* poll, reaction, edit, revoke, etc.) to `to` and returns the publish
|
|
@@ -384,10 +396,17 @@ export declare class WaMessageCoordinator {
|
|
|
384
396
|
* the sealed request and resolves once the server answers with a
|
|
385
397
|
* `mediaretry` notification, usually within a couple of seconds.
|
|
386
398
|
*
|
|
387
|
-
* On `result: 'success'`
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
399
|
+
* On `result: 'success'` the media key, hashes, and length of the original
|
|
400
|
+
* message normally stay valid, so patch the fresh `directPath` into the
|
|
401
|
+
* message and download again. A second call for a message already in
|
|
402
|
+
* flight joins the first request instead of sending another receipt.
|
|
403
|
+
*
|
|
404
|
+
* A `success` is not a guarantee that the original key still opens the
|
|
405
|
+
* blob. Some primaries re-encrypt the file on every re-upload, and the
|
|
406
|
+
* re-served bytes then fail `fileEncSha256` and make `downloadBytes()`
|
|
407
|
+
* throw a MAC mismatch. Nothing in the round-trip carries key material for
|
|
408
|
+
* the new ciphertext, so the message cannot be recovered through this API
|
|
409
|
+
* and requesting another reupload will not help.
|
|
391
410
|
*
|
|
392
411
|
* The other three `result` values are answers too, not thrown errors:
|
|
393
412
|
* `not_found` means the sender no longer holds the file and nothing can
|
|
@@ -292,14 +292,10 @@ class WaMessageCoordinator {
|
|
|
292
292
|
});
|
|
293
293
|
return parseMessageCappingMexResponse(data);
|
|
294
294
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
*/
|
|
300
|
-
async syncSignalSession(jid, reasonIdentity = false) {
|
|
301
|
-
await this.messageDispatch.syncSignalSession(jid, reasonIdentity);
|
|
302
|
-
if (reasonIdentity) {
|
|
295
|
+
async syncSignalSession(jid, options = {}) {
|
|
296
|
+
const normalized = typeof options === 'boolean' ? { reasonIdentity: options } : options;
|
|
297
|
+
await this.messageDispatch.syncSignalSession(jid, normalized);
|
|
298
|
+
if (normalized.reasonIdentity) {
|
|
303
299
|
this.trustedContactToken.reissueOnIdentityChange(jid).catch((err) => this.logger.warn('tc token reissue on identity change failed', {
|
|
304
300
|
jid,
|
|
305
301
|
message: (0, primitives_1.toError)(err).message
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WaAppStateSyncKey } from '../../appstate/types';
|
|
2
2
|
import type { WaAuthCredentials } from '../../auth/types';
|
|
3
|
+
import type { WaSyncSignalSessionOptions } from '../coordinators/WaMessageCoordinator';
|
|
3
4
|
import type { DeviceFanoutResolver } from '../messaging/fanout';
|
|
4
5
|
import type { GroupMetadataCache } from '../messaging/group-metadata';
|
|
5
6
|
import type { AppStateSyncKeyProtocol } from '../messaging/key-protocol';
|
|
@@ -7,6 +8,7 @@ import type { WaGroupEvent, WaOutgoingMessageEvent, WaSendMessageOptions, WaSign
|
|
|
7
8
|
import type { Logger } from '../../infra/log/types';
|
|
8
9
|
import { type WaSendContextInfo } from '../../message/context-info';
|
|
9
10
|
import { type IcdcMeta } from '../../message/crypto/icdc';
|
|
11
|
+
import { type WaOutboundMessageAttrs } from '../../message/encode/content';
|
|
10
12
|
import type { WaEncryptedMessageInput, WaMessageBuildResult, WaMessagePublishOptions, WaMessagePublishResult, WaSendMessageContent, WaSendReceiptInput } from '../../message/types';
|
|
11
13
|
import type { WaMessageClient } from '../../message/WaMessageClient';
|
|
12
14
|
import { type Proto } from '../../proto';
|
|
@@ -96,6 +98,16 @@ export interface WaGroupHistoryAudience {
|
|
|
96
98
|
* while nobody received anything.
|
|
97
99
|
*/
|
|
98
100
|
export declare function restrictGroupHistoryTargets(message: Proto.IMessage, participantUserJids: readonly string[], senderJid: string, onDropped?: (droppedJids: readonly string[], totalExpected: number) => void): readonly string[];
|
|
101
|
+
/**
|
|
102
|
+
* Resolves the stanza `type` and `enc.mediatype` of an outbound message. A
|
|
103
|
+
* `<biz>` companion rides a type=text stanza with no mediatype, except on a
|
|
104
|
+
* group list: there the server nacks with 479 (SMAX_INVALID) unless
|
|
105
|
+
* type=media, mediatype=list and `<biz><list>` travel together.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveOutboundStanzaShape(attrs: Pick<WaOutboundMessageAttrs, 'buttonAddonKind' | 'typeAttr' | 'mediatype'>, isGroup: boolean): {
|
|
108
|
+
readonly type: string;
|
|
109
|
+
readonly mediatype: string | undefined;
|
|
110
|
+
};
|
|
99
111
|
export declare class WaMessageDispatchCoordinator {
|
|
100
112
|
private readonly deps;
|
|
101
113
|
private readonly mobileMessageIdFormat;
|
|
@@ -145,7 +157,7 @@ export declare class WaMessageDispatchCoordinator {
|
|
|
145
157
|
* LID destination.
|
|
146
158
|
*/
|
|
147
159
|
private resolvePeerRecipientPn;
|
|
148
|
-
syncSignalSession(jid: string, reasonIdentity?:
|
|
160
|
+
syncSignalSession(jid: string, { force, reasonIdentity }?: WaSyncSignalSessionOptions): Promise<void>;
|
|
149
161
|
sendReceipt(input: WaSendReceiptInput): Promise<void>;
|
|
150
162
|
publishProtocolMessageToDevice(deviceJid: string, protocolMessage: Proto.Message.IProtocolMessage, options?: {
|
|
151
163
|
readonly id?: string;
|