@oxidezap/baileyrs 0.1.3 → 0.2.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.
- package/README.md +60 -0
- package/lib/Bridge/primitives.d.ts +49 -3
- package/lib/Bridge/primitives.js +126 -8
- package/lib/Bridge/schema.js +208 -87
- package/lib/Bridge/types.d.ts +61 -2
- package/lib/Compatibility/derived-stanza-nodes.d.ts +28 -0
- package/lib/Compatibility/derived-stanza-nodes.js +71 -0
- package/lib/Compatibility/encode-proto.d.ts +17 -0
- package/lib/Compatibility/encode-proto.js +32 -0
- package/lib/Compatibility/proto-runtime.d.ts +10 -0
- package/lib/Compatibility/proto-runtime.js +176 -11
- package/lib/Socket/events.js +35 -6
- package/lib/Socket/groups.js +6 -2
- package/lib/Socket/index.js +4 -3
- package/lib/Socket/messages.js +12 -6
- package/lib/Socket/transport.js +23 -3
- package/lib/Types/Auth.d.ts +4 -1
- package/lib/Types/Events.d.ts +17 -0
- package/lib/Types/Message.d.ts +17 -1
- package/lib/Utils/event-buffer.js +48 -11
- package/lib/Utils/messages.js +30 -7
- package/lib/Utils/process-history-message.d.ts +11 -2
- package/lib/Utils/process-history-message.js +11 -7
- package/lib/Utils/use-multi-file-auth-state.js +45 -0
- 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
|
|
50
|
-
* strings — the bridge serializes `DateTime<Utc>` as
|
|
51
|
-
*
|
|
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
|
package/lib/Bridge/primitives.js
CHANGED
|
@@ -46,20 +46,116 @@ export const asJidAddressString = (x) => {
|
|
|
46
46
|
return j ? bridgeJidToAddressString(j) : undefined;
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
-
|
|
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
|