'use strict';
/**
* Serialized into the realm that ALREADY owns WAComms. This is a borrowed
* transport, not VoIP: no login, socket creation, call parser, ACK generation,
* media processing or retries. The application owns all call semantics.
*/
function openOriginalChannelTransport() {
const fail = () => new Error('wwebjs_original_transport_unavailable');
const req = globalThis.require;
if (typeof req !== 'function') throw fail();
const api = req('WAComms');
const wap = req('WAWap');
const { WapJid } = req('WAWapJid');
const comms = api.getComms();
const socket = comms?.socket;
const socketId = comms?.socketId;
const epoch = globalThis.performance?.timeOrigin;
if (
!socket ||
typeof socket.sendFrame !== 'function' ||
!Number.isSafeInteger(socketId) ||
socketId <= 0 ||
!Number.isFinite(epoch) ||
epoch <= 0 ||
typeof wap.encodeStanza !== 'function' ||
typeof wap.WapNode !== 'function' ||
typeof WapJid !== 'function'
)
throw fail();
const marker = Symbol.for('wwebjs.original-channel-transport.v1');
let owner = comms[marker];
if (owner && owner.active) throw fail();
if (!owner) {
const offline = req('WAWebOfflineHandler')?.OfflineMessageHandler;
const handler = Object.getOwnPropertyDescriptor(comms, 'handleStanza');
const connection = Object.getOwnPropertyDescriptor(
comms,
'onConnectionChange',
);
if (
typeof offline?.newOfflineStanza !== 'function' ||
!handler?.writable ||
typeof handler.value !== 'function' ||
!connection?.writable ||
typeof connection.value !== 'function' ||
!Object.isExtensible(comms)
)
throw fail();
owner = { active: null };
const original = handler.value;
owner.handle = function (node, incomingSocketId, ...rest) {
const call =
node?.tag === 'call' ||
(node?.tag === 'ack' && node.attrs?.class === 'call');
if (!call)
return original.call(this, node, incomingSocketId, ...rest);
// WAWebCommsHandleStanza normally performs this bookkeeping before
// dispatch. Bypassing it for calls must not starve offline_batch:
// a calls-only batch otherwise blocks all subsequent messages.
// Keep it independent of the borrowed call owner's lifetime.
if (
node.attrs?.offline &&
incomingSocketId === comms.socketId &&
api.getComms() === comms &&
globalThis.performance.timeOrigin === epoch
) {
offline.newOfflineStanza(
parseInt(node.attrs.t, 10),
rest[0] ?? 0,
parseInt(node.attrs.offline, 10),
);
}
// Never fall through to browser-native VoIP, even after close.
owner.active?.receive(node, incomingSocketId);
return Promise.resolve('NO_ACK');
};
owner.connection = function (...args) {
try {
return connection.value.apply(this, args);
} finally {
owner.active?.checkConnection();
}
};
Object.defineProperty(comms, marker, { value: owner });
comms.handleStanza = owner.handle;
comms.onConnectionChange = owner.connection;
}
if (
comms.handleStanza !== owner.handle ||
comms.onConnectionChange !== owner.connection
)
throw fail();
let stopped = false;
let resolveClosed;
const closed = new Promise((resolve) => {
resolveClosed = resolve;
});
const nodes = [];
const requests = new Map();
let receiver = null;
const limit = 4 * 1024 * 1024;
let queuedBytes = 0;
const id = (value) =>
typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,128}$/.test(value);
const current = () => {
try {
return (
!stopped &&
owner.active === active &&
api.getComms() === comms &&
comms.socket === socket &&
comms.socketId === socketId &&
globalThis.performance.timeOrigin === epoch &&
comms.handleStanza === owner.handle &&
comms.onConnectionChange === owner.connection
);
} catch {
return false;
}
};
const close = () => {
if (stopped) return;
stopped = true;
if (owner.active === active) owner.active = null;
nodes.length = 0;
queuedBytes = 0;
receiver?.reject(fail());
receiver = null;
for (const pending of requests.values()) pending.reject(fail());
requests.clear();
resolveClosed();
};
const check = () => {
if (!current()) {
close();
throw fail();
}
};
const text = (value) => {
if (
typeof value !== 'string' ||
value.length > limit ||
!value.isWellFormed()
)
throw fail();
return value;
};
const jidParts = (value) => {
if (value === 'call') return { user: null, server: 'call' };
const match =
/^([a-zA-Z0-9-]{1,192})(?::([1-9][0-9]{0,2}))?@(s\.whatsapp\.net|lid|hosted|hosted\.lid|g\.us|broadcast|newsletter|call)$/.exec(
value,
);
if (
!match ||
Number(match[2] ?? 0) > 255 ||
(match[2] &&
!['s.whatsapp.net', 'lid', 'hosted', 'hosted.lid'].includes(
match[3],
))
)
throw fail();
return {
user: match[1],
device: Number(match[2] ?? 0),
server: match[3],
};
};
const domains = new Map([
['s.whatsapp.net', 0],
['lid', 1],
['hosted', 128],
['hosted.lid', 129],
]);
const toJid = (value) => {
const parts = jidParts(value);
return parts.device || parts.server.startsWith('hosted')
? WapJid.createJidU(
parts.user,
domains.get(parts.server),
parts.device,
)
: WapJid.create(parts.user, parts.server);
};
const fromJid = (value) => {
const parts = value.getInnerJid();
if (parts.type !== 0 && parts.type !== 1) throw fail();
if (parts.agent && parts.agent !== 0) throw fail();
if (parts.type === 1 && ![0, 1, 128, 129].includes(parts.domainType))
throw fail();
const result = text(value.toString());
jidParts(result);
return result;
};
const encodeBytes = (bytes) => {
if (!(bytes instanceof Uint8Array) || bytes.length > limit)
throw fail();
let binary = '';
for (let offset = 0; offset < bytes.length; offset += 8192)
binary += String.fromCharCode(
...bytes.subarray(offset, offset + 8192),
);
return btoa(binary);
};
const decodeBytes = (input) => {
const value = input ?? '';
if (
typeof value !== 'string' ||
value.length > Math.ceil(limit / 3) * 4 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
value,
)
)
throw fail();
const raw = atob(value);
if (btoa(raw) !== value || raw.length > limit) throw fail();
return Uint8Array.from(raw, (char) => char.charCodeAt(0));
};
const object = (value, allowed) => {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).some((key) => !allowed.includes(key))
)
throw fail();
return value;
};
const convert = (root, inbound) => {
let count = 0,
bytes = 0;
const owned = [];
const budget = (value) => {
bytes += new TextEncoder().encode(text(value)).length;
if (bytes > limit) throw fail();
return value;
};
const walk = (node, depth) => {
if (
!node ||
typeof node !== 'object' ||
depth > 16 ||
++count > 2048
)
throw fail();
if (!inbound) object(node, ['tag', 'attrs', 'content']);
const tag = budget(node.tag);
if (
!tag ||
tag.length > 128 ||
tag.includes(String.fromCharCode(0)) ||
/[\r\n\t ]/.test(tag)
)
throw fail();
const attrs = Object.create(null);
const source = node.attrs ?? {};
if (
typeof source !== 'object' ||
Array.isArray(source) ||
Object.keys(source).length > 64
)
throw fail();
for (const [name, raw] of Object.entries(source)) {
if (
!name ||
name.length > 128 ||
['__proto__', 'prototype', 'constructor'].includes(name)
)
throw fail();
budget(name);
if (inbound)
attrs[name] =
raw instanceof WapJid
? { jid: budget(fromJid(raw)) }
: { text: budget(raw) };
else {
object(raw, ['text', 'jid']);
if (Object.keys(raw).length !== 1) throw fail();
attrs[name] = Object.hasOwn(raw, 'jid')
? toJid(budget(raw.jid))
: budget(raw.text);
}
}
let content = null;
if (inbound) {
if (node.content instanceof Uint8Array) {
bytes += node.content.length;
if (bytes > limit) throw fail();
content = {
kind: 'bytes',
bytes: encodeBytes(node.content),
};
} else if (Array.isArray(node.content)) {
if (node.content.length > 2048 - count) throw fail();
content = {
kind: 'nodes',
nodes: node.content.map((child) =>
walk(child, depth + 1),
),
};
} else if (node.content != null) throw fail();
} else if (node.content != null) {
const value = node.content;
if (value.kind === 'bytes') {
object(value, ['kind', 'bytes']);
content = decodeBytes(value.bytes);
owned.push(content);
bytes += content.length;
if (bytes > limit) throw fail();
} else if (value.kind === 'text') {
object(value, ['kind', 'text']);
content = new TextEncoder().encode(
budget(value.text ?? ''),
);
owned.push(content);
} else if (value.kind === 'nodes') {
object(value, ['kind', 'nodes']);
const children = value.nodes ?? [];
if (
!Array.isArray(children) ||
children.length > 2048 - count
)
throw fail();
content = children.map((child) => walk(child, depth + 1));
} else throw fail();
}
return inbound
? { tag, attrs, ...(content ? { content } : {}) }
: new wap.WapNode(tag, attrs, content);
};
try {
return {
node: walk(root, 0),
clear: () => owned.forEach((value) => value.fill(0)),
};
} catch {
owned.forEach((value) => value.fill(0));
throw fail();
}
};
const isCall = (node) =>
node?.tag === 'call' ||
(node?.tag === 'ack' && node.attrs?.class?.text === 'call');
const active = {
receive(node, incomingSocketId) {
try {
check();
if (incomingSocketId !== socketId) {
close();
return;
}
const converted = convert(node, true).node;
const requestId = converted.attrs.id?.text;
const pending =
converted.tag === 'ack' ? requests.get(requestId) : null;
if (pending) {
// Keep the ID reserved until its write and ACK have both
// completed. Cancellation also must not allow ID reuse
// while the original write is still in flight.
pending.resolve(converted);
} else if (receiver) {
const pending = receiver;
receiver = null;
pending.resolve(converted);
} else {
const size = new TextEncoder().encode(
JSON.stringify(converted),
).length;
if (nodes.length >= 128 || queuedBytes + size > limit) {
close();
return;
}
nodes.push({ node: converted, size });
queuedBytes += size;
}
} catch {
close();
}
},
checkConnection() {
if (!current()) close();
},
};
owner.active = active;
const send = async (node) => {
check();
if (!isCall(node)) throw fail();
const converted = convert(node, false);
let frame;
try {
check();
frame = wap.encodeStanza(converted.node);
if (
!(frame instanceof Uint8Array) ||
frame.length === 0 ||
frame.length > limit
)
throw fail();
// Directly borrowed socket: WAComms' send helpers may retry or
// swallow write errors. This method does neither.
await socket.sendFrame(frame);
check();
} catch {
close();
throw fail();
} finally {
frame?.fill(0);
converted.clear();
}
};
return Object.freeze({
generation: `${epoch}:${socketId}`,
closed: () => closed,
check,
close,
send,
next(operationId) {
check();
if (!id(operationId) || receiver) return Promise.reject(fail());
const queued = nodes.shift();
if (queued) {
queuedBytes -= queued.size;
return Promise.resolve(queued.node);
}
return new Promise((resolve, reject) => {
receiver = { id: operationId, resolve, reject };
});
},
async request(node, requestId) {
check();
if (
!id(requestId) ||
node?.tag !== 'call' ||
node.attrs?.id?.text !== requestId ||
requests.has(requestId) ||
requests.size >= 128
)
throw fail();
const response = new Promise((resolve, reject) => {
requests.set(requestId, { resolve, reject });
});
// A synchronous write failure must not leave an unobserved rejection.
response.catch(() => {});
try {
await send(node);
const result = await response;
check();
return result;
} catch {
throw fail();
} finally {
requests.get(requestId)?.reject(fail());
requests.delete(requestId);
}
},
cancel(operationId) {
if (receiver?.id === operationId) {
receiver.reject(fail());
receiver = null;
}
const pending = requests.get(operationId);
if (pending) pending.reject(fail());
},
});
}
module.exports = { openOriginalChannelTransport };