@oxidezap/baileyrs 0.0.35 → 0.1.1

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.
Files changed (44) hide show
  1. package/README.md +147 -24
  2. package/lib/Compatibility/legacy-store/namespaces.d.ts +20 -0
  3. package/lib/Compatibility/legacy-store/namespaces.js +27 -0
  4. package/lib/Compatibility/newsletter-results.d.ts +15 -0
  5. package/lib/Compatibility/newsletter-results.js +38 -0
  6. package/lib/Compatibility/proto-runtime.js +30 -20
  7. package/lib/Compatibility/websocket-client.d.ts +23 -2
  8. package/lib/Compatibility/websocket-client.js +47 -18
  9. package/lib/Socket/bridge-client-owner.d.ts +89 -0
  10. package/lib/Socket/bridge-client-owner.js +135 -0
  11. package/lib/Socket/business.d.ts +29 -0
  12. package/lib/Socket/business.js +104 -0
  13. package/lib/Socket/chat-actions.d.ts +20 -11
  14. package/lib/Socket/chat-actions.js +171 -83
  15. package/lib/Socket/events.d.ts +31 -0
  16. package/lib/Socket/events.js +144 -41
  17. package/lib/Socket/index.d.ts +113 -16
  18. package/lib/Socket/index.js +468 -157
  19. package/lib/Socket/internals.d.ts +88 -0
  20. package/lib/Socket/internals.js +145 -0
  21. package/lib/Socket/messages.d.ts +1 -12
  22. package/lib/Socket/messages.js +3 -20
  23. package/lib/Socket/newsletter.d.ts +61 -6
  24. package/lib/Socket/newsletter.js +125 -7
  25. package/lib/Socket/privacy.d.ts +25 -0
  26. package/lib/Socket/privacy.js +54 -0
  27. package/lib/Socket/server-queries.d.ts +38 -0
  28. package/lib/Socket/server-queries.js +121 -0
  29. package/lib/Socket/terminal-close-reporter.d.ts +79 -0
  30. package/lib/Socket/terminal-close-reporter.js +108 -0
  31. package/lib/Socket/terminal-close.d.ts +39 -0
  32. package/lib/Socket/terminal-close.js +51 -0
  33. package/lib/Socket/types.d.ts +6 -0
  34. package/lib/Types/Product.d.ts +9 -0
  35. package/lib/Utils/event-buffer.js +31 -0
  36. package/lib/Utils/index.d.ts +1 -0
  37. package/lib/Utils/index.js +3 -0
  38. package/lib/Utils/link-preview.d.ts +60 -0
  39. package/lib/Utils/link-preview.js +357 -0
  40. package/lib/Utils/messages.d.ts +31 -7
  41. package/lib/Utils/messages.js +49 -17
  42. package/lib/Utils/wrap-legacy-store.d.ts +1 -0
  43. package/lib/Utils/wrap-legacy-store.js +1 -0
  44. package/package.json +4 -2
@@ -0,0 +1,121 @@
1
+ import { Boom } from '../Utils/boom.js';
2
+ /**
3
+ * Every section, flattened and deduplicated, rather than only the section the
4
+ * server types `all`.
5
+ *
6
+ * Upstream reads that one section, and the core deliberately refused to: the
7
+ * real client walks every section and uses the type only for layout, so a bot
8
+ * that appears solely under a category is dropped by the narrower reading. A
9
+ * caller iterating this list handles the extra entries; one that silently lost
10
+ * a bot has no way to notice.
11
+ */
12
+ const flattenBotList = (list) => {
13
+ const seen = new Set();
14
+ const bots = [];
15
+ for (const section of list.sections) {
16
+ for (const bot of section.bots) {
17
+ if (seen.has(bot.jid))
18
+ continue;
19
+ seen.add(bot.jid);
20
+ bots.push({ jid: bot.jid, personaId: bot.personaId });
21
+ }
22
+ }
23
+ return bots;
24
+ };
25
+ /**
26
+ * Upstream names these in snake case and types the three timestamps as
27
+ * strings. Every field is optional because the server omits whatever does not
28
+ * apply to the account's tier, and an absent quota stays absent: `0` here means
29
+ * the quota is spent.
30
+ */
31
+ const toCapInfo = (result) => ({
32
+ ...(result.totalQuota !== undefined ? { total_quota: result.totalQuota } : {}),
33
+ ...(result.usedQuota !== undefined ? { used_quota: result.usedQuota } : {}),
34
+ ...(result.remainingQuota !== undefined ? { remaining_quota: result.remainingQuota } : {}),
35
+ ...(result.cycleStartTimestamp !== undefined ? { cycle_start_timestamp: String(result.cycleStartTimestamp) } : {}),
36
+ ...(result.cycleEndTimestamp !== undefined ? { cycle_end_timestamp: String(result.cycleEndTimestamp) } : {}),
37
+ ...(result.serverSentTimestamp !== undefined ? { server_sent_timestamp: String(result.serverSentTimestamp) } : {}),
38
+ ...(result.oteStatus !== undefined ? { ote_status: result.oteStatus } : {}),
39
+ ...(result.mvStatus !== undefined ? { mv_status: result.mvStatus } : {}),
40
+ ...(result.cappingStatus !== undefined
41
+ ? { capping_status: result.cappingStatus }
42
+ : {})
43
+ });
44
+ export const makeServerQueryMethods = (ctx) => {
45
+ /** Last host seen, so the synchronous accessor upstream exposes can answer. */
46
+ let mediaHost = '';
47
+ /**
48
+ * When the credentials were obtained, not when they were last handed out.
49
+ * The engine serves a live connection from its cache and gives no signal
50
+ * for which calls were actual fetches, so the stamp is kept only while the
51
+ * connection it describes is still live: past its own ttl, on a forced
52
+ * call, or on rotated credentials, whatever comes back is a fetch.
53
+ *
54
+ * A caller renewing on `fetchDate + ttl` needs both halves of that. Always
55
+ * restamping would push its deadline forward forever; never restamping
56
+ * would leave it renewing against a moment that has already passed.
57
+ */
58
+ let fetched;
59
+ const isLive = (held) => Date.now() - held.at.getTime() < held.ttl * 1000;
60
+ return {
61
+ /**
62
+ * `maxContentLengthBytes` is absent by design: the core's hosts carry
63
+ * nothing but a hostname, so the field upstream declares has no source
64
+ * and is not invented here.
65
+ */
66
+ refreshMediaConn: async (forceGet = false) => {
67
+ // Forced on the first call, as upstream's first call is: the engine may
68
+ // already hold a connection acquired by an upload, and stamping that
69
+ // one as fetched now would report it fresher than it is.
70
+ const held = fetched;
71
+ const conn = await (await ctx.getClient()).getMediaConn(forceGet || !held);
72
+ mediaHost = conn.hosts[0]?.hostname ?? mediaHost;
73
+ const isFetch = forceGet || !held || held.auth !== conn.auth || !isLive(held);
74
+ fetched = isFetch ? { auth: conn.auth, ttl: conn.ttl, at: new Date() } : held;
75
+ // A copy: the stored instant decides when the next call restamps, and a
76
+ // consumer holding the same Date could move it.
77
+ return { auth: conn.auth, ttl: conn.ttl, hosts: conn.hosts, fetchDate: new Date(fetched.at) };
78
+ },
79
+ /** Synchronous, as upstream has it, so it reads what the last refresh saw. */
80
+ getMediaHost: () => mediaHost,
81
+ getBotListV2: async () => {
82
+ return flattenBotList(await (await ctx.getClient()).getBotList());
83
+ },
84
+ fetchNewChatMessageCap: async () => {
85
+ return toCapInfo(await (await ctx.getClient()).fetchNewChatMessageCappingInfo());
86
+ },
87
+ cleanDirtyBits: async (type, fromTimestamp) => {
88
+ let timestamp = null;
89
+ if (fromTimestamp !== undefined) {
90
+ // A blank string is not a timestamp, and `Number('')` is the epoch,
91
+ // so without this a caller meaning "no timestamp" would ask the
92
+ // server to clean from the beginning of time.
93
+ const blank = typeof fromTimestamp === 'string' && fromTimestamp.trim() === '';
94
+ timestamp = typeof fromTimestamp === 'string' ? Number(fromTimestamp) : fromTimestamp;
95
+ if (blank || !Number.isFinite(timestamp)) {
96
+ throw new Boom(`cleanDirtyBits: fromTimestamp '${fromTimestamp}' is not a number`, { statusCode: 400 });
97
+ }
98
+ }
99
+ await (await ctx.getClient()).cleanDirtyBits(type, timestamp);
100
+ },
101
+ /**
102
+ * Refused rather than wired up. The core already fires a peer data
103
+ * request itself when a message fails to decrypt, with its own age
104
+ * policy, so a second one here would duplicate it. The request a
105
+ * consumer actually drives, asking for history, is `fetchMessageHistory`.
106
+ */
107
+ sendPeerDataOperationMessage: async (_pdoMessage) => {
108
+ throw new Boom('sendPeerDataOperationMessage is not supported: use fetchMessageHistory to request history, and note the engine issues its own peer data request when a message fails to decrypt', { statusCode: 501 });
109
+ },
110
+ /**
111
+ * Refused one layer down, as a build decision. `create_call_link` exists
112
+ * in the core behind its voip feature, and the bridge pins the core with
113
+ * default features off, so it is not compiled into the wasm artifact at
114
+ * all. Reaching it would pull the webrtc stack into the bundle.
115
+ */
116
+ createCallLink: async (_type, _event, _timeoutMs) => {
117
+ throw new Boom('createCallLink is not available: the call-link operation sits behind the core voip feature, which is not compiled into the wasm bridge', { statusCode: 501 });
118
+ }
119
+ };
120
+ };
121
+ //# sourceMappingURL=server-queries.js.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Reports the terminal `connection.update { close }` — exactly once, after
3
+ * teardown, and never silently not at all.
4
+ *
5
+ * That sentence is the whole contract this branch sells, and getting it wrong
6
+ * has two opposite failure modes, both bad:
7
+ *
8
+ * - **Not reported.** The consumer's handler never runs, so it never builds a
9
+ * replacement socket. A bot offline with nothing in its logs — the original
10
+ * bug this branch exists to fix.
11
+ * - **Reported early, or twice.** The replacement socket overlaps the old
12
+ * one's auth-store flush and `free()`, or every listener sees two terminal
13
+ * notifications for one socket and a handler that cleans up on close loops.
14
+ *
15
+ * Keeping both away used to be inline logic split across the dispatcher hook
16
+ * and `logout()`, sharing a counter and a promise. Nine separate bugs came out
17
+ * of that in review — a publish that could never run, a throwing listener
18
+ * taking the process down, `logout()` resolving before the close it caused,
19
+ * the watchdog not releasing its waiters, a suppression check that also
20
+ * matched non-terminal teardown, a fallback that did not count itself. Every
21
+ * one was a different way of answering the same question, so it is one object
22
+ * now, with the answer in one place.
23
+ */
24
+ import type { ILogger } from '../Utils/logger.js';
25
+ /**
26
+ * How long teardown may run before the close is reported anyway.
27
+ *
28
+ * A deliberate trade of one guarantee for the other: past this point the close
29
+ * goes out with teardown still running, because losing the event entirely is
30
+ * the worse of the two failures. Teardown takes milliseconds in practice, so
31
+ * reaching this means a consumer end handler or a store flush never settled —
32
+ * and the error logged alongside says exactly that. Generous rather than
33
+ * tight, since firing early is the harmful direction.
34
+ */
35
+ export declare const TERMINAL_CLOSE_PUBLISH_TIMEOUT_MS = 60000;
36
+ export interface TerminalCloseReporter {
37
+ /**
38
+ * Run `teardown`, then publish. Used by the dispatcher, which learns about
39
+ * the close first and hands over the publishing so the socket can finish
40
+ * releasing its resources before a consumer can react.
41
+ *
42
+ * Never rejects and never leaves `publish` uncalled: teardown failures are
43
+ * logged, a throwing listener is contained, and a teardown that hangs is
44
+ * cut short by the watchdog.
45
+ */
46
+ reportAfter: (teardown: () => Promise<void>, publish: () => void) => void;
47
+ /**
48
+ * Publish immediately, for a close nothing else will announce — a `logout()`
49
+ * with no live client, or one whose `logout()` threw before the bridge
50
+ * dispatched anything.
51
+ */
52
+ reportNow: (publish: () => void) => void;
53
+ /**
54
+ * Whether a terminal close has been *claimed* — reported, or reported-and-
55
+ * still-tearing-down. Not "published": `reportAfter` publishes only once
56
+ * teardown finishes, so a check keyed on delivery would see `false` right
57
+ * after the bridge announced the logout and let `logout()` add a second
58
+ * close of its own.
59
+ *
60
+ * `logout()` uses this rather than "is the socket closing", which is also
61
+ * true for a plain `end()` — and those report nothing, so keying off them
62
+ * would let a logout racing one finish with no close at all.
63
+ */
64
+ hasReported: () => boolean;
65
+ /**
66
+ * Settles once the most recent report has been published — the moment the
67
+ * consumer sees it, not the moment teardown finishes, so a waiter is not
68
+ * left hanging when the watchdog is what released the event.
69
+ *
70
+ * Resolves immediately when nothing has been reported.
71
+ */
72
+ published: () => Promise<void>;
73
+ }
74
+ export declare const makeTerminalCloseReporter: (opts: {
75
+ logger: ILogger;
76
+ /** Overridable for tests; production uses the constant above. */
77
+ publishTimeoutMs?: number;
78
+ }) => TerminalCloseReporter;
79
+ //# sourceMappingURL=terminal-close-reporter.d.ts.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Reports the terminal `connection.update { close }` — exactly once, after
3
+ * teardown, and never silently not at all.
4
+ *
5
+ * That sentence is the whole contract this branch sells, and getting it wrong
6
+ * has two opposite failure modes, both bad:
7
+ *
8
+ * - **Not reported.** The consumer's handler never runs, so it never builds a
9
+ * replacement socket. A bot offline with nothing in its logs — the original
10
+ * bug this branch exists to fix.
11
+ * - **Reported early, or twice.** The replacement socket overlaps the old
12
+ * one's auth-store flush and `free()`, or every listener sees two terminal
13
+ * notifications for one socket and a handler that cleans up on close loops.
14
+ *
15
+ * Keeping both away used to be inline logic split across the dispatcher hook
16
+ * and `logout()`, sharing a counter and a promise. Nine separate bugs came out
17
+ * of that in review — a publish that could never run, a throwing listener
18
+ * taking the process down, `logout()` resolving before the close it caused,
19
+ * the watchdog not releasing its waiters, a suppression check that also
20
+ * matched non-terminal teardown, a fallback that did not count itself. Every
21
+ * one was a different way of answering the same question, so it is one object
22
+ * now, with the answer in one place.
23
+ */
24
+ /**
25
+ * How long teardown may run before the close is reported anyway.
26
+ *
27
+ * A deliberate trade of one guarantee for the other: past this point the close
28
+ * goes out with teardown still running, because losing the event entirely is
29
+ * the worse of the two failures. Teardown takes milliseconds in practice, so
30
+ * reaching this means a consumer end handler or a store flush never settled —
31
+ * and the error logged alongside says exactly that. Generous rather than
32
+ * tight, since firing early is the harmful direction.
33
+ */
34
+ export const TERMINAL_CLOSE_PUBLISH_TIMEOUT_MS = 60000;
35
+ export const makeTerminalCloseReporter = (opts) => {
36
+ const { logger } = opts;
37
+ const publishTimeoutMs = opts.publishTimeoutMs ?? TERMINAL_CLOSE_PUBLISH_TIMEOUT_MS;
38
+ /** Claims, not deliveries — see `hasReported`. */
39
+ let claimed = 0;
40
+ let publishedPromise;
41
+ /** One publish per claim, whatever gets there first, and never throwing. */
42
+ const makeOnce = (publish, settle) => {
43
+ let done = false;
44
+ return () => {
45
+ if (done)
46
+ return;
47
+ done = true;
48
+ try {
49
+ publish();
50
+ }
51
+ catch (err) {
52
+ // `publish` emits on the consumer's bus. Letting this escape
53
+ // would surface as an unhandled rejection on a detached chain —
54
+ // process exit under Node's default handler.
55
+ logger.error({ err }, 'connection.update listener threw on the terminal close');
56
+ }
57
+ settle();
58
+ };
59
+ };
60
+ return {
61
+ reportAfter: (teardown, publish) => {
62
+ claimed++;
63
+ let settle;
64
+ publishedPromise = new Promise(resolve => {
65
+ settle = resolve;
66
+ });
67
+ const publishOnce = makeOnce(publish, settle);
68
+ // Deliberately referenced: a pending promise does not keep Node's
69
+ // event loop alive, so an unref'd timer lets a minimal bot exit
70
+ // before it can publish — and the close handler that would have
71
+ // built the replacement never runs, which is what this guards.
72
+ // Cleared the moment teardown settles, so it holds the loop only
73
+ // while one is in flight.
74
+ const watchdog = setTimeout(() => {
75
+ logger.error({ afterMs: publishTimeoutMs }, 'socket teardown is still running; reporting the close anyway');
76
+ publishOnce();
77
+ }, publishTimeoutMs);
78
+ const finish = (err) => {
79
+ clearTimeout(watchdog);
80
+ if (err)
81
+ logger.error({ err }, 'socket teardown failed after a terminal disconnect');
82
+ publishOnce();
83
+ };
84
+ // `teardown()` is called inside the try because it can throw
85
+ // *synchronously*, before returning a promise — `.then` would never
86
+ // run and the close would stay unpublished until the watchdog, or
87
+ // forever if the timer were ever removed. The no-silent-failure
88
+ // promise has to hold for that path too.
89
+ try {
90
+ void teardown().then(() => finish(), finish);
91
+ }
92
+ catch (err) {
93
+ finish(err);
94
+ }
95
+ },
96
+ reportNow: publish => {
97
+ claimed++;
98
+ let settle;
99
+ publishedPromise = new Promise(resolve => {
100
+ settle = resolve;
101
+ });
102
+ makeOnce(publish, settle)();
103
+ },
104
+ hasReported: () => claimed > 0,
105
+ published: () => publishedPromise ?? Promise.resolve()
106
+ };
107
+ };
108
+ //# sourceMappingURL=terminal-close-reporter.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Which bridge disconnects end the socket for good.
3
+ *
4
+ * This mirrors one decision that lives in the Rust engine, so keep it honest:
5
+ * `whatsapp-rust` clears `enable_auto_reconnect` and breaks out of its run loop
6
+ * on exactly these, and retries everything else on the WA Web Fibonacci backoff
7
+ * (`client/lifecycle.rs`).
8
+ *
9
+ * The socket layer needs the answer because `WasmWhatsAppClient.run()` returns
10
+ * `void` — it spawns the loop as a background task, so the loop's exit is not
11
+ * observable from JS (`whatsapp_rust_bridge.d.ts`, `run(): void`). Until the
12
+ * bridge exposes loop completion, this table is how the socket knows a client
13
+ * has become dead weight that only `free()` can reclaim.
14
+ *
15
+ * The upstream Baileys contract this buys us: `connection.update { close }`
16
+ * means "this socket is finished, build a new one" — which is what every
17
+ * consumer written against upstream already assumes, because upstream has no
18
+ * auto-reconnect at all. Transient drops therefore never surface as `close`;
19
+ * they surface as `connecting`.
20
+ *
21
+ * Events deliberately absent from the terminal set, and why:
22
+ * - `disconnected` — the engine only dispatches `Event::Disconnected` for an
23
+ * *unexpected* loop exit, and every terminal path sets `expected_disconnect`
24
+ * first, which suppresses it (`client/lifecycle.rs`). So this one is the
25
+ * "engine is retrying" signal — with one exception the dispatcher handles:
26
+ * under `setAutoReconnect(false)` the run loop dispatches `Disconnected`
27
+ * and only *then* tests the flag and breaks out, which makes the very same
28
+ * event terminal. Absence from this list means "not terminal on its own",
29
+ * not "never terminal".
30
+ * - `streamError` — reaches JS only from the engine's catch-all `<stream:error>`
31
+ * branch (unknown code, `<ack/>`, `<xml-not-well-formed>`); the coded ones
32
+ * (401/409/515/516/429/503) dispatch their own events instead. Every case
33
+ * that gets here keeps the connection or recycles it deliberately.
34
+ * - `pairError` — pairing failed, but the engine keeps its loop and re-emits a
35
+ * QR; nothing about the client is dead.
36
+ */
37
+ /** True when the engine will keep retrying after this `<failure>`. */
38
+ export declare const isReconnectableConnectFailure: (reason: number | undefined) => boolean;
39
+ //# sourceMappingURL=terminal-close.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Which bridge disconnects end the socket for good.
3
+ *
4
+ * This mirrors one decision that lives in the Rust engine, so keep it honest:
5
+ * `whatsapp-rust` clears `enable_auto_reconnect` and breaks out of its run loop
6
+ * on exactly these, and retries everything else on the WA Web Fibonacci backoff
7
+ * (`client/lifecycle.rs`).
8
+ *
9
+ * The socket layer needs the answer because `WasmWhatsAppClient.run()` returns
10
+ * `void` — it spawns the loop as a background task, so the loop's exit is not
11
+ * observable from JS (`whatsapp_rust_bridge.d.ts`, `run(): void`). Until the
12
+ * bridge exposes loop completion, this table is how the socket knows a client
13
+ * has become dead weight that only `free()` can reclaim.
14
+ *
15
+ * The upstream Baileys contract this buys us: `connection.update { close }`
16
+ * means "this socket is finished, build a new one" — which is what every
17
+ * consumer written against upstream already assumes, because upstream has no
18
+ * auto-reconnect at all. Transient drops therefore never surface as `close`;
19
+ * they surface as `connecting`.
20
+ *
21
+ * Events deliberately absent from the terminal set, and why:
22
+ * - `disconnected` — the engine only dispatches `Event::Disconnected` for an
23
+ * *unexpected* loop exit, and every terminal path sets `expected_disconnect`
24
+ * first, which suppresses it (`client/lifecycle.rs`). So this one is the
25
+ * "engine is retrying" signal — with one exception the dispatcher handles:
26
+ * under `setAutoReconnect(false)` the run loop dispatches `Disconnected`
27
+ * and only *then* tests the flag and breaks out, which makes the very same
28
+ * event terminal. Absence from this list means "not terminal on its own",
29
+ * not "never terminal".
30
+ * - `streamError` — reaches JS only from the engine's catch-all `<stream:error>`
31
+ * branch (unknown code, `<ack/>`, `<xml-not-well-formed>`); the coded ones
32
+ * (401/409/515/516/429/503) dispatch their own events instead. Every case
33
+ * that gets here keeps the connection or recycles it deliberately.
34
+ * - `pairError` — pairing failed, but the engine keeps its loop and re-emits a
35
+ * QR; nothing about the client is dead.
36
+ */
37
+ /**
38
+ * `ConnectFailureReason` wire codes the engine retries — the JS mirror of
39
+ * `ConnectFailureReason::should_reconnect()`, which matches only
40
+ * `InternalServerError | ServiceUnavailable`.
41
+ *
42
+ * A `<failure>` with no reason at all is NOT retried: the engine parses it as
43
+ * `Unknown(0)`, which fails `should_reconnect()`.
44
+ */
45
+ const RECONNECTABLE_CONNECT_FAILURE_REASONS = new Set([
46
+ 500, // InternalServerError
47
+ 503 // ServiceUnavailable
48
+ ]);
49
+ /** True when the engine will keep retrying after this `<failure>`. */
50
+ export const isReconnectableConnectFailure = (reason) => reason !== undefined && RECONNECTABLE_CONNECT_FAILURE_REASONS.has(reason);
51
+ //# sourceMappingURL=terminal-close.js.map
@@ -23,6 +23,12 @@ export interface SocketContext {
23
23
  getClientSync: () => WasmWhatsAppClient;
24
24
  /** Raw stanza EventEmitter for CB: pattern compat */
25
25
  ws: EventEmitter;
26
+ /**
27
+ * Where a failure goes when it has nowhere else to go: a dispatcher that
28
+ * threw, a wire batch that would not decode. Also what the socket exposes
29
+ * as `onUnexpectedError`, so the two are one reporter rather than two.
30
+ */
31
+ reportUnexpectedError: (err: unknown, msg: string) => void;
26
32
  }
27
33
  /** Convert a bridge Jid struct to a string */
28
34
  export declare const jidStr: (jid: {
@@ -1,4 +1,13 @@
1
+ import type { CatalogResult as CatalogPageResult, CollectionsResult } from '@oxidezap/whatsapp-rust-bridge';
1
2
  import type { WAMediaUpload } from './Message.js';
3
+ /**
4
+ * What `getCatalog` returns, under a name of its own. The `CatalogResult`
5
+ * below is the raw catalog envelope and is a different shape, so a consumer
6
+ * typing the call has one name to reach for and it is this one.
7
+ */
8
+ export type CatalogPage = CatalogPageResult;
9
+ /** As `CatalogPage`, for `getCollections`. */
10
+ export type CollectionsPage = CollectionsResult;
2
11
  export type CatalogResult = {
3
12
  data: {
4
13
  paging: {
@@ -362,6 +362,37 @@ export const makeEventBuffer = (logger) => {
362
362
  let flushPendingTimeout;
363
363
  ev.on('event', (events) => {
364
364
  for (const event of Object.keys(events)) {
365
+ if (event === 'connection.update') {
366
+ // Delivered listener by listener, unlike every other event.
367
+ // `EventEmitter.emit()` stops at the first listener that throws,
368
+ // so one bad handler would keep the rest — including the app's
369
+ // reconnect handler — from ever seeing a terminal `close`, and
370
+ // the bot would stay offline. This is the lifecycle channel;
371
+ // losing delivery here is the failure mode the socket's whole
372
+ // close contract exists to prevent.
373
+ // `rawListeners`, not `listeners`: the latter unwraps `once()`
374
+ // handlers to the function underneath, so calling that would
375
+ // never run the wrapper that unregisters them and the listener
376
+ // would stay subscribed for every later update. `once` is not on
377
+ // `BaileysEventEmitter` today, so this is unreachable rather than
378
+ // broken — but the two differ only in this respect and the raw
379
+ // one is the correct primitive for calling listeners by hand.
380
+ for (const listener of ev.rawListeners(event)) {
381
+ try {
382
+ // `.call(ev, …)` so `this` is still the emitter, as it
383
+ // would be under `emit()`. A listener declared as a
384
+ // normal function that does `this.off('connection.update', …)`
385
+ // — a common self-removing pattern — would otherwise
386
+ // throw on `undefined` and be swallowed by the catch.
387
+ ;
388
+ listener.call(ev, events[event]);
389
+ }
390
+ catch (err) {
391
+ logger.error({ err, event }, 'connection.update listener threw; continuing with the rest');
392
+ }
393
+ }
394
+ continue;
395
+ }
365
396
  ev.emit(event, events[event]);
366
397
  }
367
398
  });
@@ -3,6 +3,7 @@ export * from './auth-utils.js';
3
3
  export * from './crypto.js';
4
4
  export * from './generics.js';
5
5
  export * from './messages.js';
6
+ export { getUrlInfo, type URLGenerationOptions } from './link-preview.js';
6
7
  export * from './messages-media.js';
7
8
  export * from './process-history-message.js';
8
9
  export * from './process-message.js';
@@ -3,6 +3,9 @@ export * from './auth-utils.js';
3
3
  export * from './crypto.js';
4
4
  export * from './generics.js';
5
5
  export * from './messages.js';
6
+ // Named rather than `*`: the underscore hooks in that file exist for the tests
7
+ // and would otherwise become released API.
8
+ export { getUrlInfo } from './link-preview.js';
6
9
  export * from './messages-media.js';
7
10
  export * from './process-history-message.js';
8
11
  export * from './process-message.js';
@@ -0,0 +1,60 @@
1
+ import type { WAUrlInfo } from '../Types/Message.js';
2
+ import type { ILogger } from './logger.js';
3
+ /**
4
+ * The first link in a piece of text, or undefined when there is none. Exported
5
+ * under an underscore so the extraction can be tested without a network.
6
+ *
7
+ * Trailing prose punctuation is dropped: a link at the end of a sentence
8
+ * carries the full stop with it, and `https://example.com.` is not what the
9
+ * writer meant. A closing bracket goes the same way, which costs the rare url
10
+ * that genuinely ends in one and saves the common case of a link in
11
+ * parentheses.
12
+ */
13
+ export declare const _firstLink: (text: string) => string | undefined;
14
+ export type URLGenerationOptions = {
15
+ thumbnailWidth: number;
16
+ fetchOpts: {
17
+ /** Timeout in ms */
18
+ timeout: number;
19
+ proxyUrl?: string;
20
+ headers?: HeadersInit;
21
+ };
22
+ uploadImage?: (encFilePath: string, opts: {
23
+ fileEncSha256B64: string;
24
+ mediaType: string;
25
+ }) => Promise<unknown>;
26
+ logger?: ILogger;
27
+ };
28
+ /**
29
+ * Fetched here rather than through `getHttpStream`, which forwards neither the
30
+ * timeout nor the proxy and validates no destination. The credentials in
31
+ * `headers` are for the page, so they are sent to the thumbnail only when it
32
+ * is the same origin; another host advertised by that page must not receive
33
+ * them.
34
+ */
35
+ /** Exported under an underscore so the destination guard can be driven directly. */
36
+ export declare const _getCompressedJpegThumbnail: (url: string, pageUrl: string, { thumbnailWidth, fetchOpts }: URLGenerationOptions) => Promise<{
37
+ buffer: any;
38
+ original: {
39
+ width: any;
40
+ height: any;
41
+ };
42
+ }>;
43
+ /**
44
+ * Reads the first URL out of a piece of text and fetches what a link preview
45
+ * needs. Nothing here is protocol: it is an HTTP fetch, an OpenGraph parse and
46
+ * a thumbnail, which is why it belongs in this layer rather than the engine.
47
+ *
48
+ * Resolves to undefined for the two cases that mean "no preview": text with no
49
+ * link in it, and a page with no title. Everything else throws, including a
50
+ * timeout, because a swallowed failure is indistinguishable from a page that
51
+ * genuinely had nothing, and a caller retrying the first would give up on the
52
+ * second.
53
+ *
54
+ * The metadata parse comes from `link-preview-js`, an optional peer dependency
55
+ * this package already declares and had no reader for, so nothing new is
56
+ * pulled in: a consumer who does not want link previews does not install it
57
+ * and never calls this.
58
+ */
59
+ export declare const getUrlInfo: (text: string, opts?: URLGenerationOptions) => Promise<WAUrlInfo | undefined>;
60
+ //# sourceMappingURL=link-preview.d.ts.map