@oxidezap/baileyrs 0.2.5 → 0.2.6

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
@@ -226,8 +226,23 @@ A few behaviors that differ from upstream — almost always to your advantage:
226
226
  [When `connecting` lasts minutes](#when-connecting-lasts-minutes): a
227
227
  readiness timeout written for upstream's `connecting` misreads this one.
228
228
  - **No `getMessage` / `cachedGroupMetadata` polyfill required.** The Rust
229
- side caches group metadata and message keys natively. You can still pass
230
- them they're respected as overrides but they're optional.
229
+ side caches group metadata and message keys natively, and nothing here calls
230
+ either hook: a `cachedGroupMetadata` carried over from upstream never runs,
231
+ and neither does a `getMessage`. Passing them is harmless — the whole
232
+ `SocketConfig` shape is accepted so a migration needs no edits — but they are
233
+ not overrides, so delete them rather than maintain them. Every socket warns once
234
+ at construction naming any option in that group it received; that list is the
235
+ authority — the type system requires each `SocketConfig` member to be
236
+ classified, so it cannot fall behind — and it also covers `keepAliveIntervalMs`,
237
+ `markOnlineOnConnect`, `maxMsgRetryCount`, `msgRetryCounterCache`,
238
+ `retryRequestDelayMs`, `fireInitQueries`, `syncFullHistory`,
239
+ `shouldSyncHistoryMessage`, `generateHighQualityLinkPreview`,
240
+ `linkPreviewImageThumbnailWidth`, `enableAutoSessionRecreation`,
241
+ `enableRecentMessageCache`, `appStateMacVerification`,
242
+ `patchMessageBeforeSending`, `customUploadHosts`, `countryCode`,
243
+ `connectTimeoutMs`, `qrTimeout`, `printQRInTerminal`, `ignoreOfflineMessages`,
244
+ `downloadHistory`, `agent`, `fetchAgent`, `mobile`, `mediaCache`,
245
+ `userDevicesCache`, `callOfferCache` and `placeholderResendCache`.
231
246
  - **`Boom` ships in the box.** baileyrs exports its own
232
247
  `@hapi/boom`-compatible `Boom`, so the existing
233
248
  `(err as Boom).output.statusCode` pattern works unchanged. If your
@@ -161,6 +161,12 @@ const emitClose = ({ ctx, callbacks, historySync }, reason, statusCode, data) =>
161
161
  * state upstream itself uses while a connection is being (re)established, so
162
162
  * consumers read this as `open → connecting → open` and stay out of the way.
163
163
  */
164
+ /**
165
+ * `<stream:error code="429">` — the server rate limiting this session. The one
166
+ * stream error the engine forwards that is not a preserved connection; see the
167
+ * `streamError` dispatcher.
168
+ */
169
+ const RATE_LIMITED_STREAM_ERROR = '429';
164
170
  const emitRetrying = (ctx) => ctx.ev.emit('connection.update', {
165
171
  connection: 'connecting',
166
172
  // Same shape upstream's own `connecting` always carries. Without the
@@ -298,15 +304,49 @@ const DISPATCHERS = {
298
304
  const status = mapConnectFailureToDisconnect(evt.reason);
299
305
  emitClose(dispatchCtx, evt.message ?? 'Connection failure', status);
300
306
  },
301
- // NOT a close. The engine dispatches `StreamError` only from its catch-all
302
- // `<stream:error>` branch — an unknown code, an `<ack/>` it still owes the
303
- // server, or `<xml-not-well-formed>` — and it keeps or deliberately recycles
304
- // the connection in every one of those. This used to report
307
+ // NOT a close, in either branch below. This used to report
305
308
  // `DisconnectReason.badSession`, the code bots use to wipe credentials and
306
- // re-pair, so a routine stream recycle could destroy a working session. The
307
- // coded stream errors (401/409/515/516) never arrive here; they dispatch
308
- // `loggedOut` / `streamReplaced` of their own.
309
- streamError: (evt, { ctx }) => ctx.logger.warn({ code: evt.code }, 'stream error; the connection is preserved'),
309
+ // re-pair, so a routine stream recycle could destroy a working session.
310
+ //
311
+ // Two kinds of event arrive here, and they are not interchangeable:
312
+ //
313
+ // - the engine's catch-all `<stream:error>` branch — an unknown code, an
314
+ // `<ack/>` it still owes the server, or `<xml-not-well-formed>` — where
315
+ // it keeps or deliberately recycles the connection. Nothing to publish:
316
+ // the recycle reaches the consumer as a `disconnected` of its own.
317
+ //
318
+ // - `429`, which is a *rejected session*, not a preserved connection.
319
+ // `client/node_io.rs` clears `is_logged_in`, adds five rungs to the
320
+ // Fibonacci backoff and suppresses the reset that would undo them, then
321
+ // dispatches this event so an embedder can act on it — the core's comment
322
+ // says as much ("An embedder has none, so report the rate limit through
323
+ // `StreamError`"). It falls through the handler's `should_disconnect`
324
+ // block, so the transport is still up at this instant and the socket
325
+ // would keep reporting `open` until the server ends the stream.
326
+ //
327
+ // `connecting` for 429 is not a new state: it is the one this library
328
+ // already defines for "the engine is restoring the connection", and the one
329
+ // the next server frame produces anyway. Publishing it here just moves it to
330
+ // the moment the engine already knows, instead of the round trip that
331
+ // confirms it. The coded errors that *are* terminal (401/409/515/516) never
332
+ // reach this dispatcher; they emit `loggedOut` / `streamReplaced` instead.
333
+ streamError: (evt, { ctx, callbacks }) => {
334
+ if (evt.code === RATE_LIMITED_STREAM_ERROR) {
335
+ ctx.logger.warn({ code: evt.code }, 'stream error: the server rate limited this session; the engine will retry with an extended backoff');
336
+ // `connecting` promises the engine is restoring this connection.
337
+ // Under `setAutoReconnect(false)` it will not — the run loop breaks
338
+ // on the pass after the disconnect that follows — so publishing it
339
+ // would leave a retrying state nobody resolves. Silence, not a
340
+ // close: the engine still dispatches `Disconnected` when the server
341
+ // ends the stream, and that dispatcher already turns it into a
342
+ // terminal close under this flag. Closing here too would publish two
343
+ // for one failure, the second after teardown had already run.
344
+ if (callbacks?.isAutoReconnectEnabled?.() !== false)
345
+ emitRetrying(ctx);
346
+ return;
347
+ }
348
+ ctx.logger.warn({ code: evt.code }, 'stream error; the connection is preserved');
349
+ },
310
350
  streamReplaced: (_, dispatchCtx) => emitClose(dispatchCtx, 'Connection replaced', DisconnectReason.connectionReplaced),
311
351
  // Carries the wire code (405), not `DisconnectReason.badSession`. Upstream
312
352
  // does the same for stream errors — `+node.attrs.code` first, `badSession`
@@ -28,6 +28,7 @@ import { makeChatActionMethods } from './chat-actions.js';
28
28
  import { makeContactMethods } from './contacts.js';
29
29
  import { makeCommunityMethods } from './communities.js';
30
30
  import { makeBridgeClientOwner } from './bridge-client-owner.js';
31
+ import { warnUnsupportedConfig } from './unsupported-config.js';
31
32
  import { wrapBridgeClient } from './bridge-error-boundary.js';
32
33
  import { makeTerminalCloseReporter } from './terminal-close-reporter.js';
33
34
  import { makeEventHandlers } from './events.js';
@@ -74,6 +75,10 @@ const browserToPlatformType = (browser) => {
74
75
  const makeWASocket = (config) => {
75
76
  const fullConfig = { ...DEFAULT_CONNECTION_CONFIG, ...config };
76
77
  const { logger } = fullConfig;
78
+ // Against `config`, not `fullConfig`: only what this caller actually passed
79
+ // is worth naming. Merging the defaults first would report every unsupported
80
+ // option on every socket, including the ones nobody chose.
81
+ warnUnsupportedConfig(config, logger);
77
82
  const auth = normalizeSocketAuthenticationState(fullConfig.auth);
78
83
  const getExposedKeys = makeLazyTransactionKeyStore(auth.keys, logger, fullConfig.transactionOpts);
79
84
  const ev = makeEventBuffer(logger);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Options `SocketConfig` accepts that nothing on this side reads.
3
+ *
4
+ * Being a drop-in replacement means accepting the whole upstream shape, so none
5
+ * of these throws and none of them ever will. But the Rust engine owns
6
+ * keepalive, retry, history sync, link previews and its own message and group
7
+ * caches, so for these keys there is no reader to hand the value to — and a
8
+ * consumer that passes one is describing behaviour it will not get.
9
+ *
10
+ * That gap is expensive precisely because it is quiet. Two shapes found in a
11
+ * production bot, both dead for months, both looking deliberate:
12
+ *
13
+ * ```ts
14
+ * cachedGroupMetadata: async jid => myOwnCache.get(jid) // never called
15
+ * getMessage: async key => myStore.get(key.id) // never called
16
+ * ```
17
+ *
18
+ * `enableRecentMessageCache: true` is worse: it names a cache the consumer
19
+ * believes it turned on.
20
+ *
21
+ * The list is deliberately hand-maintained rather than derived. Deriving it
22
+ * (scanning for reads) would quietly go wrong in both directions — an option
23
+ * read through a computed key would look unsupported, and one only mentioned in
24
+ * a type would look supported. A test anchors every entry to `SocketConfig`, so
25
+ * an option that starts being read here fails the audit for whoever removes it.
26
+ */
27
+ export declare const UNSUPPORTED_CONFIG_KEYS: readonly ["keepAliveIntervalMs", "maxMsgRetryCount", "msgRetryCounterCache", "retryRequestDelayMs", "fireInitQueries", "markOnlineOnConnect", "syncFullHistory", "shouldSyncHistoryMessage", "generateHighQualityLinkPreview", "linkPreviewImageThumbnailWidth", "enableAutoSessionRecreation", "enableRecentMessageCache", "appStateMacVerification", "cachedGroupMetadata", "getMessage", "patchMessageBeforeSending", "customUploadHosts", "countryCode", "connectTimeoutMs", "qrTimeout", "agent", "fetchAgent", "mobile", "printQRInTerminal", "ignoreOfflineMessages", "downloadHistory", "mediaCache", "userDevicesCache", "callOfferCache", "placeholderResendCache"];
28
+ /**
29
+ * The other half: options something on this side actually reads.
30
+ *
31
+ * Only here to make the classification total. Nothing consumes this list at
32
+ * runtime; its job is to let the assertion below fail the build when a new
33
+ * member of `SocketConfig` belongs to neither list — which is exactly how the
34
+ * first version of the catalog shipped twelve keys short.
35
+ */
36
+ export declare const READ_CONFIG_KEYS: readonly ["waWebSocketUrl", "options", "logger", "version", "browser", "pushName", "auth", "cache", "deviceProps", "wantedPreKeyCount", "emitOwnEvents", "shouldIgnoreJid", "defaultQueryTimeoutMs", "transactionOpts", "makeSignalRepository"];
37
+ /**
38
+ * Which unsupported options this caller actually passed.
39
+ *
40
+ * Only own, non-`undefined` properties count: spreading a partial config
41
+ * commonly leaves `{ getMessage: undefined }` behind, and warning about a key
42
+ * whose value is absent would be noise about nothing.
43
+ *
44
+ * @param config the object handed to `makeWASocket`, before defaults are merged
45
+ * @returns the offending keys, in catalog order (stable across consumers)
46
+ */
47
+ export declare const unsupportedConfigKeys: (config: object) => string[];
48
+ /**
49
+ * Say once per socket, at construction, which unsupported options this caller
50
+ * passed.
51
+ *
52
+ * Per socket, not per process: a replacement socket built after a terminal
53
+ * close carries its own configuration decision, and `Socket/internals.ts`
54
+ * already warns "once per socket rather than per call" for its own no-ops.
55
+ *
56
+ * `warn` and not `error`: nothing is broken.
57
+ *
58
+ * A socket must never fail to build over a diagnostic. That guarantee is
59
+ * unconditional, so the guard covers the whole of it — the reading as well as
60
+ * the writing:
61
+ *
62
+ * - the optional chaining, for a logger with no `warn` (a minimal one in a
63
+ * test, say);
64
+ * - the `try` around the logging call, for a logger whose `warn` throws — a
65
+ * synchronous transport failing there would take down `makeWASocket` itself;
66
+ * - the same `try` around the inspection, because telling a passed option from
67
+ * a spread leftover means reading `config[key]`, and reading fires getters. A
68
+ * config with a throwing accessor, or a proxy with a throwing trap, would
69
+ * otherwise fail construction from outside the logging call.
70
+ *
71
+ * All of it only ever bites consumers who passed an inert option, which is the
72
+ * one group this warning exists to help. The failure is swallowed rather than
73
+ * reported, because the only thing left to report it with is the logger that
74
+ * just threw.
75
+ *
76
+ * @param config the object handed to `makeWASocket`, before defaults are merged
77
+ * @param logger the socket's logger
78
+ */
79
+ export declare const warnUnsupportedConfig: (config: object, logger: {
80
+ warn?: (payload: object, message: string) => void;
81
+ }) => void;
82
+ //# sourceMappingURL=unsupported-config.d.ts.map
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Options `SocketConfig` accepts that nothing on this side reads.
3
+ *
4
+ * Being a drop-in replacement means accepting the whole upstream shape, so none
5
+ * of these throws and none of them ever will. But the Rust engine owns
6
+ * keepalive, retry, history sync, link previews and its own message and group
7
+ * caches, so for these keys there is no reader to hand the value to — and a
8
+ * consumer that passes one is describing behaviour it will not get.
9
+ *
10
+ * That gap is expensive precisely because it is quiet. Two shapes found in a
11
+ * production bot, both dead for months, both looking deliberate:
12
+ *
13
+ * ```ts
14
+ * cachedGroupMetadata: async jid => myOwnCache.get(jid) // never called
15
+ * getMessage: async key => myStore.get(key.id) // never called
16
+ * ```
17
+ *
18
+ * `enableRecentMessageCache: true` is worse: it names a cache the consumer
19
+ * believes it turned on.
20
+ *
21
+ * The list is deliberately hand-maintained rather than derived. Deriving it
22
+ * (scanning for reads) would quietly go wrong in both directions — an option
23
+ * read through a computed key would look unsupported, and one only mentioned in
24
+ * a type would look supported. A test anchors every entry to `SocketConfig`, so
25
+ * an option that starts being read here fails the audit for whoever removes it.
26
+ */
27
+ export const UNSUPPORTED_CONFIG_KEYS = [
28
+ // The engine runs its own keepalive (WA Web's idle-ping + dead-socket
29
+ // watchdog) and its own Fibonacci reconnect ladder.
30
+ 'keepAliveIntervalMs',
31
+ // Retry of an undecryptable message is engine-side, keyed off its own store
32
+ // of sent messages; there is nothing for a consumer counter to count.
33
+ 'maxMsgRetryCount',
34
+ 'msgRetryCounterCache',
35
+ 'retryRequestDelayMs',
36
+ // The engine decides its own post-login queries.
37
+ 'fireInitQueries',
38
+ // Presence is explicit here: call `sendPresenceUpdate` after `open`.
39
+ 'markOnlineOnConnect',
40
+ // History sync is driven by the engine.
41
+ 'syncFullHistory',
42
+ 'shouldSyncHistoryMessage',
43
+ // Link previews are not generated on this side.
44
+ 'generateHighQualityLinkPreview',
45
+ 'linkPreviewImageThumbnailWidth',
46
+ // Session recreation and the recent-message cache are engine-side, and
47
+ // configured through `cache` (CacheConfig), not through these flags.
48
+ 'enableAutoSessionRecreation',
49
+ 'enableRecentMessageCache',
50
+ // App-state MAC verification happens in the engine.
51
+ 'appStateMacVerification',
52
+ // The engine resolves group metadata and sent messages from its own stores;
53
+ // these two are the ones consumers most often carry over from upstream and
54
+ // keep believing in.
55
+ 'cachedGroupMetadata',
56
+ 'getMessage',
57
+ // Upstream hooks with no counterpart on this side.
58
+ 'patchMessageBeforeSending',
59
+ 'customUploadHosts',
60
+ 'countryCode',
61
+ // Timeouts and transport knobs the engine owns. `connectTimeoutMs` is the
62
+ // one to point at: `Socket/internals.ts` already documented that it "reaches
63
+ // neither the transport nor the core", and it was still missing from the
64
+ // first version of this list. Proxy and TLS configuration goes through
65
+ // `options.dispatcher`, which is read — `agent` and `fetchAgent` are not.
66
+ 'connectTimeoutMs',
67
+ 'qrTimeout',
68
+ 'agent',
69
+ 'fetchAgent',
70
+ 'mobile',
71
+ // The QR is published on `connection.update`; drawing it is the consumer's.
72
+ 'printQRInTerminal',
73
+ // Offline delivery is the engine's, and history sync is driven by it too.
74
+ 'ignoreOfflineMessages',
75
+ 'downloadHistory',
76
+ // Caches the engine keeps natively, in Rust, keyed off its own stores.
77
+ 'mediaCache',
78
+ 'userDevicesCache',
79
+ 'callOfferCache',
80
+ 'placeholderResendCache'
81
+ ];
82
+ /**
83
+ * The other half: options something on this side actually reads.
84
+ *
85
+ * Only here to make the classification total. Nothing consumes this list at
86
+ * runtime; its job is to let the assertion below fail the build when a new
87
+ * member of `SocketConfig` belongs to neither list — which is exactly how the
88
+ * first version of the catalog shipped twelve keys short.
89
+ */
90
+ export const READ_CONFIG_KEYS = [
91
+ 'waWebSocketUrl',
92
+ 'options',
93
+ 'logger',
94
+ 'version',
95
+ 'browser',
96
+ 'pushName',
97
+ 'auth',
98
+ 'cache',
99
+ 'deviceProps',
100
+ 'wantedPreKeyCount',
101
+ 'emitOwnEvents',
102
+ 'shouldIgnoreJid',
103
+ 'defaultQueryTimeoutMs',
104
+ 'transactionOpts',
105
+ 'makeSignalRepository'
106
+ ];
107
+ const _everyConfigKeyIsClassified = true;
108
+ void _everyConfigKeyIsClassified;
109
+ /**
110
+ * Which unsupported options this caller actually passed.
111
+ *
112
+ * Only own, non-`undefined` properties count: spreading a partial config
113
+ * commonly leaves `{ getMessage: undefined }` behind, and warning about a key
114
+ * whose value is absent would be noise about nothing.
115
+ *
116
+ * @param config the object handed to `makeWASocket`, before defaults are merged
117
+ * @returns the offending keys, in catalog order (stable across consumers)
118
+ */
119
+ export const unsupportedConfigKeys = (config) => UNSUPPORTED_CONFIG_KEYS.filter(key => Object.hasOwn(config, key) && config[key] !== undefined);
120
+ /**
121
+ * Say once per socket, at construction, which unsupported options this caller
122
+ * passed.
123
+ *
124
+ * Per socket, not per process: a replacement socket built after a terminal
125
+ * close carries its own configuration decision, and `Socket/internals.ts`
126
+ * already warns "once per socket rather than per call" for its own no-ops.
127
+ *
128
+ * `warn` and not `error`: nothing is broken.
129
+ *
130
+ * A socket must never fail to build over a diagnostic. That guarantee is
131
+ * unconditional, so the guard covers the whole of it — the reading as well as
132
+ * the writing:
133
+ *
134
+ * - the optional chaining, for a logger with no `warn` (a minimal one in a
135
+ * test, say);
136
+ * - the `try` around the logging call, for a logger whose `warn` throws — a
137
+ * synchronous transport failing there would take down `makeWASocket` itself;
138
+ * - the same `try` around the inspection, because telling a passed option from
139
+ * a spread leftover means reading `config[key]`, and reading fires getters. A
140
+ * config with a throwing accessor, or a proxy with a throwing trap, would
141
+ * otherwise fail construction from outside the logging call.
142
+ *
143
+ * All of it only ever bites consumers who passed an inert option, which is the
144
+ * one group this warning exists to help. The failure is swallowed rather than
145
+ * reported, because the only thing left to report it with is the logger that
146
+ * just threw.
147
+ *
148
+ * @param config the object handed to `makeWASocket`, before defaults are merged
149
+ * @param logger the socket's logger
150
+ */
151
+ export const warnUnsupportedConfig = (config, logger) => {
152
+ try {
153
+ const options = unsupportedConfigKeys(config);
154
+ if (!options.length)
155
+ return;
156
+ logger?.warn?.({ options }, 'these options are accepted for upstream compatibility but nothing reads them here: the engine owns that behaviour');
157
+ }
158
+ catch {
159
+ // See above: nothing here is worth failing a socket over.
160
+ }
161
+ };
162
+ //# sourceMappingURL=unsupported-config.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxidezap/baileyrs",
3
3
  "type": "module",
4
- "version": "0.2.5",
4
+ "version": "0.2.6",
5
5
  "description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
6
6
  "keywords": [
7
7
  "whatsapp",