Source: session/ChannelSessionTransport.js

'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 handler = Object.getOwnPropertyDescriptor(comms, 'handleStanza');
        const connection = Object.getOwnPropertyDescriptor(
            comms,
            'onConnectionChange',
        );
        if (
            !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);
            // 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 };