@oxidezap/baileyrs 0.1.2 → 0.2.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.
Files changed (50) hide show
  1. package/README.md +60 -0
  2. package/lib/Bridge/primitives.d.ts +49 -3
  3. package/lib/Bridge/primitives.js +126 -8
  4. package/lib/Bridge/schema.js +208 -87
  5. package/lib/Bridge/types.d.ts +61 -2
  6. package/lib/Compatibility/derived-stanza-nodes.d.ts +28 -0
  7. package/lib/Compatibility/derived-stanza-nodes.js +71 -0
  8. package/lib/Compatibility/encode-proto.d.ts +17 -0
  9. package/lib/Compatibility/encode-proto.js +32 -0
  10. package/lib/Compatibility/proto-runtime.d.ts +10 -0
  11. package/lib/Compatibility/proto-runtime.js +176 -11
  12. package/lib/Defaults/index.d.ts +6 -0
  13. package/lib/Defaults/index.js +6 -0
  14. package/lib/Socket/blocking.d.ts +3 -1
  15. package/lib/Socket/blocking.js +3 -0
  16. package/lib/Socket/communities.d.ts +14 -9
  17. package/lib/Socket/communities.js +24 -5
  18. package/lib/Socket/contacts.d.ts +3 -1
  19. package/lib/Socket/contacts.js +3 -0
  20. package/lib/Socket/events.js +35 -6
  21. package/lib/Socket/groups.d.ts +30 -4
  22. package/lib/Socket/groups.js +20 -4
  23. package/lib/Socket/index.d.ts +18 -17
  24. package/lib/Socket/index.js +18 -6
  25. package/lib/Socket/messages.js +18 -7
  26. package/lib/Socket/newsletter.d.ts +3 -1
  27. package/lib/Socket/newsletter.js +3 -3
  28. package/lib/Socket/presence.d.ts +3 -2
  29. package/lib/Socket/presence.js +4 -0
  30. package/lib/Socket/privacy.d.ts +7 -1
  31. package/lib/Socket/privacy.js +16 -0
  32. package/lib/Socket/server-queries.d.ts +3 -1
  33. package/lib/Socket/server-queries.js +3 -0
  34. package/lib/Types/Auth.d.ts +4 -1
  35. package/lib/Types/Chat.d.ts +26 -8
  36. package/lib/Types/Chat.js +18 -0
  37. package/lib/Types/Events.d.ts +17 -0
  38. package/lib/Types/GroupMetadata.d.ts +3 -1
  39. package/lib/Types/GroupMetadata.js +1 -1
  40. package/lib/Types/Message.d.ts +20 -2
  41. package/lib/Types/Message.js +11 -0
  42. package/lib/Utils/argument-domain.d.ts +15 -0
  43. package/lib/Utils/argument-domain.js +36 -0
  44. package/lib/Utils/event-buffer.js +48 -11
  45. package/lib/Utils/messages.d.ts +3 -1
  46. package/lib/Utils/messages.js +35 -7
  47. package/lib/Utils/process-history-message.d.ts +11 -2
  48. package/lib/Utils/process-history-message.js +11 -7
  49. package/lib/Utils/use-multi-file-auth-state.js +45 -0
  50. package/package.json +9 -3
package/README.md CHANGED
@@ -27,6 +27,14 @@ so existing integrations can migrate with minimal changes. See
27
27
  | Key management | JS auth state | Rust `PersistenceManager` |
28
28
  | Auto-reconnect | Manual `startSock()` loop | Transient drops retried in Rust (fibonacci backoff); terminal ones still yours |
29
29
 
30
+ Compatibility is checked rather than assumed: a declaration audit against
31
+ upstream's `.d.ts`, a wire-fidelity audit of the send path, ~50 behavioural
32
+ compatibility suites, and a
33
+ [differential fuzz suite](src/__fuzz__/README.md) that generates its own inputs
34
+ from the proto schema and compares the two libraries directly. Differences the
35
+ fuzzers find are recorded with a reason and a review date, and known open ones
36
+ are listed in `src/__fuzz__/harness/divergence.ts`.
37
+
30
38
  ## Documentation
31
39
 
32
40
  The full API reference and guides live in the
@@ -212,6 +220,11 @@ A few behaviors that differ from upstream — almost always to your advantage:
212
220
  | anything else | reconnect, after a short delay |
213
221
 
214
222
  `Example/example.ts` implements exactly this.
223
+ - **`connecting` is not a short state here.** The engine's backoff grows with
224
+ each consecutive failure, so a single `connecting` can stand for minutes of
225
+ downtime with nothing else emitted in between. See
226
+ [When `connecting` lasts minutes](#when-connecting-lasts-minutes): a
227
+ readiness timeout written for upstream's `connecting` misreads this one.
215
228
  - **No `getMessage` / `cachedGroupMetadata` polyfill required.** The Rust
216
229
  side caches group metadata and message keys natively. You can still pass
217
230
  them — they're respected as overrides — but they're optional.
@@ -224,6 +237,53 @@ A few behaviors that differ from upstream — almost always to your advantage:
224
237
  See [Bridge state in your key store](#bridge-state-in-your-key-store) — this
225
238
  one can break a boot path, so it has its own section.
226
239
 
240
+ ### When `connecting` lasts minutes
241
+
242
+ Upstream Baileys emits `connecting` once per socket, and it resolves to `open`
243
+ or `close` within seconds, because upstream never retries on its own. On
244
+ baileyrs the same value also covers every drop the engine is retrying, and the
245
+ backoff between those retries climbs with each consecutive failure.
246
+
247
+ Here is what that costs on a rate-limited account. Four consecutive
248
+ `429 rate-overlimit`, measured against the production reconnect path:
249
+
250
+ | failure | next attempt in | offline so far |
251
+ | --- | --- | --- |
252
+ | `429` #1 | 8.4s | 8.4s |
253
+ | `429` #2 | 146.2s | 154.6s |
254
+ | `429` #3 | 813.9s | 968.5s |
255
+ | `429` #4 | 903.5s | 1872.0s |
256
+
257
+ About 16 minutes offline once the third retry delay has run, and about 31 once
258
+ the fourth has. For all of it the consumer sees exactly **one**
259
+ `connection: 'connecting'`: no `lastDisconnect`, no status code, no repeat.
260
+ `isConnected` and `isLoggedIn` are both `false` throughout, exactly as they are
261
+ during a first connection, so neither tells you a retry is scheduled.
262
+
263
+ **Do not arm a readiness timeout on `connecting`, and above all do not restart
264
+ the process when one expires.** The backoff counter lives in the Rust client
265
+ that your socket owns, so it dies with the process. The next boot starts a new
266
+ attempt immediately, replays the whole startup burst against a server that just
267
+ asked for less traffic, and earns the next `429` sooner. Restarting is the
268
+ single worst answer to a rate limit, and an upstream-shaped readiness timeout
269
+ leads straight to it.
270
+
271
+ What to do instead:
272
+
273
+ - Treat `open` and `close` as the only decision points. `connecting` carries no
274
+ failure to react to, and it is never a reason to build a second socket. Only
275
+ `close` is.
276
+ - Read `connecting` in context: after an `open` it means the engine is retrying
277
+ a drop it owns and will keep retrying; with no `open` before it, it is a first
278
+ connection still being established. Neither one is stuck.
279
+ - If you need a liveness watchdog anyway, budget it well past the ladder above,
280
+ tens of minutes rather than seconds, and have it alert a human instead of
281
+ killing the process. A shorter one fires on a backoff that was about to
282
+ succeed.
283
+ - Fix the cause on your side: send less. The engine restores the connection,
284
+ but it does not pace your traffic, and the traffic is what earned the `429`.
285
+ Queueing, throttling and deferral are yours to decide, the same as upstream.
286
+
227
287
  ### Bridge state in your key store
228
288
 
229
289
  Upstream Baileys keeps engine state in `creds` (persisted by `saveCreds`) and
@@ -46,15 +46,61 @@ export declare const bridgeJidToAddressString: (j: BridgeJid) => string;
46
46
  /** Validate and stringify a signaling JID without discarding its device. */
47
47
  export declare const asJidAddressString: (x: unknown) => string | undefined;
48
48
  /**
49
- * Coerce a timestamp value into unix seconds. Accepts both numbers and ISO
50
- * strings — the bridge serializes `DateTime<Utc>` as ISO unless explicitly
51
- * typed with `ts_seconds`, so being lenient here insulates us from drift.
49
+ * Coerce a timestamp value into unix seconds. Accepts both numbers and RFC 3339
50
+ * strings — the bridge serializes `DateTime<Utc>` as a string unless the field
51
+ * names one of chrono's `ts_*` modules, so being lenient about which of the two
52
+ * arrives insulates us from drift.
52
53
  */
53
54
  export declare const toUnixSeconds: (raw: unknown) => number;
55
+ /**
56
+ * Same as `toUnixSeconds`, but absence stays absent.
57
+ *
58
+ * The optional timestamps — `last_seen`, a server ack's `t`, an app-state
59
+ * mutation's own time — mean "the server did not say" when missing, which is
60
+ * not the same claim as the epoch.
61
+ */
62
+ export declare const asUnixSeconds: (raw: unknown) => number | undefined;
63
+ /**
64
+ * A 64-bit proto field, however the bridge chose to carry it.
65
+ *
66
+ * A protobuf `int64` reaches JavaScript as a protobufjs `Long`
67
+ * (`{ low, high, unsigned }`, sometimes with `toNumber`), because 64 bits do
68
+ * not fit a JS number. A plain number is accepted too: the same field arrived
69
+ * that way while these payloads crossed through serde, and a bridge is free to
70
+ * go back to it.
71
+ *
72
+ * Reconstructed from the pair only when `high` is 0 or -1, the range a JS
73
+ * number represents exactly. Beyond that the value would be silently rounded,
74
+ * and a wrong timestamp is worse than a missing one.
75
+ */
76
+ export declare const asInt64: (x: unknown) => number | undefined;
77
+ /**
78
+ * A `std::time::Duration`, in whole seconds.
79
+ *
80
+ * Serde writes one as `{ secs, nanos }`, and the bridge's own declaration for
81
+ * these fields says `number` — so a consumer reading the declared type off a
82
+ * plain-serialized event gets an object where it expected a count. Both are
83
+ * accepted here rather than betting on which side moves first; sub-second
84
+ * precision is dropped because every caller of this is a ban or a backoff
85
+ * measured in seconds.
86
+ */
87
+ export declare const asDurationSeconds: (x: unknown) => number | undefined;
54
88
  /**
55
89
  * Lowercase a discriminator string defensively — handles both the current
56
90
  * lowercase wire-tag form and any legacy PascalCase form an older bridge
57
91
  * might still emit.
58
92
  */
59
93
  export declare const normalizeDiscriminator: (x: unknown) => string | undefined;
94
+ /**
95
+ * Turn a duration in seconds into the unix-seconds instant it ends at.
96
+ *
97
+ * The bridge reports a temporary ban the way the wire states it, as how long
98
+ * the ban lasts. Consumers are promised the deadline: the socket formats it
99
+ * with `new Date(expire * 1000)` and a reconnect policy subtracts `Date.now()`
100
+ * from it, so handing either of them a relative count puts the ban in 1970 and
101
+ * lets a bot retry immediately.
102
+ */
103
+ export declare const absoluteFromDuration: (seconds: number | undefined) => number | undefined;
104
+ /** Wire collection names and the like: anything that is not a string is not one. */
105
+ export declare const asStringArray: (x: unknown) => string[];
60
106
  //# sourceMappingURL=primitives.d.ts.map
@@ -46,20 +46,116 @@ export const asJidAddressString = (x) => {
46
46
  return j ? bridgeJidToAddressString(j) : undefined;
47
47
  };
48
48
  /**
49
- * Coerce a timestamp value into unix seconds. Accepts both numbers and ISO
50
- * strings — the bridge serializes `DateTime<Utc>` as ISO unless explicitly
51
- * typed with `ts_seconds`, so being lenient here insulates us from drift.
49
+ * The shape chrono writes for a `DateTime` it serializes plainly.
50
+ *
51
+ * Checked before parsing because `Date.parse` accepts far more than that, and
52
+ * silently: `Date.parse('0')` is the year 2000, so a numeric-string sentinel
53
+ * would become a plausible, wrong instant instead of being refused.
54
+ */
55
+ const RFC_3339 = /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:?\d{2})$/;
56
+ const MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
57
+ /** Gregorian, so a century is only a leap year when it divides by 400. */
58
+ const daysInMonth = (year, month) => month === 2 && year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : (MONTH_LENGTHS[month - 1] ?? 0);
59
+ /** Fixed-width digits at a known offset, which the pattern above guarantees. */
60
+ const digits2 = (raw, at) => (raw.charCodeAt(at) - 48) * 10 + (raw.charCodeAt(at + 1) - 48);
61
+ const parseRfc3339Seconds = (raw) => {
62
+ // `test` rather than `exec`: the components come out of fixed offsets
63
+ // below, and asking the engine for capture groups costs more than the whole
64
+ // rest of this function.
65
+ if (!RFC_3339.test(raw))
66
+ return undefined;
67
+ // `Date.parse` refuses month 13, hour 25 and second 61, but rolls a day
68
+ // past the end of its month forward instead: `2023-02-30` comes back as
69
+ // March 2. That is the one component worth checking here.
70
+ const year = digits2(raw, 0) * 100 + digits2(raw, 2);
71
+ if (digits2(raw, 8) > daysInMonth(year, digits2(raw, 5)))
72
+ return undefined;
73
+ // The other one it rolls forward rather than refusing: RFC 3339 stops the
74
+ // hour at 23, while `2023-11-14T24:00:00Z` parses as midnight the next day.
75
+ if (digits2(raw, 11) > 23)
76
+ return undefined;
77
+ const ms = Date.parse(raw);
78
+ return Number.isFinite(ms) ? Math.floor(ms / 1000) : undefined;
79
+ };
80
+ /**
81
+ * Coerce a timestamp value into unix seconds. Accepts both numbers and RFC 3339
82
+ * strings — the bridge serializes `DateTime<Utc>` as a string unless the field
83
+ * names one of chrono's `ts_*` modules, so being lenient about which of the two
84
+ * arrives insulates us from drift.
52
85
  */
53
86
  export const toUnixSeconds = (raw) => {
54
87
  if (typeof raw === 'number' && Number.isFinite(raw))
55
88
  return raw;
56
- if (typeof raw === 'string') {
57
- const ms = Date.parse(raw);
58
- if (Number.isFinite(ms))
59
- return Math.floor(ms / 1000);
60
- }
89
+ if (typeof raw === 'string')
90
+ return parseRfc3339Seconds(raw) ?? 0;
61
91
  return 0;
62
92
  };
93
+ /**
94
+ * Same as `toUnixSeconds`, but absence stays absent.
95
+ *
96
+ * The optional timestamps — `last_seen`, a server ack's `t`, an app-state
97
+ * mutation's own time — mean "the server did not say" when missing, which is
98
+ * not the same claim as the epoch.
99
+ */
100
+ export const asUnixSeconds = (raw) => {
101
+ if (typeof raw === 'number' && Number.isFinite(raw))
102
+ return raw;
103
+ if (typeof raw === 'string')
104
+ return parseRfc3339Seconds(raw);
105
+ return undefined;
106
+ };
107
+ /**
108
+ * A 64-bit proto field, however the bridge chose to carry it.
109
+ *
110
+ * A protobuf `int64` reaches JavaScript as a protobufjs `Long`
111
+ * (`{ low, high, unsigned }`, sometimes with `toNumber`), because 64 bits do
112
+ * not fit a JS number. A plain number is accepted too: the same field arrived
113
+ * that way while these payloads crossed through serde, and a bridge is free to
114
+ * go back to it.
115
+ *
116
+ * Reconstructed from the pair only when `high` is 0 or -1, the range a JS
117
+ * number represents exactly. Beyond that the value would be silently rounded,
118
+ * and a wrong timestamp is worse than a missing one.
119
+ */
120
+ export const asInt64 = (x) => {
121
+ if (typeof x === 'number')
122
+ return Number.isSafeInteger(x) ? x : undefined;
123
+ if (!isObject(x))
124
+ return undefined;
125
+ if (typeof x.toNumber === 'function') {
126
+ const value = x.toNumber();
127
+ // `Long.toNumber` rounds rather than refusing, so a value past 2^53
128
+ // comes back finite and wrong. Only exact ones are worth reporting.
129
+ return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
130
+ }
131
+ const low = x.low;
132
+ if (typeof low !== 'number')
133
+ return undefined;
134
+ const high = typeof x.high === 'number' ? x.high : 0;
135
+ // The pair is `high * 2^32 + (low >>> 0)`, signed through `high`. Reading
136
+ // the halves separately gets the sign right only by accident: `high: -1`
137
+ // with `low: 0` is -2^32, not 0.
138
+ const value = high * 2 ** 32 + (low >>> 0);
139
+ return Number.isSafeInteger(value) ? value : undefined;
140
+ };
141
+ /**
142
+ * A `std::time::Duration`, in whole seconds.
143
+ *
144
+ * Serde writes one as `{ secs, nanos }`, and the bridge's own declaration for
145
+ * these fields says `number` — so a consumer reading the declared type off a
146
+ * plain-serialized event gets an object where it expected a count. Both are
147
+ * accepted here rather than betting on which side moves first; sub-second
148
+ * precision is dropped because every caller of this is a ban or a backoff
149
+ * measured in seconds.
150
+ */
151
+ export const asDurationSeconds = (x) => {
152
+ if (typeof x === 'number')
153
+ return Number.isFinite(x) ? x : undefined;
154
+ if (!isObject(x))
155
+ return undefined;
156
+ const secs = asInt64(x.secs);
157
+ return secs === undefined ? undefined : secs;
158
+ };
63
159
  /**
64
160
  * Lowercase a discriminator string defensively — handles both the current
65
161
  * lowercase wire-tag form and any legacy PascalCase form an older bridge
@@ -69,4 +165,26 @@ export const normalizeDiscriminator = (x) => {
69
165
  const s = asString(x);
70
166
  return s ? s.toLowerCase() : undefined;
71
167
  };
168
+ /**
169
+ * Turn a duration in seconds into the unix-seconds instant it ends at.
170
+ *
171
+ * The bridge reports a temporary ban the way the wire states it, as how long
172
+ * the ban lasts. Consumers are promised the deadline: the socket formats it
173
+ * with `new Date(expire * 1000)` and a reconnect policy subtracts `Date.now()`
174
+ * from it, so handing either of them a relative count puts the ban in 1970 and
175
+ * lets a bot retry immediately.
176
+ */
177
+ export const absoluteFromDuration = (seconds) => {
178
+ if (seconds === undefined)
179
+ return undefined;
180
+ const deadline = Math.floor(Date.now() / 1000) + seconds;
181
+ // A deadline `Date` cannot hold is worse than none: the socket formats this
182
+ // with `new Date(expire * 1000).toISOString()`, which throws a RangeError
183
+ // past ±8.64e15 ms and would take the event dispatch down with it.
184
+ return Number.isSafeInteger(deadline) && Math.abs(deadline) <= MAX_DATE_SECONDS ? deadline : undefined;
185
+ };
186
+ /** `Date` holds ±8.64e15 ms, which is this many whole seconds. */
187
+ const MAX_DATE_SECONDS = 8640000000000;
188
+ /** Wire collection names and the like: anything that is not a string is not one. */
189
+ export const asStringArray = (x) => Array.isArray(x) ? x.filter((item) => typeof item === 'string') : [];
72
190
  //# sourceMappingURL=primitives.js.map