libzapitu 1.0.0-alpha.24 → 1.0.0-alpha.26

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.
@@ -76,22 +76,79 @@ function proxyCallback(socketId, key, log, ...args) {
76
76
  // Create a socket for a given socketId
77
77
  // ---------------------------------------------------------------------------
78
78
  /**
79
- * Recursively clone a value, replacing non-cloneable types (Error, Function)
80
- * with plain serializable representations.
79
+ * Recursively clone a value for postMessage, replacing non-cloneable types
80
+ * (Error, Function) with plain serializable representations, preserving
81
+ * built-in value objects, and converting protobufjs message instances to JSON.
82
+ *
83
+ * Protobuf messages (e.g. proto.Message, proto.WebMessageInfo) carry a
84
+ * toJSON() method that serializes byte fields as base64 strings and 64-bit
85
+ * integer fields as strings. Structured clone strips the prototype chain, so
86
+ * without this step Buffers become Uint8Array plain objects and Longs become
87
+ * { low, high, unsigned } objects on the parent side. Converting them before
88
+ * crossing the boundary keeps the worker event shape identical to direct mode
89
+ * when the data is logged or JSON.stringify'd.
81
90
  */
82
- function sanitizeForPostMessage(value, seen = new WeakSet()) {
91
+ function serializeForPostMessage(value, seen = new WeakSet()) {
83
92
  if (value instanceof Error) {
93
+ // Boom carries critical metadata (statusCode, output, data, isServer).
94
+ // Preserve it so the parent can reconstruct a real Boom instance.
95
+ if (value.isBoom) {
96
+ return {
97
+ __error__: true,
98
+ __boom__: true,
99
+ message: value.message,
100
+ name: value.name,
101
+ stack: value.stack,
102
+ data: value.data,
103
+ output: value.output,
104
+ isServer: value.isServer
105
+ };
106
+ }
84
107
  return { __error__: true, message: value.message, name: value.name, stack: value.stack };
85
108
  }
86
109
  if (typeof value === 'function') {
87
110
  return '__fn__';
88
111
  }
112
+ // Preserve built-in value objects that structured clone handles, but tag
113
+ // them so the parent can reconstruct the exact same runtime type.
114
+ if (value instanceof Date) {
115
+ return { __date__: true, value: value.toISOString() };
116
+ }
117
+ if (value instanceof RegExp) {
118
+ return { __regexp__: true, source: value.source, flags: value.flags };
119
+ }
120
+ if (value instanceof URL) {
121
+ return { __url__: true, href: value.href };
122
+ }
123
+ if (value instanceof Map) {
124
+ return { __map__: true, entries: serializeForPostMessage(Array.from(value.entries()), seen) };
125
+ }
126
+ if (value instanceof Set) {
127
+ return { __set__: true, entries: serializeForPostMessage(Array.from(value.values()), seen) };
128
+ }
89
129
  if (value !== null && typeof value === 'object') {
90
130
  if (seen.has(value))
91
131
  return '[Circular]';
92
132
  seen.add(value);
133
+ // Detect a protobufjs message instance by the static methods its
134
+ // constructor exposes, then use the instance's own toJSON() so nested
135
+ // bytes/long fields are serialized consistently. Tag the resulting
136
+ // plain object with the constructor name so the parent can revive it
137
+ // back into a real protobuf instance (and therefore recover the
138
+ // runtime Buffer types downstream code expects).
139
+ const ctor = value.constructor;
140
+ if (ctor &&
141
+ typeof ctor === 'function' &&
142
+ typeof ctor.encode === 'function' &&
143
+ typeof ctor.decode === 'function' &&
144
+ typeof ctor.toObject === 'function' &&
145
+ typeof value.toJSON === 'function') {
146
+ const json = value.toJSON();
147
+ json.__protobufType__ = ctor.name;
148
+ return serializeForPostMessage(json, seen);
149
+ }
93
150
  if (Array.isArray(value)) {
94
- return value.map(v => sanitizeForPostMessage(v, seen));
151
+ return value.map(v => serializeForPostMessage(v, seen));
95
152
  }
96
153
  // Preserve binary/typed-array types — structured clone handles them natively
97
154
  if (Buffer.isBuffer(value) ||
@@ -102,7 +159,7 @@ function sanitizeForPostMessage(value, seen = new WeakSet()) {
102
159
  }
103
160
  const result = {};
104
161
  for (const key of Object.keys(value)) {
105
- result[key] = sanitizeForPostMessage(value[key], seen);
162
+ result[key] = serializeForPostMessage(value[key], seen);
106
163
  }
107
164
  return result;
108
165
  }
@@ -188,6 +245,8 @@ function createSocket(socketId, rawConfig) {
188
245
  log.info('socket created successfully');
189
246
  // Send user and authState to the parent when they become available.
190
247
  // These are needed for wsocket.user and wsocket.authState on the proxy side.
248
+ // authState.keys contains forwarding functions, so we must serialize the
249
+ // info before postMessage or the whole message is dropped.
191
250
  const sendSocketInfo = () => {
192
251
  try {
193
252
  const info = {};
@@ -196,7 +255,11 @@ function createSocket(socketId, rawConfig) {
196
255
  if (sock.authState)
197
256
  info.authState = sock.authState;
198
257
  if (Object.keys(info).length > 0) {
199
- worker_threads_1.parentPort.postMessage({ type: 'socket-info', socketId, info });
258
+ worker_threads_1.parentPort.postMessage({
259
+ type: 'socket-info',
260
+ socketId,
261
+ info: serializeForPostMessage(info)
262
+ });
200
263
  }
201
264
  }
202
265
  catch (_) {
@@ -226,18 +289,10 @@ function createSocket(socketId, rawConfig) {
226
289
  sock.ev.on('event', (map) => {
227
290
  log.trace({ eventKeys: Object.keys(map) }, 'event → parent');
228
291
  try {
229
- worker_threads_1.parentPort.postMessage({ type: 'event', socketId, map });
292
+ worker_threads_1.parentPort.postMessage({ type: 'event', socketId, map: serializeForPostMessage(map) });
230
293
  }
231
294
  catch (err) {
232
295
  log.error({ err, eventKeys: Object.keys(map) }, 'failed to send event to parent (non-cloneable data)');
233
- // Attempt to send a sanitized version without Error objects
234
- try {
235
- const sanitized = sanitizeForPostMessage(map);
236
- worker_threads_1.parentPort.postMessage({ type: 'event', socketId, map: sanitized });
237
- }
238
- catch (_) {
239
- log.error({ eventKeys: Object.keys(map) }, 'event dropped — could not sanitize');
240
- }
241
296
  }
242
297
  });
243
298
  // Forward raw WS events to the parent so ws.on() works on the proxy side.
@@ -305,11 +360,11 @@ worker_threads_1.parentPort.on('message', async (msg) => {
305
360
  const thisCtx = path.length > 1 ? resolveTarget(sock, path.slice(0, -1)) : sock;
306
361
  const result = await target.apply(thisCtx, args || []);
307
362
  log.trace({ rpcId: id, method }, 'RPC call succeeded');
308
- worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result });
363
+ worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result: serializeForPostMessage(result) });
309
364
  }
310
365
  else {
311
366
  log.trace({ rpcId: id, method }, 'RPC property read');
312
- worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result: target });
367
+ worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result: serializeForPostMessage(target) });
313
368
  }
314
369
  }
315
370
  catch (err) {
@@ -36,6 +36,8 @@ const stream_1 = require("stream");
36
36
  const util_1 = require("util");
37
37
  const os_2 = require("os");
38
38
  const long_1 = __importDefault(require("long"));
39
+ const boom_1 = require("@hapi/boom");
40
+ const WAProto_1 = require("../../WAProto");
39
41
  const pipelineAsync = (0, util_1.promisify)(stream_1.pipeline);
40
42
  // ---------------------------------------------------------------------------
41
43
  // Stream serialization for RPC boundary
@@ -224,23 +226,98 @@ function createNoopLogger() {
224
226
  };
225
227
  }
226
228
  /**
227
- * Recursively revive objects that were sanitized by the child's
228
- * sanitizeForPostMessage (e.g. Error sentinels) or decomposed by
229
- * structured clone (e.g. Long → { low, high, unsigned }).
229
+ * Build a lookup table from protobufjs type name to the generated
230
+ * constructor. This lets us revive a message that was serialized to JSON
231
+ * in the worker back into a real protobuf instance on the parent side.
232
+ */
233
+ function buildProtobufTypeRegistry(namespace, result = new Map()) {
234
+ for (const [key, value] of Object.entries(namespace)) {
235
+ if (typeof value === 'function' && value.fromObject && value.encode && value.decode) {
236
+ result.set(value.name || key, value);
237
+ }
238
+ else if (value !== null && typeof value === 'object' && !value.fromObject) {
239
+ buildProtobufTypeRegistry(value, result);
240
+ }
241
+ }
242
+ return result;
243
+ }
244
+ const PROTOBUF_TYPE_REGISTRY = buildProtobufTypeRegistry(WAProto_1.proto);
245
+ /**
246
+ * Recursively revive objects that were serialized by the child's
247
+ * serializeForPostMessage (e.g. Error sentinels, protobuf messages) or
248
+ * decomposed by structured clone (e.g. Long → { low, high, unsigned },
249
+ * Buffer → Uint8Array).
230
250
  *
231
- * IMPORTANT: structured clone already produces a perfect copy of the data.
232
- * We only need to recursively look for __error__ sentinels and convert
233
- * them back to Error instances, and revive Long-like plain objects back
234
- * to real Long instances. Everything else passes through unchanged.
251
+ * Protobuf message instances are converted to JSON by the child, tagged with
252
+ * __protobufType__, and then revived here with the corresponding
253
+ * constructor's fromObject(). This restores the runtime Buffer/Long types
254
+ * that downstream code expects while keeping JSON.stringify output identical
255
+ * to direct mode (base64 strings for bytes, strings for 64-bit integers).
235
256
  */
236
257
  function revivePostMessage(value) {
258
+ var _a;
259
+ // After structured clone, Buffer subclasses become plain Uint8Arrays.
260
+ // Restore them to Buffer so downstream code gets the same type as in
261
+ // direct mode for non-protobuf binary fields.
262
+ if (value instanceof Uint8Array && !Buffer.isBuffer(value)) {
263
+ return Buffer.from(value);
264
+ }
237
265
  if (value !== null && typeof value === 'object') {
238
266
  if (value.__error__) {
267
+ // Boom errors carry status codes, payloads, and extra data. Rebuild
268
+ // a real Boom instance so callers can inspect err.output.statusCode.
269
+ if (value.__boom__) {
270
+ const boom = new boom_1.Boom(value.message, {
271
+ statusCode: (_a = value.output) === null || _a === void 0 ? void 0 : _a.statusCode,
272
+ data: value.data
273
+ });
274
+ // Restore the exact output payload/headers from the worker so the
275
+ // status code and error name match direct mode.
276
+ if (value.output) {
277
+ Object.assign(boom.output, value.output);
278
+ }
279
+ boom.stack = value.stack || boom.stack;
280
+ return boom;
281
+ }
239
282
  const err = new Error(value.message);
240
283
  err.name = value.name;
241
284
  err.stack = value.stack;
242
285
  return err;
243
286
  }
287
+ // Revive built-in value objects tagged by the child.
288
+ if (value.__date__) {
289
+ return new Date(value.value);
290
+ }
291
+ if (value.__regexp__) {
292
+ return new RegExp(value.source, value.flags);
293
+ }
294
+ if (value.__url__) {
295
+ return new URL(value.href);
296
+ }
297
+ if (value.__map__) {
298
+ return new Map(revivePostMessage(value.entries));
299
+ }
300
+ if (value.__set__) {
301
+ return new Set(revivePostMessage(value.entries));
302
+ }
303
+ // Revive protobuf messages tagged by the child. fromObject() will
304
+ // recursively recreate nested protobuf instances and convert base64
305
+ // strings back into Buffer instances.
306
+ const protobufType = value.__protobufType__;
307
+ if (protobufType) {
308
+ const type = PROTOBUF_TYPE_REGISTRY.get(protobufType);
309
+ if (type === null || type === void 0 ? void 0 : type.fromObject) {
310
+ const copy = { ...value };
311
+ delete copy.__protobufType__;
312
+ try {
313
+ return type.fromObject(copy);
314
+ }
315
+ catch (_b) {
316
+ // Fall back to the plain JSON object if fromObject fails;
317
+ // downstream code will see base64 strings instead of Buffers.
318
+ }
319
+ }
320
+ }
244
321
  // Revive Long-like objects that were decomposed by structured clone.
245
322
  // protobufjs uses Long for 64-bit integers; after postMessage they
246
323
  // become { low, high, unsigned } plain objects.
@@ -248,7 +325,7 @@ function revivePostMessage(value) {
248
325
  return new long_1.default(value.low, value.high, value.unsigned);
249
326
  }
250
327
  if (Array.isArray(value)) {
251
- // Arrays: recurse to revive any Error sentinels / Longs inside
328
+ // Arrays: recurse to revive any Error sentinels / Longs / Uint8Arrays / protobuf messages / built-ins inside
252
329
  let changed = false;
253
330
  const result = value.map(v => {
254
331
  const revived = revivePostMessage(v);
@@ -258,8 +335,8 @@ function revivePostMessage(value) {
258
335
  });
259
336
  return changed ? result : value;
260
337
  }
261
- // Plain objects: only recurse if they might contain __error__ sentinels
262
- // or Long-like objects.
338
+ // Plain objects: only recurse if they might contain __error__ sentinels,
339
+ // Long-like objects, Uint8Arrays, protobuf markers, or built-in tags.
263
340
  if (value.constructor === Object || value.constructor === undefined) {
264
341
  let changed = false;
265
342
  const result = {};
@@ -471,7 +548,12 @@ async function handleCallbackCall(slot, socketId, msg) {
471
548
  try {
472
549
  log === null || log === void 0 ? void 0 : log.debug({ keystoreMethod: method, argCount: args.length }, 'invoking keystore method');
473
550
  const result = await keystore[method](...args);
474
- slot.worker.postMessage({ type: 'callback-result', socketId, id, result });
551
+ slot.worker.postMessage({
552
+ type: 'callback-result',
553
+ socketId,
554
+ id,
555
+ result: deepStripFunctions(result)
556
+ });
475
557
  }
476
558
  catch (err) {
477
559
  log === null || log === void 0 ? void 0 : log.error({ err, keystoreMethod: method }, 'keystore method threw error');
@@ -499,7 +581,12 @@ async function handleCallbackCall(slot, socketId, msg) {
499
581
  try {
500
582
  log === null || log === void 0 ? void 0 : log.debug({ callbackKey: key, argCount: args.length }, 'invoking proxied callback');
501
583
  const result = await fn(...args);
502
- slot.worker.postMessage({ type: 'callback-result', socketId, id, result });
584
+ slot.worker.postMessage({
585
+ type: 'callback-result',
586
+ socketId,
587
+ id,
588
+ result: deepStripFunctions(result)
589
+ });
503
590
  }
504
591
  catch (err) {
505
592
  log === null || log === void 0 ? void 0 : log.error({ err, callbackKey: key }, 'proxied callback threw error');
@@ -572,8 +659,30 @@ async function drainPool() {
572
659
  * Recursively clone a value, replacing all functions with the sentinel
573
660
  * string '__proxy_fn__'. This ensures the config can be sent via
574
661
  * postMessage (structured clone).
662
+ *
663
+ * Built-in value objects (Date, RegExp, URL, Map, Set) are preserved in
664
+ * their serializable tagged form so they survive the boundary instead of
665
+ * being reduced to empty objects by Object.keys().
575
666
  */
576
667
  function deepStripFunctions(value, seen = new WeakSet()) {
668
+ if (typeof value === 'function') {
669
+ return '__proxy_fn__';
670
+ }
671
+ if (value instanceof Date) {
672
+ return { __date__: true, value: value.toISOString() };
673
+ }
674
+ if (value instanceof RegExp) {
675
+ return { __regexp__: true, source: value.source, flags: value.flags };
676
+ }
677
+ if (value instanceof URL) {
678
+ return { __url__: true, href: value.href };
679
+ }
680
+ if (value instanceof Map) {
681
+ return { __map__: true, entries: deepStripFunctions(Array.from(value.entries()), seen) };
682
+ }
683
+ if (value instanceof Set) {
684
+ return { __set__: true, entries: deepStripFunctions(Array.from(value.values()), seen) };
685
+ }
577
686
  if (typeof value === 'function') {
578
687
  return '__proxy_fn__';
579
688
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libzapitu",
3
- "version": "1.0.0-alpha.24",
3
+ "version": "1.0.0-alpha.26",
4
4
  "description": "A WebSockets library for interacting with WhatsApp Web",
5
5
  "keywords": [
6
6
  "whatsapp",