@oxidezap/baileyrs 0.2.11 → 0.2.12
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/lib/Bridge/index.d.ts +1 -1
- package/lib/Bridge/index.js +1 -1
- package/lib/Bridge/primitives.d.ts +18 -6
- package/lib/Bridge/primitives.js +26 -13
- package/lib/Bridge/schema.js +98 -16
- package/lib/Bridge/types.d.ts +11 -0
- package/lib/Socket/index.js +7 -1
- package/lib/Socket/terminal-close-reporter.d.ts +29 -8
- package/lib/Socket/terminal-close-reporter.js +31 -8
- package/lib/Socket/unsupported-config.d.ts +1 -1
- package/lib/Socket/unsupported-config.js +1 -0
- package/lib/Types/Socket.d.ts +8 -0
- package/lib/Utils/use-bridge-store.d.ts +74 -3
- package/lib/Utils/use-bridge-store.js +541 -201
- package/package.json +3 -2
package/lib/Bridge/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { adaptBridgeEvent, adaptBridgeMessageWire, KNOWN_BRIDGE_EVENT_TYPES } from './adapt.js';
|
|
10
10
|
export * from './types.js';
|
|
11
|
-
export { asBool, asBoolOr, asJidString, asNumber, asString, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
11
|
+
export { asBool, asBoolOr, asJidString, asNumber, asString, asUnixSeconds, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/Bridge/index.js
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { adaptBridgeEvent, adaptBridgeMessageWire, KNOWN_BRIDGE_EVENT_TYPES } from './adapt.js';
|
|
10
10
|
export * from './types.js';
|
|
11
|
-
export { asBool, asBoolOr, asJidString, asNumber, asString, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
11
|
+
export { asBool, asBoolOr, asJidString, asNumber, asString, asUnixSeconds, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
12
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -50,16 +50,28 @@ export declare const asJidAddressString: (x: unknown) => string | undefined;
|
|
|
50
50
|
* strings — the bridge serializes `DateTime<Utc>` as a string unless the field
|
|
51
51
|
* names one of chrono's `ts_*` modules, so being lenient about which of the two
|
|
52
52
|
* arrives insulates us from drift.
|
|
53
|
+
*
|
|
54
|
+
* Absence and unparseable input stay absent (`undefined`): a missing,
|
|
55
|
+
* malformed, or non-finite timestamp must never become the epoch, which
|
|
56
|
+
* would place the event at 1970 and corrupt timeline ordering. Callers
|
|
57
|
+
* apply the per-field policy explicitly — required timestamps (message,
|
|
58
|
+
* receipt, call, group action) drop the event, optional ones (presence
|
|
59
|
+
* `last_seen`, server-ack time, pin/mute time) omit the field.
|
|
53
60
|
*/
|
|
54
|
-
export declare const
|
|
61
|
+
export declare const asUnixSeconds: (raw: unknown) => number | undefined;
|
|
55
62
|
/**
|
|
56
|
-
*
|
|
63
|
+
* Validated coercion to unix seconds for external consumers that need a
|
|
64
|
+
* plain number (no `undefined` branch).
|
|
57
65
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
66
|
+
* Accepts the same inputs as `asUnixSeconds` — a finite number or an RFC
|
|
67
|
+
* 3339 string — and rejects everything else by throwing `RangeError`
|
|
68
|
+
* instead of fabricating the epoch: a missing, malformed, or non-finite
|
|
69
|
+
* timestamp must never become 1970-01-01 and corrupt timeline ordering.
|
|
70
|
+
* Internal adapters do not use this; they take `asUnixSeconds` with an
|
|
71
|
+
* explicit per-field policy (required timestamps drop the event, optional
|
|
72
|
+
* ones omit the field).
|
|
61
73
|
*/
|
|
62
|
-
export declare const
|
|
74
|
+
export declare const toUnixSeconds: (raw: unknown) => number;
|
|
63
75
|
/**
|
|
64
76
|
* A 64-bit proto field, however the bridge chose to carry it.
|
|
65
77
|
*
|
package/lib/Bridge/primitives.js
CHANGED
|
@@ -82,20 +82,13 @@ const parseRfc3339Seconds = (raw) => {
|
|
|
82
82
|
* strings — the bridge serializes `DateTime<Utc>` as a string unless the field
|
|
83
83
|
* names one of chrono's `ts_*` modules, so being lenient about which of the two
|
|
84
84
|
* arrives insulates us from drift.
|
|
85
|
-
*/
|
|
86
|
-
export const toUnixSeconds = (raw) => {
|
|
87
|
-
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
88
|
-
return raw;
|
|
89
|
-
if (typeof raw === 'string')
|
|
90
|
-
return parseRfc3339Seconds(raw) ?? 0;
|
|
91
|
-
return 0;
|
|
92
|
-
};
|
|
93
|
-
/**
|
|
94
|
-
* Same as `toUnixSeconds`, but absence stays absent.
|
|
95
85
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
86
|
+
* Absence and unparseable input stay absent (`undefined`): a missing,
|
|
87
|
+
* malformed, or non-finite timestamp must never become the epoch, which
|
|
88
|
+
* would place the event at 1970 and corrupt timeline ordering. Callers
|
|
89
|
+
* apply the per-field policy explicitly — required timestamps (message,
|
|
90
|
+
* receipt, call, group action) drop the event, optional ones (presence
|
|
91
|
+
* `last_seen`, server-ack time, pin/mute time) omit the field.
|
|
99
92
|
*/
|
|
100
93
|
export const asUnixSeconds = (raw) => {
|
|
101
94
|
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
@@ -104,6 +97,26 @@ export const asUnixSeconds = (raw) => {
|
|
|
104
97
|
return parseRfc3339Seconds(raw);
|
|
105
98
|
return undefined;
|
|
106
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Validated coercion to unix seconds for external consumers that need a
|
|
102
|
+
* plain number (no `undefined` branch).
|
|
103
|
+
*
|
|
104
|
+
* Accepts the same inputs as `asUnixSeconds` — a finite number or an RFC
|
|
105
|
+
* 3339 string — and rejects everything else by throwing `RangeError`
|
|
106
|
+
* instead of fabricating the epoch: a missing, malformed, or non-finite
|
|
107
|
+
* timestamp must never become 1970-01-01 and corrupt timeline ordering.
|
|
108
|
+
* Internal adapters do not use this; they take `asUnixSeconds` with an
|
|
109
|
+
* explicit per-field policy (required timestamps drop the event, optional
|
|
110
|
+
* ones omit the field).
|
|
111
|
+
*/
|
|
112
|
+
export const toUnixSeconds = (raw) => {
|
|
113
|
+
const parsed = asUnixSeconds(raw);
|
|
114
|
+
if (parsed === undefined) {
|
|
115
|
+
const preview = typeof raw === 'string' ? `: ${raw.slice(0, 80)}` : '';
|
|
116
|
+
throw new RangeError(`toUnixSeconds: cannot coerce ${typeof raw}${preview} to unix seconds`);
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
};
|
|
107
120
|
/**
|
|
108
121
|
* A 64-bit proto field, however the bridge chose to carry it.
|
|
109
122
|
*
|
package/lib/Bridge/schema.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { processHistoryMessage } from '../Utils/process-history-message.js';
|
|
26
26
|
import { isJidGroup } from '../WABinary/jid-utils.js';
|
|
27
|
-
import { absoluteFromDuration, asBool, asBoolOr, asStringArray, asDurationSeconds, asInt64, asJidAddressString, asJidString, asNumber, asString, asUnixSeconds, isObject, normalizeDiscriminator
|
|
27
|
+
import { absoluteFromDuration, asBool, asBoolOr, asStringArray, asDurationSeconds, asInt64, asJidAddressString, asJidString, asNumber, asString, asUnixSeconds, isObject, normalizeDiscriminator } from './primitives.js';
|
|
28
28
|
/**
|
|
29
29
|
* Bridge sync-action events (`pin_update`, `mute_update`, etc.) carry the
|
|
30
30
|
* proto action under `data.action`, but the bridge `.d.ts` types it as
|
|
@@ -39,6 +39,17 @@ const extractAction = (data) => isObject(data?.action) ? data.action : undefined
|
|
|
39
39
|
const resolveIsGroup = (wireValue, chatJid) => asBoolOr(wireValue, false) || isJidGroup(chatJid) === true;
|
|
40
40
|
const resolveParticipantAlt = (senderAlt, isGroup) => isGroup ? senderAlt : undefined;
|
|
41
41
|
const resolveRemoteJidAlt = (senderAlt, recipientAlt, isGroup, isFromMe) => (isGroup ? undefined : isFromMe ? recipientAlt : senderAlt);
|
|
42
|
+
/**
|
|
43
|
+
* Rejection diagnostic for a required timestamp that is missing or
|
|
44
|
+
* unparseable. Metadata only — never the payload: these objects can carry
|
|
45
|
+
* message bodies and JIDs.
|
|
46
|
+
*/
|
|
47
|
+
const invalidTimestampDetail = (event, raw) => ({
|
|
48
|
+
event,
|
|
49
|
+
field: 'timestamp',
|
|
50
|
+
reason: raw === undefined || raw === null ? 'missing' : 'invalid',
|
|
51
|
+
receivedType: typeof raw
|
|
52
|
+
});
|
|
42
53
|
const parseEditAttribute = (value) => {
|
|
43
54
|
switch (value) {
|
|
44
55
|
case '1':
|
|
@@ -141,7 +152,7 @@ const ADAPTERS = {
|
|
|
141
152
|
error: asString(data?.error)
|
|
142
153
|
};
|
|
143
154
|
},
|
|
144
|
-
undecryptable_message: data => {
|
|
155
|
+
undecryptable_message: (data, logger) => {
|
|
145
156
|
// Bridge ships `{ info: MessageInfo, is_unavailable, unavailable_type,
|
|
146
157
|
// decrypt_fail_mode }` — same MessageInfo shape as the regular
|
|
147
158
|
// `message` event. Extract the chat / sender / id so the dispatcher
|
|
@@ -161,12 +172,17 @@ const ADAPTERS = {
|
|
|
161
172
|
const isFromMe = asBoolOr(src.is_from_me, false);
|
|
162
173
|
const senderAlt = asJidString(src.sender_alt);
|
|
163
174
|
const recipientAlt = asJidString(src.recipient_alt);
|
|
175
|
+
const timestamp = asUnixSeconds(info.timestamp);
|
|
176
|
+
if (timestamp === undefined) {
|
|
177
|
+
logger?.debug(invalidTimestampDetail('undecryptable_message', info.timestamp), 'undecryptable_message adapter: missing or invalid timestamp');
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
164
180
|
return {
|
|
165
181
|
type: 'undecryptableMessage',
|
|
166
182
|
chatJid: chat,
|
|
167
183
|
senderJid: isGroup ? asJidString(src.sender) : undefined,
|
|
168
184
|
id,
|
|
169
|
-
timestamp
|
|
185
|
+
timestamp,
|
|
170
186
|
isFromMe,
|
|
171
187
|
isGroup,
|
|
172
188
|
pushName: asString(info.push_name),
|
|
@@ -386,28 +402,38 @@ const ADAPTERS = {
|
|
|
386
402
|
call_log_sync: () => ({ type: 'noop', bridgeType: 'call_log_sync' }),
|
|
387
403
|
// ── Calls ──
|
|
388
404
|
incoming_call: (data, logger) => adaptIncomingCall(data, logger),
|
|
389
|
-
missed_call: data => {
|
|
405
|
+
missed_call: (data, logger) => {
|
|
390
406
|
const from = asJidString(data?.from);
|
|
391
407
|
const callId = asString(data?.call_id);
|
|
392
408
|
if (!from || !callId)
|
|
393
409
|
return null;
|
|
410
|
+
const timestamp = asUnixSeconds(data?.timestamp);
|
|
411
|
+
if (timestamp === undefined) {
|
|
412
|
+
logger?.debug(invalidTimestampDetail('missed_call', data?.timestamp), 'missed_call adapter: missing or invalid timestamp');
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
394
415
|
return {
|
|
395
416
|
type: 'incomingCall',
|
|
396
417
|
from,
|
|
397
|
-
timestamp
|
|
418
|
+
timestamp,
|
|
398
419
|
offline: data?.reason === 'offline',
|
|
399
420
|
action: { type: 'timeout', callId }
|
|
400
421
|
};
|
|
401
422
|
},
|
|
402
|
-
call_ended_elsewhere: data => {
|
|
423
|
+
call_ended_elsewhere: (data, logger) => {
|
|
403
424
|
const from = asJidString(data?.from);
|
|
404
425
|
const callId = asString(data?.call_id);
|
|
405
426
|
if (!from || !callId)
|
|
406
427
|
return null;
|
|
428
|
+
const timestamp = asUnixSeconds(data?.timestamp);
|
|
429
|
+
if (timestamp === undefined) {
|
|
430
|
+
logger?.debug(invalidTimestampDetail('call_ended_elsewhere', data?.timestamp), 'call_ended_elsewhere adapter: missing or invalid timestamp');
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
407
433
|
return {
|
|
408
434
|
type: 'incomingCall',
|
|
409
435
|
from,
|
|
410
|
-
timestamp
|
|
436
|
+
timestamp,
|
|
411
437
|
offline: false,
|
|
412
438
|
action: { type: data?.outcome === 'accepted' ? 'accept' : 'reject', callId }
|
|
413
439
|
};
|
|
@@ -680,6 +706,15 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
|
|
|
680
706
|
const isFromMe = asBoolOr(info.isFromMe, false);
|
|
681
707
|
const senderAlt = asString(info.senderAlt);
|
|
682
708
|
const recipientAlt = asString(info.recipientAlt);
|
|
709
|
+
// The packed envelope carries the timestamp as a numeric record, so the
|
|
710
|
+
// only invalid shapes here are a missing or non-finite value. Like the
|
|
711
|
+
// object route below, those drop the message rather than dating it at
|
|
712
|
+
// the epoch.
|
|
713
|
+
const timestamp = asNumber(info.timestamp);
|
|
714
|
+
if (timestamp === undefined) {
|
|
715
|
+
logger?.debug(invalidTimestampDetail('message_wire', info.timestamp), 'message wire adapter: missing or invalid timestamp');
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
683
718
|
return {
|
|
684
719
|
type: 'message',
|
|
685
720
|
chatJid: chat,
|
|
@@ -687,7 +722,7 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
|
|
|
687
722
|
isGroup,
|
|
688
723
|
isFromMe,
|
|
689
724
|
id,
|
|
690
|
-
timestamp
|
|
725
|
+
timestamp,
|
|
691
726
|
pushName: asString(info.pushName),
|
|
692
727
|
participantAlt: resolveParticipantAlt(senderAlt, isGroup),
|
|
693
728
|
remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
|
|
@@ -712,6 +747,11 @@ const adaptMessageParts = (info, messageProto, logger) => {
|
|
|
712
747
|
const senderJid = isGroup ? asJidString(senderRaw) : undefined;
|
|
713
748
|
const senderAlt = asJidString(src.sender_alt);
|
|
714
749
|
const recipientAlt = asJidString(src.recipient_alt);
|
|
750
|
+
const timestamp = asUnixSeconds(info.timestamp);
|
|
751
|
+
if (timestamp === undefined) {
|
|
752
|
+
logger?.debug(invalidTimestampDetail('message', info.timestamp), 'message adapter: missing or invalid timestamp');
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
715
755
|
return {
|
|
716
756
|
type: 'message',
|
|
717
757
|
chatJid: chat,
|
|
@@ -719,7 +759,7 @@ const adaptMessageParts = (info, messageProto, logger) => {
|
|
|
719
759
|
isGroup,
|
|
720
760
|
isFromMe,
|
|
721
761
|
id,
|
|
722
|
-
timestamp
|
|
762
|
+
timestamp,
|
|
723
763
|
pushName: asString(info.push_name),
|
|
724
764
|
participantAlt: resolveParticipantAlt(senderAlt, isGroup),
|
|
725
765
|
remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
|
|
@@ -743,6 +783,16 @@ const adaptReceipt = (data, logger) => {
|
|
|
743
783
|
return null;
|
|
744
784
|
}
|
|
745
785
|
const isGroup = resolveIsGroup(src.is_group, chat);
|
|
786
|
+
// A receipt without a trustworthy timestamp cannot be placed on any
|
|
787
|
+
// timeline slot (`readTimestamp` / `playedTimestamp` /
|
|
788
|
+
// `receiptTimestamp`), so the event is dropped rather than dated at the
|
|
789
|
+
// epoch. Optional timestamps elsewhere stay absent via `asUnixSeconds`.
|
|
790
|
+
const timestamp = asUnixSeconds(data.timestamp);
|
|
791
|
+
if (timestamp === undefined) {
|
|
792
|
+
logger?.debug(invalidTimestampDetail('receipt', data.timestamp), 'receipt adapter: missing or invalid timestamp');
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const { receiptType, raw: receiptTypeRaw } = parseReceiptType(data.type);
|
|
746
796
|
return {
|
|
747
797
|
type: 'receipt',
|
|
748
798
|
chatJid: chat,
|
|
@@ -750,8 +800,9 @@ const adaptReceipt = (data, logger) => {
|
|
|
750
800
|
isGroup,
|
|
751
801
|
isFromMe: asBoolOr(src.is_from_me, false),
|
|
752
802
|
messageIds: ids,
|
|
753
|
-
timestamp
|
|
754
|
-
receiptType
|
|
803
|
+
timestamp,
|
|
804
|
+
receiptType,
|
|
805
|
+
...(receiptTypeRaw !== undefined ? { receiptTypeRaw } : {})
|
|
755
806
|
};
|
|
756
807
|
};
|
|
757
808
|
const adaptContactUpdate = (data) => {
|
|
@@ -772,7 +823,8 @@ const adaptContactUpdate = (data) => {
|
|
|
772
823
|
};
|
|
773
824
|
};
|
|
774
825
|
/**
|
|
775
|
-
* Bridge wire `ReceiptType` → canonical kebab-cased variant
|
|
826
|
+
* Bridge wire `ReceiptType` → canonical kebab-cased variant, preserving the
|
|
827
|
+
* original value when it matches nothing known.
|
|
776
828
|
*
|
|
777
829
|
* The bridge's generated `.d.ts` advertises `ReceiptType` as
|
|
778
830
|
* `{type: "delivered"} | …`, but `#[serde(from = "String")]` on the rust
|
|
@@ -780,6 +832,12 @@ const adaptContactUpdate = (data) => {
|
|
|
780
832
|
* bare PascalCase variant name (`"Delivered"`, `"Read"`, `"PeerMsg"`).
|
|
781
833
|
* Keep both spellings here so a future bridge bump that re-introduces
|
|
782
834
|
* the snake_case wire form keeps working.
|
|
835
|
+
*
|
|
836
|
+
* Unrecognized wire values arrive wrapped as `{ Other: value }` (the packed
|
|
837
|
+
* `ReceiptWireData.type` contract) or, defensively, as a future bare
|
|
838
|
+
* string. Those map to `'other'` with the original value in `raw`. The
|
|
839
|
+
* wrapper is trusted over spelling: `{ Other: 'Read' }` is `other`, not
|
|
840
|
+
* `read` (see `parseReceiptType`).
|
|
783
841
|
*/
|
|
784
842
|
const RECEIPT_TYPE_MAP = {
|
|
785
843
|
Delivered: 'delivered',
|
|
@@ -810,10 +868,22 @@ const RECEIPT_TYPE_MAP = {
|
|
|
810
868
|
server_error: 'server-error'
|
|
811
869
|
};
|
|
812
870
|
const parseReceiptType = (raw) => {
|
|
871
|
+
// The `Other` wrapper is authoritative: the packed codec round-trips
|
|
872
|
+
// `{ Other: value }` verbatim and never synthesizes it for a known
|
|
873
|
+
// variant, so even a payload whose spelling collides with a known
|
|
874
|
+
// variant name (e.g. `{ Other: 'Read' }`) stays category `other` with
|
|
875
|
+
// its exact payload preserved. Only bare strings and `{ type }` go
|
|
876
|
+
// through the known-variant lookup.
|
|
877
|
+
if (isObject(raw) && typeof raw.Other === 'string')
|
|
878
|
+
return { receiptType: 'other', raw: raw.Other };
|
|
813
879
|
const norm = typeof raw === 'string' ? raw : isObject(raw) && typeof raw.type === 'string' ? raw.type : undefined;
|
|
814
880
|
if (norm == null)
|
|
815
|
-
return undefined;
|
|
816
|
-
|
|
881
|
+
return { receiptType: undefined };
|
|
882
|
+
// Own-property lookup: the table is an ordinary object, so indexing an
|
|
883
|
+
// unknown bare name like 'constructor' or '__proto__' would otherwise
|
|
884
|
+
// return an inherited value instead of falling through to 'other'.
|
|
885
|
+
const mapped = Object.hasOwn(RECEIPT_TYPE_MAP, norm) ? RECEIPT_TYPE_MAP[norm] : undefined;
|
|
886
|
+
return mapped ? { receiptType: mapped } : { receiptType: 'other', raw: norm };
|
|
817
887
|
};
|
|
818
888
|
const adaptStarUpdate = (data) => {
|
|
819
889
|
if (!isObject(data))
|
|
@@ -854,6 +924,13 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
854
924
|
logger?.debug({ data }, 'incoming_call adapter: missing action.type/call_id');
|
|
855
925
|
return null;
|
|
856
926
|
}
|
|
927
|
+
// The call offer is timeline-ordered by its timestamp downstream; an
|
|
928
|
+
// unparseable one drops the event rather than dating it at the epoch.
|
|
929
|
+
const timestamp = asUnixSeconds(data.timestamp);
|
|
930
|
+
if (timestamp === undefined) {
|
|
931
|
+
logger?.debug(invalidTimestampDetail('incoming_call', data.timestamp), 'incoming_call adapter: missing or invalid timestamp');
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
857
934
|
const canonicalAction = {
|
|
858
935
|
type: actionType,
|
|
859
936
|
callId,
|
|
@@ -876,7 +953,7 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
876
953
|
return {
|
|
877
954
|
type: 'incomingCall',
|
|
878
955
|
from: asJidAddressString(data.from) ?? from,
|
|
879
|
-
timestamp
|
|
956
|
+
timestamp,
|
|
880
957
|
offline: asBoolOr(data.offline, false),
|
|
881
958
|
stanzaId: asString(data.stanza_id),
|
|
882
959
|
notify: asString(data.notify),
|
|
@@ -1108,6 +1185,11 @@ const adaptGroupUpdate = (data, logger) => {
|
|
|
1108
1185
|
logger?.warn({ data }, 'group_update adapter: action shape rejected');
|
|
1109
1186
|
return null;
|
|
1110
1187
|
}
|
|
1188
|
+
const timestamp = asUnixSeconds(data.timestamp);
|
|
1189
|
+
if (timestamp === undefined) {
|
|
1190
|
+
logger?.debug(invalidTimestampDetail('group_update', data.timestamp), 'group_update adapter: missing or invalid timestamp');
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1111
1193
|
return {
|
|
1112
1194
|
type: 'groupUpdate',
|
|
1113
1195
|
groupJid,
|
|
@@ -1117,7 +1199,7 @@ const adaptGroupUpdate = (data, logger) => {
|
|
|
1117
1199
|
authorPn: asJidString(data.participant_pn),
|
|
1118
1200
|
authorUsername: asString(data.participant_username),
|
|
1119
1201
|
authorCountryCode: asString(data.participant_country_code),
|
|
1120
|
-
timestamp
|
|
1202
|
+
timestamp,
|
|
1121
1203
|
isLidAddressingMode: asBoolOr(data.is_lid_addressing_mode, false),
|
|
1122
1204
|
action
|
|
1123
1205
|
};
|
package/lib/Bridge/types.d.ts
CHANGED
|
@@ -172,6 +172,17 @@ export interface CanonicalReceipt {
|
|
|
172
172
|
* `readTimestamp` / `playedTimestamp` slot in the emitted update.
|
|
173
173
|
*/
|
|
174
174
|
receiptType?: 'delivered' | 'sent' | 'sender' | 'retry' | 'enc-rekey-retry' | 'read' | 'read-self' | 'played' | 'played-self' | 'inactive' | 'peer-msg' | 'history-sync' | 'server-error' | 'other';
|
|
175
|
+
/**
|
|
176
|
+
* Original wire value when `receiptType` is `'other'` — the bridge sends
|
|
177
|
+
* unrecognized variants as `{ Other: value }` (or a future bare string),
|
|
178
|
+
* and the category alone would lose which one it was. Always set for an
|
|
179
|
+
* `Other`-wrapped payload, even when its spelling collides with a known
|
|
180
|
+
* variant name: the wrapper means the producer did not recognize it.
|
|
181
|
+
* Absent whenever the value matched a known variant through a bare
|
|
182
|
+
* string or `{ type }` shape, and whenever `receiptType` itself is
|
|
183
|
+
* absent.
|
|
184
|
+
*/
|
|
185
|
+
receiptTypeRaw?: string;
|
|
175
186
|
}
|
|
176
187
|
export interface CanonicalContactUpdate {
|
|
177
188
|
type: 'contactUpdate';
|
package/lib/Socket/index.js
CHANGED
|
@@ -454,7 +454,13 @@ const makeWASocket = (config) => {
|
|
|
454
454
|
}
|
|
455
455
|
if (useNativeMemory)
|
|
456
456
|
logger.debug('auth: using socket-local native memory backend');
|
|
457
|
-
const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null
|
|
457
|
+
const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null,
|
|
458
|
+
// Passed through exactly as configured, never normalized by truthiness:
|
|
459
|
+
// the bridge only honours a literal `true` here and rejects any
|
|
460
|
+
// other truthy value at construction, so a `!!`/ternary-style
|
|
461
|
+
// coercion could promote a malformed opt-out into an opt-in.
|
|
462
|
+
// Absent stays strict.
|
|
463
|
+
fullConfig.dangerSkipCertChainVerify);
|
|
458
464
|
// `end()` can land while the client is still being built — a `sock.end()`
|
|
459
465
|
// or `await using` right after `makeWASocket()` does exactly that. When
|
|
460
466
|
// it has, `adopt` frees this client and tells us to stop: nothing else
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reports the terminal `connection.update { close }` — exactly once,
|
|
3
|
-
*
|
|
2
|
+
* Reports the terminal `connection.update { close }` — exactly once, and
|
|
3
|
+
* never silently not at all.
|
|
4
|
+
*
|
|
5
|
+
* The accepted claim publishes after its teardown settles, so a consumer that
|
|
6
|
+
* answers the close with a replacement socket does not overlap the old one's
|
|
7
|
+
* release. The watchdog is the deliberate exception: past the timeout the
|
|
8
|
+
* close goes out with teardown still running (and logged), because losing the
|
|
9
|
+
* event entirely is the worse failure. Nothing here promises that every close
|
|
10
|
+
* lands after every resource is released — only that at most one close lands.
|
|
4
11
|
*
|
|
5
12
|
* That sentence is the whole contract this branch sells, and getting it wrong
|
|
6
13
|
* has two opposite failure modes, both bad:
|
|
@@ -8,9 +15,8 @@
|
|
|
8
15
|
* - **Not reported.** The consumer's handler never runs, so it never builds a
|
|
9
16
|
* replacement socket. A bot offline with nothing in its logs — the original
|
|
10
17
|
* bug this branch exists to fix.
|
|
11
|
-
* - **Reported
|
|
12
|
-
* one
|
|
13
|
-
* notifications for one socket and a handler that cleans up on close loops.
|
|
18
|
+
* - **Reported twice.** Every listener sees two terminal notifications for
|
|
19
|
+
* one socket and a handler that cleans up on close loops.
|
|
14
20
|
*
|
|
15
21
|
* Keeping both away used to be inline logic split across the dispatcher hook
|
|
16
22
|
* and `logout()`, sharing a counter and a promise. Nine separate bugs came out
|
|
@@ -41,13 +47,26 @@ export interface TerminalCloseReporter {
|
|
|
41
47
|
*
|
|
42
48
|
* Never rejects and never leaves `publish` uncalled: teardown failures are
|
|
43
49
|
* logged, a throwing listener is contained, and a teardown that hangs is
|
|
44
|
-
*
|
|
50
|
+
* reported past by the watchdog.
|
|
51
|
+
*
|
|
52
|
+
* Idempotent per socket: the first claim wins and every later call is
|
|
53
|
+
* ignored entirely — neither its teardown nor its publish is invoked. A
|
|
54
|
+
* socket never gets a second generation — a terminal close means "build a
|
|
55
|
+
* new socket" — so a second terminal event (a logout racing a dispatcher
|
|
56
|
+
* close, a late duplicate dispatch) must not publish again.
|
|
57
|
+
*
|
|
58
|
+
* The watchdog bounds how long `published()` waits, not the teardown
|
|
59
|
+
* itself: it neither cancels nor settles the underlying teardown, and a
|
|
60
|
+
* teardown that finishes late publishes nothing further.
|
|
45
61
|
*/
|
|
46
62
|
reportAfter: (teardown: () => Promise<void>, publish: () => void) => void;
|
|
47
63
|
/**
|
|
48
64
|
* Publish immediately, for a close nothing else will announce — a `logout()`
|
|
49
65
|
* with no live client, or one whose `logout()` threw before the bridge
|
|
50
66
|
* dispatched anything.
|
|
67
|
+
*
|
|
68
|
+
* Part of the same single claim as `reportAfter`: a no-op when a close
|
|
69
|
+
* was already claimed.
|
|
51
70
|
*/
|
|
52
71
|
reportNow: (publish: () => void) => void;
|
|
53
72
|
/**
|
|
@@ -63,11 +82,13 @@ export interface TerminalCloseReporter {
|
|
|
63
82
|
*/
|
|
64
83
|
hasReported: () => boolean;
|
|
65
84
|
/**
|
|
66
|
-
* Settles once the
|
|
85
|
+
* Settles once the single report has been published — the moment the
|
|
67
86
|
* consumer sees it, not the moment teardown finishes, so a waiter is not
|
|
68
87
|
* left hanging when the watchdog is what released the event.
|
|
69
88
|
*
|
|
70
|
-
* Resolves immediately when nothing has been reported.
|
|
89
|
+
* Resolves immediately when nothing has been reported. Every waiter
|
|
90
|
+
* observes the same promise, so concurrent `logout()` and dispatcher
|
|
91
|
+
* paths cannot split across generations.
|
|
71
92
|
*/
|
|
72
93
|
published: () => Promise<void>;
|
|
73
94
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reports the terminal `connection.update { close }` — exactly once,
|
|
3
|
-
*
|
|
2
|
+
* Reports the terminal `connection.update { close }` — exactly once, and
|
|
3
|
+
* never silently not at all.
|
|
4
|
+
*
|
|
5
|
+
* The accepted claim publishes after its teardown settles, so a consumer that
|
|
6
|
+
* answers the close with a replacement socket does not overlap the old one's
|
|
7
|
+
* release. The watchdog is the deliberate exception: past the timeout the
|
|
8
|
+
* close goes out with teardown still running (and logged), because losing the
|
|
9
|
+
* event entirely is the worse failure. Nothing here promises that every close
|
|
10
|
+
* lands after every resource is released — only that at most one close lands.
|
|
4
11
|
*
|
|
5
12
|
* That sentence is the whole contract this branch sells, and getting it wrong
|
|
6
13
|
* has two opposite failure modes, both bad:
|
|
@@ -8,9 +15,8 @@
|
|
|
8
15
|
* - **Not reported.** The consumer's handler never runs, so it never builds a
|
|
9
16
|
* replacement socket. A bot offline with nothing in its logs — the original
|
|
10
17
|
* bug this branch exists to fix.
|
|
11
|
-
* - **Reported
|
|
12
|
-
* one
|
|
13
|
-
* notifications for one socket and a handler that cleans up on close loops.
|
|
18
|
+
* - **Reported twice.** Every listener sees two terminal notifications for
|
|
19
|
+
* one socket and a handler that cleans up on close loops.
|
|
14
20
|
*
|
|
15
21
|
* Keeping both away used to be inline logic split across the dispatcher hook
|
|
16
22
|
* and `logout()`, sharing a counter and a promise. Nine separate bugs came out
|
|
@@ -35,9 +41,24 @@ export const TERMINAL_CLOSE_PUBLISH_TIMEOUT_MS = 60000;
|
|
|
35
41
|
export const makeTerminalCloseReporter = (opts) => {
|
|
36
42
|
const { logger } = opts;
|
|
37
43
|
const publishTimeoutMs = opts.publishTimeoutMs ?? TERMINAL_CLOSE_PUBLISH_TIMEOUT_MS;
|
|
38
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Claims, not deliveries — see `hasReported`. At most one: a socket has a
|
|
46
|
+
* single terminal generation, so the first claim wins and later ones are
|
|
47
|
+
* ignored rather than published.
|
|
48
|
+
*/
|
|
39
49
|
let claimed = 0;
|
|
40
50
|
let publishedPromise;
|
|
51
|
+
/**
|
|
52
|
+
* True once the single terminal generation has been claimed. Deliberately
|
|
53
|
+
* log-free: a duplicate arrives on a path whose logger is
|
|
54
|
+
* consumer-replaceable, and an ignored signal must never throw.
|
|
55
|
+
*/
|
|
56
|
+
const claim = () => {
|
|
57
|
+
if (claimed > 0)
|
|
58
|
+
return false;
|
|
59
|
+
claimed++;
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
41
62
|
/** One publish per claim, whatever gets there first, and never throwing. */
|
|
42
63
|
const makeOnce = (publish, settle) => {
|
|
43
64
|
let done = false;
|
|
@@ -59,7 +80,8 @@ export const makeTerminalCloseReporter = (opts) => {
|
|
|
59
80
|
};
|
|
60
81
|
return {
|
|
61
82
|
reportAfter: (teardown, publish) => {
|
|
62
|
-
|
|
83
|
+
if (!claim())
|
|
84
|
+
return;
|
|
63
85
|
let settle;
|
|
64
86
|
publishedPromise = new Promise(resolve => {
|
|
65
87
|
settle = resolve;
|
|
@@ -94,7 +116,8 @@ export const makeTerminalCloseReporter = (opts) => {
|
|
|
94
116
|
}
|
|
95
117
|
},
|
|
96
118
|
reportNow: publish => {
|
|
97
|
-
|
|
119
|
+
if (!claim())
|
|
120
|
+
return;
|
|
98
121
|
let settle;
|
|
99
122
|
publishedPromise = new Promise(resolve => {
|
|
100
123
|
settle = resolve;
|
|
@@ -33,7 +33,7 @@ export declare const UNSUPPORTED_CONFIG_KEYS: readonly ["keepAliveIntervalMs", "
|
|
|
33
33
|
* member of `SocketConfig` belongs to neither list — which is exactly how the
|
|
34
34
|
* first version of the catalog shipped twelve keys short.
|
|
35
35
|
*/
|
|
36
|
-
export declare const READ_CONFIG_KEYS: readonly ["waWebSocketUrl", "options", "logger", "version", "browser", "pushName", "auth", "cache", "deviceProps", "wantedPreKeyCount", "emitOwnEvents", "shouldIgnoreJid", "defaultQueryTimeoutMs", "transactionOpts", "makeSignalRepository"];
|
|
36
|
+
export declare const READ_CONFIG_KEYS: readonly ["waWebSocketUrl", "options", "logger", "version", "browser", "pushName", "auth", "cache", "deviceProps", "wantedPreKeyCount", "dangerSkipCertChainVerify", "emitOwnEvents", "shouldIgnoreJid", "defaultQueryTimeoutMs", "transactionOpts", "makeSignalRepository"];
|
|
37
37
|
/**
|
|
38
38
|
* Which unsupported options this caller actually passed.
|
|
39
39
|
*
|
package/lib/Types/Socket.d.ts
CHANGED
|
@@ -93,6 +93,14 @@ export type SocketConfig = {
|
|
|
93
93
|
* generated and encoded in one shot). Must be set before connecting.
|
|
94
94
|
*/
|
|
95
95
|
wantedPreKeyCount?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Testing-only bypass for the Noise server-cert chain check, for mock
|
|
98
|
+
* servers that cannot sign a chain rooted in WhatsApp's issuer. Strict by
|
|
99
|
+
* default: absent, null and false all verify, and only a literal `true`
|
|
100
|
+
* opts in — the bridge rejects any other truthy value at construction
|
|
101
|
+
* rather than treating it as opt-in. Never set this outside tests.
|
|
102
|
+
*/
|
|
103
|
+
dangerSkipCertChainVerify?: boolean;
|
|
96
104
|
/** @deprecated QR timeout is handled by the bridge connection state machine. */
|
|
97
105
|
qrTimeout?: number;
|
|
98
106
|
/** Maximum retry count. */
|