@violetix/baileys 0.3.18 → 0.5.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.
@@ -7,6 +7,8 @@ import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_C
7
7
  import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js';
8
8
  import { DisconnectReason, XWAPaths } from '../Types/index.js';
9
9
  import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, fetchLatestWaWebVersion, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, getPairingCodePlatform, invalidPairingCodeChars, makeEventBuffer, makeNoiseHandler, normalizePairingCode, PAIRING_CODE_ALPHABET, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
10
+ import { ignoredStanzaNeedsAck, makeIgnoreKeyRegistry } from '../Utils/ignore-key.js';
11
+ import { buildAckStanza } from '../Utils/stanza-ack.js';
10
12
  import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
11
13
  import { BinaryInfo } from '../WAM/BinaryInfo.js';
12
14
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
@@ -134,12 +136,13 @@ export const makeSocket = (config) => {
134
136
  node.attrs.id = generateMessageTag();
135
137
  }
136
138
  const msgId = node.attrs.id;
139
+ pendingQueries++;
137
140
  const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
138
141
  const result = waitForMessage(msgId, timeoutMs).catch(reject);
139
142
  sendNode(node)
140
143
  .then(async () => resolve(await result))
141
144
  .catch(reject);
142
- });
145
+ }).finally(() => { pendingQueries--; });
143
146
  if (result && 'tag' in result) {
144
147
  assertNodeErrorFree(result);
145
148
  }
@@ -374,6 +377,8 @@ export const makeSocket = (config) => {
374
377
  const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
375
378
  const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
376
379
  let lastDateRecv;
380
+ /** queries awaiting a reply; keep-alive skips its ping while these are flowing */
381
+ let pendingQueries = 0;
377
382
  let epoch = 1;
378
383
  let keepAliveReq;
379
384
  let qrTimer;
@@ -560,6 +565,11 @@ export const makeSocket = (config) => {
560
565
  // Don't throw - allow connection to continue even if pre-key check fails
561
566
  }
562
567
  };
568
+ const ignoreKeyRegistry = makeIgnoreKeyRegistry({
569
+ getMeId: () => authState.creds.me?.id,
570
+ getMeLid: () => authState.creds.me?.lid,
571
+ logger
572
+ });
563
573
  const onMessageReceived = async (data) => {
564
574
  await noise.decodeFrame(data, frame => {
565
575
  lastDateRecv = new Date();
@@ -570,6 +580,16 @@ export const makeSocket = (config) => {
570
580
  if (logger.level === 'trace') {
571
581
  logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
572
582
  }
583
+ // Filters run before every other handler, so a dropped stanza never reaches the
584
+ // decryption or persistence paths. The ack still goes out, otherwise the server
585
+ // keeps re-delivering what the host just asked to ignore.
586
+ if (ignoreKeyRegistry.shouldDrop(frame)) {
587
+ logger.debug({ tag: frame.tag, id: msgId, from: frame.attrs.from }, 'stanza dropped by ignoreKey');
588
+ if (ignoredStanzaNeedsAck(frame.tag)) {
589
+ sendNode(buildAckStanza(frame, undefined, authState.creds.me?.id)).catch(err => logger.error({ err, id: msgId }, 'failed to ack ignored stanza'));
590
+ }
591
+ return;
592
+ }
573
593
  anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
574
594
  const l0 = frame.tag;
575
595
  const l1 = frame.attrs || {};
@@ -694,48 +714,89 @@ export const makeSocket = (config) => {
694
714
  ws.off('error', onClose);
695
715
  });
696
716
  };
717
+ /**
718
+ * Keep-alive, modelled on zapo-js (src/transport/keepalive/WaKeepAlive.ts):
719
+ * - self-rescheduling setTimeout with +-10% jitter (min 250ms), so a fleet of sockets in one
720
+ * process drifts apart instead of pinging in the same tick
721
+ * - at most one ping in flight; a slow pong never stacks more pings on a busy loop
722
+ * - no ping while other queries are pending and data arrived recently: the link is
723
+ * demonstrably alive and the ping would only add queue pressure
724
+ * - "lost" is still decided by inbound silence (interval + grace), and skipped when the
725
+ * timer itself fired late (blocked event loop: pongs may be unread in the socket buffer)
726
+ */
697
727
  const startKeepAliveRequest = () => {
728
+ let timer;
729
+ let stopped = false;
730
+ let pingInFlight = false;
731
+ let lastCheckAt;
732
+ let expectedDelayMs = 0;
733
+ const nextDelay = () => {
734
+ const jitter = Math.max(250, Math.floor(keepAliveIntervalMs * 0.1));
735
+ return Math.max(1000, keepAliveIntervalMs + Math.floor((Math.random() * 2 - 1) * jitter));
736
+ };
737
+ const schedule = (delayMs) => {
738
+ if (stopped) {
739
+ return;
740
+ }
741
+ expectedDelayMs = delayMs;
742
+ timer = safeSetTimeout(() => {
743
+ timer = undefined;
744
+ cb();
745
+ schedule(nextDelay());
746
+ }, delayMs);
747
+ };
698
748
  const cb = () => {
699
749
  if (!lastDateRecv) {
700
750
  lastDateRecv = new Date();
701
751
  }
702
- const diff = Date.now() - lastDateRecv.getTime();
703
- if (diff > keepAliveIntervalMs + keepAliveGracePeriodMs) {
704
- void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
752
+ const now = Date.now();
753
+ const sinceRecvMs = now - lastDateRecv.getTime();
754
+ const lagMs = lastCheckAt ? now - lastCheckAt - expectedDelayMs : 0;
755
+ lastCheckAt = now;
756
+ const loopStalled = lagMs > keepAliveGracePeriodMs;
757
+ if (loopStalled) {
758
+ logger.warn({ lagMs, sinceLastRecvMs: sinceRecvMs }, 'event loop stalled; deferring keep-alive timeout');
705
759
  }
706
- else if (ws.isOpen) {
707
- query({
708
- tag: 'iq',
709
- attrs: {
710
- id: generateMessageTag(),
711
- to: S_WHATSAPP_NET,
712
- type: 'get',
713
- xmlns: 'w:p'
714
- },
715
- content: [{ tag: 'ping', attrs: {} }]
716
- }).catch(err => {
717
- logger.error({ trace: err.stack }, 'error in sending keep alive');
718
- });
760
+ if (sinceRecvMs > keepAliveIntervalMs + keepAliveGracePeriodMs && !loopStalled) {
761
+ void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
762
+ return;
719
763
  }
720
- else {
764
+ if (!ws.isOpen) {
721
765
  logger.warn('keep alive called when WS not open');
766
+ return;
767
+ }
768
+ if (pingInFlight) {
769
+ logger.trace('keep alive skipped: ping already in flight');
770
+ return;
722
771
  }
772
+ if (pendingQueries > 0 && sinceRecvMs < keepAliveGracePeriodMs) {
773
+ logger.trace({ pendingQueries, sinceRecvMs }, 'keep alive skipped: queries pending with recent inbound');
774
+ return;
775
+ }
776
+ pingInFlight = true;
777
+ query({
778
+ tag: 'iq',
779
+ attrs: {
780
+ id: generateMessageTag(),
781
+ to: S_WHATSAPP_NET,
782
+ type: 'get',
783
+ xmlns: 'w:p'
784
+ },
785
+ content: [{ tag: 'ping', attrs: {} }]
786
+ }, keepAliveIntervalMs + keepAliveGracePeriodMs)
787
+ .catch(err => {
788
+ // the verdict stays with inbound silence above; a failed ping alone is not "lost"
789
+ logger.warn({ trace: err.stack }, 'error in sending keep alive');
790
+ })
791
+ .finally(() => { pingInFlight = false; });
723
792
  };
724
- // Per socket, so keepAliveIntervalMs from the config is actually the ping interval: the
725
- // shared process-wide timer ran at a hardcoded 30s while the staleness check below still
726
- // used the configured value, so any interval other than 30s produced false connectionLost.
727
- // The random start offset staggers the fleet - on the shared timer every socket in the
728
- // process pinged in the same tick, which is a self-inflicted burst at 100+ sessions.
729
- let pinger;
730
- const starter = safeSetTimeout(() => {
731
- cb();
732
- pinger = safeSetInterval(cb, keepAliveIntervalMs);
733
- }, Math.floor(Math.random() * (keepAliveIntervalMs / 2)));
793
+ // random first tick staggers sockets that connect together (reconnect storms)
794
+ schedule(Math.floor(Math.random() * (keepAliveIntervalMs / 2)));
734
795
  keepAliveReq = {
735
796
  cb,
736
797
  cancel: () => {
737
- clearSafeTimer(starter);
738
- clearSafeTimer(pinger);
798
+ stopped = true;
799
+ clearSafeTimer(timer);
739
800
  }
740
801
  };
741
802
  };
@@ -1227,6 +1288,7 @@ export const makeSocket = (config) => {
1227
1288
  return authState.creds.me;
1228
1289
  },
1229
1290
  generateMessageTag,
1291
+ ignoreKey: ignoreKeyRegistry.register,
1230
1292
  query,
1231
1293
  waitForMessage,
1232
1294
  waitForSocketOpen,
@@ -9,7 +9,7 @@ import { writeFileSync, readFileSync, existsSync } from 'fs';
9
9
  import { WAProto } from '../Types/index.js';
10
10
  import { LabelAssociationType } from '../Types/LabelAssociation.js';
11
11
  import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
12
- import { md5, toNumber, updateMessageWithReceipt, updateMessageWithReaction } from '../Utils/index.js';
12
+ import { BufferJSON, md5, toNumber, updateMessageWithReceipt, updateMessageWithReaction } from '../Utils/index.js';
13
13
  import { jidDecode, jidNormalizedUser } from '../WABinary/index.js';
14
14
  import { makeOrderedDictionary } from './make-ordered-dictionary.js';
15
15
  import { ObjectRepository } from './object-repository.js';
@@ -69,6 +69,9 @@ export const makeInMemoryStore = (config = {}) => {
69
69
  }
70
70
  }
71
71
  };
72
+ // one key rule for every handler; upsert used alt+normalized while update/receipt/reaction/delete
73
+ // used raw or non-alt keys, so LID chats never found their stored messages
74
+ const keyJid = (key) => jidNormalizedUser(key.remoteJidAlt || key.remoteJid);
72
75
  const assertMessageList = (jid) => {
73
76
  if (!messages[jid]) {
74
77
  messages[jid] = makeMessagesDictionary();
@@ -116,8 +119,7 @@ export const makeInMemoryStore = (config = {}) => {
116
119
  }
117
120
  logger.debug({ deletedContacts: isLatest ? oldContacts.size : 0, newContacts }, 'synced contacts');
118
121
  for (const msg of newMessages) {
119
- const jid = msg.key.remoteJidAlt || msg.key.remoteJid;
120
- const list = assertMessageList(jid);
122
+ const list = assertMessageList(keyJid(msg.key));
121
123
  list.upsert(msg, 'prepend');
122
124
  }
123
125
  enforceLimits(messages, groupMetadata, chats, maxChats, maxMessagesPerChat);
@@ -149,9 +151,13 @@ export const makeInMemoryStore = (config = {}) => {
149
151
  }
150
152
  }
151
153
  else {
152
- return logger.debug({ update }, 'got update for non-existant contact');
154
+ logger.debug({ update }, 'got update for non-existant contact');
155
+ continue;
153
156
  }
154
- Object.assign(contacts[contact.id], contact);
157
+ // was Object.assign(contact, contact): update fields (notify, name...) never landed.
158
+ // id may be a WA_ADD_NOTIF hash and imgUrl a sentinel, so neither is copied.
159
+ const { id: _id, imgUrl: _imgUrl, ...fields } = update;
160
+ Object.assign(contact, fields);
155
161
  }
156
162
  });
157
163
  on('chats.upsert', newChats => {
@@ -211,7 +217,7 @@ export const makeInMemoryStore = (config = {}) => {
211
217
  case 'append':
212
218
  case 'notify':
213
219
  for (const msg of newMessages) {
214
- const jid = jidNormalizedUser(msg.key.remoteJidAlt || msg.key.remoteJid);
220
+ const jid = keyJid(msg.key);
215
221
  const list = assertMessageList(jid);
216
222
  list.upsert(msg, 'append');
217
223
  enforceLimits(messages, groupMetadata, chats, maxChats, maxMessagesPerChat);
@@ -231,7 +237,12 @@ export const makeInMemoryStore = (config = {}) => {
231
237
  on('messages.update', updates => {
232
238
  var _a;
233
239
  for (const { update, key } of updates) {
234
- const list = assertMessageList(jidNormalizedUser(key.remoteJid));
240
+ // no create: an update for an unknown chat must not add an empty list that evicts real ones
241
+ const list = messages[keyJid(key)];
242
+ if (!list) {
243
+ logger.debug({ update }, 'got update for non-existent message');
244
+ continue;
245
+ }
235
246
  if (update === null || update === void 0 ? void 0 : update.status) {
236
247
  const listStatus = (_a = list.get(key.id)) === null || _a === void 0 ? void 0 : _a.status;
237
248
  if (listStatus && (update === null || update === void 0 ? void 0 : update.status) <= listStatus) {
@@ -252,8 +263,7 @@ export const makeInMemoryStore = (config = {}) => {
252
263
  list === null || list === void 0 ? void 0 : list.clear();
253
264
  }
254
265
  else {
255
- const jid = item.keys[0].remoteJidAlt || item.keys[0].remoteJid;
256
- const list = messages[jid];
266
+ const list = messages[keyJid(item.keys[0])];
257
267
  if (list) {
258
268
  const idSet = new Set(item.keys.map(k => k.id));
259
269
  list.filter(m => !idSet.has(m.key.id));
@@ -308,8 +318,7 @@ export const makeInMemoryStore = (config = {}) => {
308
318
  });
309
319
  on('message-receipt.update', updates => {
310
320
  for (const { key, receipt } of updates) {
311
- const obj = messages[key.remoteJidAlt || key.remoteJid];
312
- const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
321
+ const msg = messages[keyJid(key)]?.get(key.id);
313
322
  if (msg) {
314
323
  updateMessageWithReceipt(msg, receipt);
315
324
  }
@@ -317,8 +326,7 @@ export const makeInMemoryStore = (config = {}) => {
317
326
  });
318
327
  on('messages.reaction', (reactions) => {
319
328
  for (const { key, reaction } of reactions) {
320
- const obj = messages[key.remoteJidAlt || key.remoteJid];
321
- const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
329
+ const msg = messages[keyJid(key)]?.get(key.id);
322
330
  if (msg) {
323
331
  updateMessageWithReaction(msg, reaction);
324
332
  }
@@ -363,28 +371,28 @@ export const makeInMemoryStore = (config = {}) => {
363
371
  bind,
364
372
  /** loads messages from the store, if not found -- uses the legacy connection */
365
373
  loadMessages: async (jid, count, cursor) => {
366
- const list = assertMessageList(jid);
374
+ const list = messages[jid]; // read-only: don't create phantom lists
367
375
  const mode = !cursor || 'before' in cursor ? 'before' : 'after';
368
376
  const cursorKey = !!cursor ? ('before' in cursor ? cursor.before : cursor.after) : undefined;
369
- const cursorValue = cursorKey ? list.get(cursorKey.id) : undefined;
370
- let messages;
377
+ const cursorValue = cursorKey ? list?.get(cursorKey.id) : undefined;
378
+ let result;
371
379
  if (list && mode === 'before' && (!cursorKey || cursorValue)) {
372
380
  if (cursorValue) {
373
381
  const msgIdx = list.array.findIndex(m => m.key.id === (cursorKey === null || cursorKey === void 0 ? void 0 : cursorKey.id));
374
- messages = list.array.slice(0, msgIdx);
382
+ result = list.array.slice(0, msgIdx);
375
383
  }
376
384
  else {
377
- messages = list.array;
385
+ result = list.array;
378
386
  }
379
- const diff = count - messages.length;
387
+ const diff = count - result.length;
380
388
  if (diff < 0) {
381
- messages = messages.slice(-count); // get the last X messages
389
+ result = result.slice(-count); // get the last X messages
382
390
  }
383
391
  }
384
392
  else {
385
- messages = [];
393
+ result = [];
386
394
  }
387
- return messages;
395
+ return result;
388
396
  },
389
397
  /**
390
398
  * Get all available labels for profile
@@ -447,13 +455,13 @@ export const makeInMemoryStore = (config = {}) => {
447
455
  toJSON,
448
456
  fromJSON,
449
457
  writeToFile: (path) => {
450
- writeFileSync(path, JSON.stringify(toJSON()));
458
+ writeFileSync(path, JSON.stringify(toJSON(), BufferJSON.replacer));
451
459
  },
452
460
  readFromFile: (path) => {
453
461
  if (existsSync(path)) {
454
462
  logger.debug({ path }, 'reading from file');
455
463
  const jsonStr = readFileSync(path, { encoding: 'utf-8' });
456
- const json = JSON.parse(jsonStr);
464
+ const json = JSON.parse(jsonStr, BufferJSON.reviver);
457
465
  fromJSON(json);
458
466
  }
459
467
  }
@@ -0,0 +1,50 @@
1
+ export type A2UIWidget = {
2
+ uuid: string;
3
+ data: string;
4
+ type: string;
5
+ fallback?: string;
6
+ };
7
+ export type A2UIBuildOptions = {
8
+ uuid?: string;
9
+ surfaceId?: string;
10
+ type?: string;
11
+ /** `false` emits a bare `{ components }` payload instead of the `createSurface` envelope. */
12
+ wrapped?: boolean;
13
+ };
14
+ export type A2UIChoice = {
15
+ label: string;
16
+ value: unknown;
17
+ };
18
+ export type A2UIListCardItem = {
19
+ title: string;
20
+ price?: string;
21
+ trailingLabel?: string;
22
+ assetId?: string;
23
+ assetType?: string;
24
+ emphasis?: string;
25
+ };
26
+ /** Every method registers a component and returns its id; container methods take those ids. */
27
+ export declare class A2UI {
28
+ constructor(options?: { catalogId?: string; version?: string });
29
+ text(text: string, options?: { id?: string; variant?: string }): string;
30
+ image(url: string, options?: { id?: string; variant?: string; fit?: string }): string;
31
+ video(url: string, options?: { id?: string }): string;
32
+ checkbox(label: string, options?: { id?: string; value?: boolean }): string;
33
+ textField(label: string, options?: { id?: string; variant?: string }): string;
34
+ button(childId: string, options?: { id?: string; variant?: string; action?: any }): string;
35
+ card(childId: string, options?: { id?: string }): string;
36
+ column(children: string[], options?: { id?: string; justify?: string; align?: string }): string;
37
+ row(children: string[], options?: { id?: string }): string;
38
+ divider(options?: { id?: string }): string;
39
+ choicePicker(label: string, options: A2UIChoice[], extra?: {
40
+ id?: string;
41
+ variant?: string;
42
+ value?: unknown;
43
+ displayStyle?: string;
44
+ filterable?: boolean;
45
+ }): string;
46
+ root(children: string[]): this;
47
+ listCard(input: { title: string; items: A2UIListCardItem[]; fallbackText?: string; uuid?: string }): this;
48
+ build(options?: A2UIBuildOptions): A2UIWidget;
49
+ }
50
+ export declare const buildA2UIWidget: (a2ui: A2UI | A2UIWidget, options?: A2UIBuildOptions) => A2UIWidget;
@@ -0,0 +1,168 @@
1
+ // A2UI (Agent-to-UI) widget builder. Declarative components — Text, Image, Video, Button,
2
+ // Card, Column, Row, Divider, CheckBox, TextField, ChoicePicker, plus the `listCard` shortcut
3
+ // — compiled into the JSON payload WhatsApp carries in `interactiveMessage.bloksWidget`.
4
+ //
5
+ // Ported from @rennzsync/baileys (MIT).
6
+ //
7
+ // Every builder method registers a component and returns its id; ids are what the container
8
+ // methods take, so a tree is written bottom-up:
9
+ //
10
+ // const ui = new A2UI()
11
+ // const title = ui.text('Hello', { variant: 'h1' })
12
+ // const button = ui.button(ui.text('Press me'), { action: { name: 'noop' } })
13
+ // ui.root([ui.card(ui.column([title, button]))])
14
+ // await sock.sendMessage(jid, { a2ui: ui, text: 'Widget' })
15
+ //
16
+ // This is an internal WhatsApp format: a send can succeed while the recipient's client renders
17
+ // nothing. Verify against your target client build before relying on it.
18
+ import { randomUUID } from 'crypto';
19
+ const DEFAULT_CATALOG_ID = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
20
+ export class A2UI {
21
+ constructor({ catalogId = DEFAULT_CATALOG_ID, version = 'v0.9' } = {}) {
22
+ this._version = version;
23
+ this._catalogId = catalogId;
24
+ this._components = new Map();
25
+ this._counter = 0;
26
+ this._rootChildren = [];
27
+ this._listCardPayload = undefined;
28
+ }
29
+ #nextId(prefix) {
30
+ return `${prefix}_${(this._counter++).toString(36)}`;
31
+ }
32
+ #reg(id, component, extra = {}) {
33
+ id ??= this.#nextId(component.toLowerCase());
34
+ if (this._components.has(id)) {
35
+ throw new Error(`Component id "${id}" already used`);
36
+ }
37
+ this._components.set(id, { id, component, ...extra });
38
+ return id;
39
+ }
40
+ text(text, { id, variant = 'body' } = {}) {
41
+ return this.#reg(id, 'Text', { text, variant });
42
+ }
43
+ image(url, { id, variant, fit = 'cover' } = {}) {
44
+ return this.#reg(id, 'Image', { url, ...(variant ? { variant } : {}), fit });
45
+ }
46
+ video(url, { id } = {}) {
47
+ return this.#reg(id, 'Video', { url });
48
+ }
49
+ checkbox(label, { id, value = false } = {}) {
50
+ return this.#reg(id, 'CheckBox', { label, value });
51
+ }
52
+ textField(label, { id, variant = 'text' } = {}) {
53
+ return this.#reg(id, 'TextField', { label, variant });
54
+ }
55
+ button(childId, { id, variant = 'primary', action } = {}) {
56
+ if (!childId) {
57
+ throw new TypeError('button(childId) requires the id of a child component (e.g. from .text())');
58
+ }
59
+ return this.#reg(id, 'Button', { child: childId, variant, ...(action ? { action } : {}) });
60
+ }
61
+ card(childId, { id } = {}) {
62
+ if (!childId) {
63
+ throw new TypeError('card(childId) requires the id of a child component');
64
+ }
65
+ return this.#reg(id, 'Card', { child: childId });
66
+ }
67
+ column(children = [], { id, justify, align } = {}) {
68
+ if (!children.length) {
69
+ throw new TypeError('column(children) requires at least one child id');
70
+ }
71
+ return this.#reg(id, 'Column', { children, ...(justify ? { justify } : {}), ...(align ? { align } : {}) });
72
+ }
73
+ row(children = [], { id } = {}) {
74
+ if (!children.length) {
75
+ throw new TypeError('row(children) requires at least one child id');
76
+ }
77
+ return this.#reg(id, 'Row', { children });
78
+ }
79
+ divider({ id } = {}) {
80
+ return this.#reg(id, 'Divider', {});
81
+ }
82
+ choicePicker(label, options, { id, variant = 'mutuallyExclusive', value, displayStyle = 'checkbox', filterable = false } = {}) {
83
+ if (!Array.isArray(options) || !options.length) {
84
+ throw new TypeError('choicePicker(label, options) requires a non-empty options array of {label, value}');
85
+ }
86
+ return this.#reg(id, 'ChoicePicker', {
87
+ label,
88
+ variant,
89
+ ...(value !== undefined ? { value } : {}),
90
+ options,
91
+ displayStyle,
92
+ filterable
93
+ });
94
+ }
95
+ /** Names the top-level components. Required before `build()` unless `listCard` was used. */
96
+ root(children) {
97
+ if (!Array.isArray(children) || !children.length) {
98
+ throw new TypeError('root(children) requires a non-empty array of top-level component ids');
99
+ }
100
+ this._rootChildren = children;
101
+ return this;
102
+ }
103
+ /**
104
+ * Shortcut for the product-list widget, which uses its own payload shape rather than the
105
+ * component catalog. It replaces whatever the component methods built — `build()` returns
106
+ * this payload and ignores `root()`.
107
+ */
108
+ listCard({ title, items, fallbackText, uuid = randomUUID() } = {}) {
109
+ if (!title) {
110
+ throw new TypeError('listCard requires a title');
111
+ }
112
+ if (!Array.isArray(items) || !items.length) {
113
+ throw new TypeError('listCard requires a non-empty items array');
114
+ }
115
+ this._listCardPayload = {
116
+ uuid,
117
+ data: JSON.stringify({
118
+ type: 'list_card',
119
+ title,
120
+ fallback_text: fallbackText ?? '',
121
+ items: items.map(item => ({
122
+ asset_id: item.assetId ?? randomUUID().replace(/-/g, '').slice(0, 17),
123
+ asset_type: item.assetType ?? 'PRODUCT_ITEM',
124
+ title: item.title,
125
+ trailing_label: item.price ?? item.trailingLabel ?? '',
126
+ trailing_emphasis: item.emphasis ?? 'strong'
127
+ }))
128
+ }),
129
+ type: 'im_a2ui',
130
+ fallback: fallbackText ?? ''
131
+ };
132
+ return this;
133
+ }
134
+ /** Compiles the tree into the `bloksWidget` payload. Called for you by `sendMessage`. */
135
+ build({ uuid = randomUUID(), surfaceId, type = 'im_a2ui', wrapped = true } = {}) {
136
+ if (this._listCardPayload) {
137
+ return this._listCardPayload;
138
+ }
139
+ if (!this._rootChildren.length) {
140
+ throw new Error('Call root([...ids]) before build()');
141
+ }
142
+ const root = { id: 'root', component: 'Column', children: this._rootChildren };
143
+ const components = [root, ...this._components.values()];
144
+ const data = wrapped
145
+ ? {
146
+ version: this._version,
147
+ createSurface: {
148
+ surfaceId: surfaceId ?? `starcore-widget=${uuid}`,
149
+ catalogId: this._catalogId,
150
+ components
151
+ }
152
+ }
153
+ : { components };
154
+ return {
155
+ uuid,
156
+ data: JSON.stringify(data),
157
+ type
158
+ };
159
+ }
160
+ }
161
+ /** Accepts an `A2UI` instance or an already-built payload, and returns the payload. */
162
+ export const buildA2UIWidget = (a2ui, options = {}) => {
163
+ const widget = typeof a2ui?.build === 'function' ? a2ui.build(options) : a2ui;
164
+ if (!widget || typeof widget.data !== 'string') {
165
+ throw new TypeError('a2ui must be an A2UI instance or a built widget payload with a `data` string');
166
+ }
167
+ return widget;
168
+ };
@@ -0,0 +1,71 @@
1
+ export type AnchorGuardLimits = {
2
+ maxText: number;
3
+ maxCaption: number;
4
+ maxMentions: number;
5
+ maxGroupMentions: number;
6
+ maxButtons: number;
7
+ maxSections: number;
8
+ maxRows: number;
9
+ maxCards: number;
10
+ maxDepth: number;
11
+ maxNodes: number;
12
+ maxBytes: number;
13
+ maxParamsJson: number;
14
+ maxInvisibleRun: number;
15
+ maxCombiningRun: number;
16
+ maxNewlines: number;
17
+ };
18
+ export declare const ANCHORGUARD_DEFAULTS: AnchorGuardLimits;
19
+ export type AnchorGuardDetectOptions = Partial<AnchorGuardLimits> & {
20
+ /** Pre-computed encoded size, checked against `maxBytes` without re-encoding. */
21
+ byteLength?: number;
22
+ /** Pass the `proto` namespace to also measure the encoded size. Costs an encode per check. */
23
+ proto?: any;
24
+ };
25
+ export type AnchorGuardDetectResult = {
26
+ flagged: boolean;
27
+ reasons: string[];
28
+ };
29
+ export declare const detectBug: (message: any, options?: AnchorGuardDetectOptions) => AnchorGuardDetectResult;
30
+ export type AnchorGuardDetection = {
31
+ direction: 'incoming' | 'outgoing' | 'group-add';
32
+ jid?: string;
33
+ sender?: string;
34
+ message?: any;
35
+ content?: any;
36
+ reasons: string[];
37
+ burst?: boolean;
38
+ };
39
+ export type AnchorGuardOptions = {
40
+ /** Delete a flagged inbound message. Default `true`. */
41
+ autoDelete?: boolean;
42
+ /** `'auto'` revokes own messages and deletes others locally. Default `'auto'`. */
43
+ deleteMode?: 'auto' | 'everyone' | 'me';
44
+ /** Guard `messages.upsert`. Default `true`. */
45
+ guardIncoming?: boolean;
46
+ /** Wrap `sock.sendMessage` and reject crash payloads. Default `false`. */
47
+ guardOutgoing?: boolean;
48
+ /** Block the sender of a flagged message. Default `false`. */
49
+ blockOnBug?: boolean;
50
+ /** Only guard own chats. Default `false`. */
51
+ selfOnly?: boolean;
52
+ thresholds?: Partial<AnchorGuardLimits>;
53
+ onDetect?: (detection: AnchorGuardDetection) => void | Promise<void>;
54
+ proto?: any;
55
+ /** Messages from one sender inside `burstWindowMs` before it counts as a burst. `0` disables. */
56
+ burstThreshold?: number;
57
+ burstWindowMs?: number;
58
+ /** Remove the sender from the group on burst. Needs admin. Default `false`. */
59
+ kickOnBurst?: boolean;
60
+ /** Flag/kick when a burst-flagged inviter adds this account to a group. Default `false`. */
61
+ guardGroupAdds?: boolean;
62
+ /** Guard Meta AI's numbers instead of exempting them. Default `false`. */
63
+ metaAiNumbers?: boolean;
64
+ ownJid?: string;
65
+ logger?: any;
66
+ };
67
+ export declare const createAnchorGuard: (sock: any, options?: AnchorGuardOptions) => {
68
+ detect: (message: any) => AnchorGuardDetectResult;
69
+ stop: () => void;
70
+ };
71
+ export declare const createAntiBugGuard: typeof createAnchorGuard;