@oxidezap/baileyrs 0.2.7 → 0.2.9

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 CHANGED
@@ -374,6 +374,29 @@ baileyrs auto-wraps, including `useLegacyMultiFileAuthState`. It does not apply
374
374
  to `useMultiFileAuthState`, whose `keys` is a projection over the engine's own
375
375
  store; the `bridge-` namespaces never surface through it.
376
376
 
377
+ The move back down is not symmetric either. From 0.2.8 on, the core buffers a
378
+ skipped-message key as its seed alone, where an earlier release wrote the
379
+ derived cipher/mac/iv triple beside it. Forward is fine — a record written
380
+ before this carries both, and the newer engine reads it unchanged. Backward is
381
+ not: an older engine cannot parse a record whose skipped keys carry no triple,
382
+ and it fails on **the whole record**, not on the one key.
383
+
384
+ Where that bites depends on which bytes the engine is handed, and a wrapped
385
+ store is not automatically on the safe side of it. It holds the Baileys
386
+ projection, which has only ever held seeds — but it also mirrors the core's
387
+ exact bytes under `bridge-native-session`, and a read prefers that mirror for as
388
+ long as its fingerprint still matches the projection. A downgrade can therefore
389
+ be handed the newer record with a perfectly readable projection sitting right
390
+ beside it.
391
+
392
+ Dropping a mirror row makes the next read rebuild it from the projection, which
393
+ is the format both versions agree on — but only for a session that has one. A
394
+ session that turned native-only lives in that row alone, so dropping *that* one
395
+ loses the session outright, which is the case the warning above is about. A
396
+ session with no skipped keys buffered at the time is unaffected either way: the
397
+ difference only exists while a chain is holding keys for messages that arrived
398
+ out of order.
399
+
377
400
  ## Disclaimer
378
401
 
379
402
  This project is not affiliated with, endorsed by, or in any way officially connected to
@@ -102,11 +102,18 @@ function tcTokenCodec() {
102
102
  },
103
103
  toLegacy(_key, value) {
104
104
  const native = fromJsonBytes(value);
105
- return {
105
+ // Added rather than set to `undefined`, matching upstream, which
106
+ // only carries the field when it has a value. An undefined-valued
107
+ // key survives in memory but not through a store, and the mirror
108
+ // fingerprint is taken on both sides of that boundary — see the
109
+ // note in `canonicalize`.
110
+ const legacy = {
106
111
  token: Buffer.from(requireByteArray(native.token, 'native TC token')),
107
- timestamp: (native.token_timestamp ?? TimeValue.UNKNOWN_SECONDS).toString(),
108
- senderTimestamp: native.sender_timestamp == null ? undefined : native.sender_timestamp.toString()
112
+ timestamp: (native.token_timestamp ?? TimeValue.UNKNOWN_SECONDS).toString()
109
113
  };
114
+ if (native.sender_timestamp != null)
115
+ legacy.senderTimestamp = native.sender_timestamp.toString();
116
+ return legacy;
110
117
  }
111
118
  };
112
119
  }
@@ -177,7 +177,16 @@ function coreEntryToLegacy(session) {
177
177
  messageKeys: messageKeysToLegacy(chain.messageKeys)
178
178
  };
179
179
  }
180
- return {
180
+ // Both optional parts are *added* rather than set to `undefined`, which is
181
+ // what upstream does (`session_record.js` guards `if (this.pendingPreKey)`,
182
+ // `session_builder.js` guards `if (device.preKey)`). The distinction is
183
+ // invisible once the value reaches a store — JSON drops an undefined-valued
184
+ // key — but it is visible to the mirror fingerprint, which is taken from
185
+ // the live object on write and from the reloaded one on read. Emitting the
186
+ // key made those two disagree for every session without a pending pre-key,
187
+ // so the mirror was rejected on the next read and the record rebuilt from
188
+ // the projection, discarding the counter lease the rebuild cannot express.
189
+ const entry = {
181
190
  registrationId: session.registrationId,
182
191
  currentRatchet: {
183
192
  ephemeralKeyPair: {
@@ -196,15 +205,18 @@ function coreEntryToLegacy(session) {
196
205
  created: session.index.createdAtMs,
197
206
  remoteIdentityKey: toBase64(session.index.remoteIdentityKey)
198
207
  },
199
- _chains: chains,
200
- pendingPreKey: session.pendingPreKey
201
- ? {
202
- preKeyId: session.pendingPreKey.preKeyId,
203
- signedKeyId: session.pendingPreKey.signedPreKeyId,
204
- baseKey: toBase64(session.pendingPreKey.baseKey)
205
- }
206
- : undefined
208
+ _chains: chains
207
209
  };
210
+ if (session.pendingPreKey) {
211
+ entry.pendingPreKey = {
212
+ signedKeyId: session.pendingPreKey.signedPreKeyId,
213
+ baseKey: toBase64(session.pendingPreKey.baseKey)
214
+ };
215
+ if (session.pendingPreKey.preKeyId !== undefined) {
216
+ entry.pendingPreKey.preKeyId = session.pendingPreKey.preKeyId;
217
+ }
218
+ }
219
+ return entry;
208
220
  }
209
221
  function sessionToLegacy(nativeBytes) {
210
222
  const projection = projectLegacySessionRecordV1(nativeBytes);
@@ -16,15 +16,29 @@ export const toBytes = (value) => {
16
16
  return null;
17
17
  };
18
18
  export const bytesToNumbers = (value) => Array.from(value);
19
+ /**
20
+ * Marks a value JSON would not have kept, so the object and array branches can
21
+ * each drop it the way `JSON.stringify` does. A plain `undefined` return could
22
+ * not be told apart from a property that is genuinely absent.
23
+ */
24
+ const ABSENT = Symbol('absent');
19
25
  /**
20
26
  * Stable, runtime-independent representation used only to notice legacy-side
21
27
  * ownership changes. Buffer and Uint8Array intentionally share one shape.
28
+ *
29
+ * `undefined` follows `JSON.stringify`, and that is deliberately asymmetric: an
30
+ * object property is dropped, an array element becomes `null`. The fingerprint
31
+ * is taken from the live projection when the mirror is written and from the
32
+ * reloaded one when it is read, so any distinction a store cannot carry makes
33
+ * the two disagree forever. Tagging `undefined` as its own value was such a
34
+ * distinction — no persisted store can express it, and the mirror it invalidated
35
+ * is the only copy of the record fields the projection has no room for.
22
36
  */
23
37
  function canonicalize(value, seen) {
24
38
  if (value === null)
25
39
  return null;
26
40
  if (value === undefined)
27
- return { [CanonicalTag.UNDEFINED]: true };
41
+ return ABSENT;
28
42
  if (typeof value === 'string' || typeof value === 'boolean')
29
43
  return value;
30
44
  if (typeof value === 'number') {
@@ -51,11 +65,17 @@ function canonicalize(value, seen) {
51
65
  throw new TypeError('cannot fingerprint a cyclic legacy store value');
52
66
  seen.add(value);
53
67
  try {
54
- if (Array.isArray(value))
55
- return value.map(item => canonicalize(item, seen));
68
+ if (Array.isArray(value)) {
69
+ return value.map(item => {
70
+ const canonical = canonicalize(item, seen);
71
+ return canonical === ABSENT ? null : canonical;
72
+ });
73
+ }
56
74
  const out = {};
57
75
  for (const key of Object.keys(value).toSorted()) {
58
- out[key] = canonicalize(value[key], seen);
76
+ const canonical = canonicalize(value[key], seen);
77
+ if (canonical !== ABSENT)
78
+ out[key] = canonical;
59
79
  }
60
80
  return out;
61
81
  }
@@ -64,8 +84,13 @@ function canonicalize(value, seen) {
64
84
  }
65
85
  }
66
86
  function fingerprintLegacy(value) {
67
- const canonical = JSON.stringify(canonicalize(value, new Set()));
68
- return createHash(MirrorEnvelope.HASH).update(canonical).digest();
87
+ // A top-level absent value hashes as `null`: a store reports a row it does
88
+ // not hold as either, and the two have to agree for the same reason the
89
+ // property case does.
90
+ const canonical = canonicalize(value, new Set());
91
+ return createHash(MirrorEnvelope.HASH)
92
+ .update(JSON.stringify(canonical === ABSENT ? null : canonical))
93
+ .digest();
69
94
  }
70
95
  /** Persist exact native bytes together with the projection generation. */
71
96
  export function encodeNativeEnvelope(payload, legacyValue) {
@@ -101,7 +101,6 @@ export declare const CanonicalTag: Readonly<{
101
101
  readonly BYTES: '$bytes';
102
102
  readonly DATE: '$date';
103
103
  readonly NUMBER: '$number';
104
- readonly UNDEFINED: '$undefined';
105
104
  readonly UNSUPPORTED: '$unsupported';
106
105
  }>;
107
106
  export declare const NumericEncoding: Readonly<{
@@ -99,7 +99,6 @@ export const CanonicalTag = Object.freeze({
99
99
  BYTES: '$bytes',
100
100
  DATE: '$date',
101
101
  NUMBER: '$number',
102
- UNDEFINED: '$undefined',
103
102
  UNSUPPORTED: '$unsupported'
104
103
  });
105
104
  export const NumericEncoding = Object.freeze({
@@ -35,7 +35,17 @@ const emitCBEvents = (ctx, node) => {
35
35
  const id = l1.id;
36
36
  if (id)
37
37
  ws.emit(`${DEF_TAG_PREFIX}${id}`, node);
38
- for (const [key, val] of Object.entries(l1)) {
38
+ // `for..in` over the attrs rather than `Object.entries`: this runs for every
39
+ // stanza the socket sees and the pair array it built was thrown away
40
+ // immediately. `Object.entries` was own-only by construction though, and
41
+ // `BinaryNode` is public, so the walk is guarded rather than trusting every
42
+ // caller to hand over an attrs map with a bare prototype. An inherited
43
+ // enumerable would otherwise fire CB events for an attr the stanza does not
44
+ // carry; the check costs nothing next to the emits below.
45
+ for (const key in l1) {
46
+ if (!Object.hasOwn(l1, key))
47
+ continue;
48
+ const val = l1[key];
39
49
  if (l2)
40
50
  ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val},${l2}`, node);
41
51
  ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val}`, node);
@@ -112,6 +112,8 @@ export type PollMessageOptions = {
112
112
  name: string;
113
113
  selectableCount?: number;
114
114
  values: string[];
115
+ /** 32 byte message secret to encrypt poll selections */
116
+ messageSecret?: Uint8Array;
115
117
  toAnnouncementGroup?: boolean;
116
118
  };
117
119
  export type EventMessageOptions = {
@@ -8,6 +8,7 @@ import { proto } from '../WAProto/runtime.js';
8
8
  import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
9
9
  import { assertArgumentDomain } from './argument-domain.js';
10
10
  import { Boom } from './boom.js';
11
+ import { randomBytes } from 'node:crypto';
11
12
  import { sha256 } from './crypto.js';
12
13
  import { getKeyAuthor, toNumber, unixTimestampSeconds } from './generics.js';
13
14
  import { generateThumbnail, getAudioDuration, getAudioWaveform, getStream, toBuffer } from './messages-media.js';
@@ -434,6 +435,31 @@ export const generateWAMessageContent = async (message, options) => {
434
435
  m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed;
435
436
  m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
436
437
  m.eventMessage.location = message.event.location;
438
+ // The sender's copy of the key that encrypts responses to this event;
439
+ // `decryptEventResponse` takes it as `eventEncKey`. WhatsApp Web writes it
440
+ // on the sender at creation (`WAWebSendEventCreationMsgAction`) and refuses
441
+ // to build an event edit without it (`WAWebCreateEncryptedEventEditMsgData`
442
+ // throws `EventCreationValidationError`).
443
+ //
444
+ // The core settles `messageSecret` on relay for messages that reach it
445
+ // without one — see the comment in `Socket/messages.ts` — but it settles it
446
+ // on the wire, where the caller never sees it. That is right for the
447
+ // reporting token and wrong for a key the caller has to keep.
448
+ //
449
+ // Written after `m.eventMessage`, for the reason spelled out on the poll
450
+ // branch below: the blocks that attach `contextInfo` take `Object.keys(m)[0]`
451
+ // as the content key. `event` is not `Mentionable` or `Contextable` today,
452
+ // so this is ordering that stays correct rather than ordering that is
453
+ // currently load-bearing — but it costs nothing and the poll branch shows
454
+ // what getting it wrong looks like.
455
+ //
456
+ // Spread rather than upstream's wholesale assignment: nothing else writes
457
+ // `messageContextInfo` on this branch today, so the two cannot differ, and
458
+ // this cannot silently drop a field a later change puts there.
459
+ m.messageContextInfo = {
460
+ ...m.messageContextInfo,
461
+ messageSecret: message.event.messageSecret || randomBytes(32)
462
+ };
437
463
  }
438
464
  else if (hasNonNullishProperty(message, 'poll')) {
439
465
  (_a = message.poll).selectableCount || (_a.selectableCount = 0);
@@ -465,6 +491,29 @@ export const generateWAMessageContent = async (message, options) => {
465
491
  m.pollCreationMessage = pollCreationMessage;
466
492
  }
467
493
  }
494
+ // The key that encrypts the votes. `decryptPollVote` takes it as
495
+ // `pollEncKey`, and without it here the poll goes out fine but its own
496
+ // sender can never read the votes: the copy they store — the message
497
+ // `sendMessage` returns and emits as `messages.upsert` — is this object,
498
+ // and only `key.id` is patched from the relay result. WhatsApp Web writes it
499
+ // on the sender at creation too (`WAWebPollsSendPollCreationMsgAction`) and
500
+ // reuses the stored one for poll edits
501
+ // (`WAWebPollsGeneratePollEditMessageProto`).
502
+ //
503
+ // After the payload, not before, and that ordering is load-bearing: the
504
+ // `mentions` and `contextInfo` blocks below pick their target with
505
+ // `Object.keys(m)[0]`, so a `messageContextInfo` written first becomes the
506
+ // content key they attach to. `contextInfo` is not a field of
507
+ // `IMessageContextInfo`, so the encoder drops it and a poll sent with
508
+ // mentions loses them silently. `poll` is `Mentionable & Contextable`, so
509
+ // that is a combination callers can and do write. Upstream assigns it
510
+ // before the payload and has exactly that bug; measured on
511
+ // `{poll, mentions:['x@s.whatsapp.net']}`, its output carries no
512
+ // `mentionedJid` at all.
513
+ m.messageContextInfo = {
514
+ ...m.messageContextInfo,
515
+ messageSecret: message.poll.messageSecret || randomBytes(32)
516
+ };
468
517
  }
469
518
  else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
470
519
  m.protocolMessage = {
@@ -612,13 +661,54 @@ export const generateWAMessage = async (jid, content, options) => {
612
661
  : options;
613
662
  return generateWAMessageFromContent(jid, await generateWAMessageContent(content, contentOptions), options);
614
663
  };
664
+ /**
665
+ * The content keys that carry almost every message on a live socket, settled by
666
+ * identity before the structural predicate runs. Every name here already
667
+ * satisfies that predicate, so this only shortcuts the substring scan; it never
668
+ * changes which key wins.
669
+ */
670
+ const isCommonContentKey = (key) => {
671
+ switch (key) {
672
+ case 'conversation':
673
+ case 'extendedTextMessage':
674
+ case 'imageMessage':
675
+ case 'videoMessage':
676
+ case 'audioMessage':
677
+ case 'stickerMessage':
678
+ case 'documentMessage':
679
+ case 'reactionMessage':
680
+ case 'protocolMessage':
681
+ case 'pollCreationMessage':
682
+ return true;
683
+ default:
684
+ return false;
685
+ }
686
+ };
615
687
  /** Get the key to access the true type of content */
616
688
  export const getContentType = (content) => {
617
- if (content) {
618
- const keys = Object.keys(content);
619
- const key = keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
620
- return key;
689
+ if (!content) {
690
+ return undefined;
691
+ }
692
+ // The answer stays positional (the first own key naming content wins), but
693
+ // the scan is an indexed loop with an early return instead of `Array.find`,
694
+ // so no closure is allocated and the common keys never reach `includes`.
695
+ //
696
+ // Deliberately still `Object.keys` and not `for..in`: a decoded
697
+ // `WAProto.Message` inherits a default for every field of the schema, so
698
+ // `for..in` would enumerate ~100 prototype keys per call (and would have to
699
+ // filter them back out with `hasOwn` to stay positional) where `Object.keys`
700
+ // yields only the handful that were actually set.
701
+ const keys = Object.keys(content);
702
+ for (let i = 0; i < keys.length; i++) {
703
+ const key = keys[i];
704
+ if (isCommonContentKey(key)) {
705
+ return key;
706
+ }
707
+ if (key.includes('Message') && key !== 'senderKeyDistributionMessage') {
708
+ return key;
709
+ }
621
710
  }
711
+ return undefined;
622
712
  };
623
713
  const getFutureProofMessage = (message) => message?.ephemeralMessage ||
624
714
  message?.viewOnceMessage ||
@@ -24,19 +24,53 @@ export const isRealMessage = (message) => {
24
24
  !normalizedContent?.pollUpdateMessage);
25
25
  };
26
26
  export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
27
+ /**
28
+ * WhatsApp Web's rules, which upstream Baileys does not always follow.
29
+ *
30
+ * **A field with nothing to normalise stays absent.** `WAWebMsgKey` only assigns
31
+ * the participant it was given — `h !== void 0 && (this.participant = h)` — and
32
+ * builds the message's identity by joining the parts that are present, so an
33
+ * empty string there would change what the key serialises to. Upstream runs
34
+ * every field through `jidNormalizedUser`, which answers `''` for a jid it was
35
+ * not given, and so writes `participant: ''` onto every direct-message key.
36
+ * Matching that would mean copying a bug into the key handed back to callers.
37
+ *
38
+ * **A hosted jid is re-encoded onto its plain server**, and the fallback that
39
+ * used to keep `@hosted` when the user part was empty is gone. WA Web accepts
40
+ * hosted only as `<digits>:99@hosted` (`WAWebWidValidator`), and every jid that
41
+ * reached the old fallback is one it rejects outright — so there is no
42
+ * behaviour to preserve there, and agreeing with upstream costs nothing. Valid
43
+ * hosted jids already agreed.
44
+ */
27
45
  const normalizeMessageJid = (jid) => {
28
46
  if (!jid)
29
47
  return undefined;
30
48
  const hostedPn = isHostedPnUser(jid);
31
49
  if (!hostedPn && !isHostedLidUser(jid))
32
50
  return jidNormalizedUser(jid);
33
- const user = jidDecode(jid)?.user;
34
- return user ? jidEncode(user, hostedPn ? 's.whatsapp.net' : 'lid') : jidNormalizedUser(jid);
51
+ return jidEncode(jidDecode(jid)?.user ?? null, hostedPn ? 's.whatsapp.net' : 'lid');
35
52
  };
36
53
  /** Normalize device/hosted JIDs and nested reaction/poll keys in place. */
37
54
  export const cleanMessage = (message, meId, meLid) => {
38
- message.key.remoteJid = normalizeMessageJid(message.key.remoteJid);
39
- message.key.participant = normalizeMessageJid(message.key.participant);
55
+ // Normalise the fields that are there; do not add the ones that are not.
56
+ //
57
+ // Writing the result unconditionally put the property on the key even when
58
+ // there was nothing to normalise, so a direct-message key — which carries no
59
+ // participant — still gained one holding `undefined`, and `Object.keys` and a
60
+ // spread saw it. `WAWebMsgKey` assigns the participant it was given and
61
+ // leaves the key without one otherwise, and that absence is load-bearing
62
+ // there: it joins the parts that are present into the id the message is
63
+ // stored under.
64
+ //
65
+ // A field the caller did provide is normalised even when it comes back with
66
+ // nothing, which is the empty string upstream writes — they provided it, so
67
+ // it is theirs to have normalised, and leaving a `null` in place would only
68
+ // swap one falsy spelling for another.
69
+ for (const field of ['remoteJid', 'participant']) {
70
+ if (!Object.hasOwn(message.key, field) && message.key[field] == null)
71
+ continue;
72
+ message.key[field] = normalizeMessageJid(message.key[field]) ?? '';
73
+ }
40
74
  const content = normalizeMessageContent(message.message);
41
75
  if (content?.reactionMessage)
42
76
  normaliseKey(content.reactionMessage.key);
@@ -57,8 +57,52 @@ export const jidDecode = (jid) => {
57
57
  device: device ? +device : undefined
58
58
  };
59
59
  };
60
+ const CHAR_DEVICE_SEP = 58; // ':'
61
+ const CHAR_AGENT_SEP = 95; // '_'
62
+ /**
63
+ * Index at which the user component ends, given the index of the '@'.
64
+ *
65
+ * `jidDecode` derives the user by splitting the pre-`@` half on ':' and then on
66
+ * '_', so the user always runs to whichever of ':' or '_' comes first, and to
67
+ * the '@' when neither is there. Locating that boundary answers both callers
68
+ * below without the intermediate arrays and result object `jidDecode` allocates.
69
+ */
70
+ const userEndBefore = (jid, sepIdx) => {
71
+ for (let i = 0; i < sepIdx; i++) {
72
+ const code = jid.charCodeAt(i);
73
+ if (code === CHAR_DEVICE_SEP || code === CHAR_AGENT_SEP) {
74
+ return i;
75
+ }
76
+ }
77
+ return sepIdx;
78
+ };
79
+ /** As above, or -1 when the JID has no server part: the case `jidDecode` reports as `undefined`. */
80
+ const userEnd = (jid) => {
81
+ const sepIdx = jid.indexOf('@');
82
+ return sepIdx < 0 ? -1 : userEndBefore(jid, sepIdx);
83
+ };
60
84
  /** Compare the user component of two JIDs, matching upstream Baileys. */
61
- export const areJidsSameUser = (jid1, jid2) => jidDecode(jid1)?.user === jidDecode(jid2)?.user;
85
+ export const areJidsSameUser = (jid1, jid2) => {
86
+ // Same answer as comparing `jidDecode(...)?.user`, compared in place: this
87
+ // runs once per participant check on the message pipeline, and decoding both
88
+ // sides allocated four arrays and two objects only to throw them away.
89
+ const end = typeof jid1 === 'string' ? userEnd(jid1) : -1;
90
+ if (end !== (typeof jid2 === 'string' ? userEnd(jid2) : -1)) {
91
+ return false;
92
+ }
93
+ // Neither side decodes, so upstream compares `undefined === undefined`.
94
+ if (end < 0) {
95
+ return true;
96
+ }
97
+ const left = jid1;
98
+ const right = jid2;
99
+ for (let i = 0; i < end; i++) {
100
+ if (left.charCodeAt(i) !== right.charCodeAt(i)) {
101
+ return false;
102
+ }
103
+ }
104
+ return true;
105
+ };
62
106
  export const isJidMetaAI = (jid) => jid?.endsWith('@bot');
63
107
  export const isPnUser = (jid) => jid?.endsWith('@s.whatsapp.net');
64
108
  export const isLidUser = (jid) => jid?.endsWith('@lid');
@@ -71,12 +115,25 @@ export const isHostedPnUser = (jid) => jid?.endsWith('@hosted');
71
115
  const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/;
72
116
  export const isJidBot = (jid) => jid && botRegexp.test(jid.split('@')[0]) && jid.endsWith('@c.us');
73
117
  export const jidNormalizedUser = (jid) => {
74
- const result = jidDecode(jid);
75
- if (!result) {
118
+ if (typeof jid !== 'string') {
119
+ return '';
120
+ }
121
+ const sepIdx = jid.indexOf('@');
122
+ if (sepIdx < 0) {
76
123
  return '';
77
124
  }
78
- const { user, server } = result;
79
- return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : server);
125
+ // Everything between the user and the '@' is exactly what normalization
126
+ // drops, so the boundary is all this needs. `jidDecode` would split the same
127
+ // half twice and box the pieces in an object only for them to be re-joined.
128
+ const end = userEndBefore(jid, sepIdx);
129
+ // Fast path for the shape that dominates the pipeline: nothing to strip and a
130
+ // server `jidEncode` re-emits verbatim, so the JID already is its own normal
131
+ // form and no new string has to be built at all.
132
+ if (end === sepIdx && !jid.endsWith('@c.us')) {
133
+ return jid;
134
+ }
135
+ const server = jid.slice(sepIdx + 1);
136
+ return jidEncode(jid.slice(0, end), server === 'c.us' ? 's.whatsapp.net' : server);
80
137
  };
81
138
  export const transferDevice = (fromJid, toJid) => {
82
139
  const deviceId = jidDecode(fromJid)?.device || 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxidezap/baileyrs",
3
3
  "type": "module",
4
- "version": "0.2.7",
4
+ "version": "0.2.9",
5
5
  "description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
6
6
  "keywords": [
7
7
  "whatsapp",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "@hapi/boom": "^9.1.4",
85
- "@oxidezap/whatsapp-rust-bridge": "0.17.0",
85
+ "@oxidezap/whatsapp-rust-bridge": "0.18.0",
86
86
  "long": "^5.3.2",
87
87
  "pino": "^10.3.1",
88
88
  "protobufjs": "^7.6.5"