'use strict';
const { randomUUID } = require('node:crypto');
const { openOriginalChannelTransport } = require('./ChannelSessionTransport');
const { attachOriginalChannelSession } = require('./ChannelSessionPrimitives');
/**
* Borrows a transport in an existing Puppeteer realm selected by the channel
* owner. Never creates a page, worker, browser, login or WhatsApp socket.
* assertOwned is mandatory and must fence the caller's original realm/session.
* The owner signals navigation, worker loss and lease revocation via signal.
* This is an internal transport API, not a call engine or readiness claim.
*/
async function borrowOriginalChannelTransport(realm, signal, assertOwned) {
return borrowOriginalChannelHandle(realm, signal, assertOwned, false);
}
/** Same original realm, with opaque session operations (no private key export). */
async function borrowOriginalChannelSession(realm, signal, assertOwned) {
return borrowOriginalChannelHandle(realm, signal, assertOwned, true);
}
async function borrowOriginalChannelHandle(
realm,
signal,
assertOwned,
session,
) {
const fail = () => new Error('wwebjs_original_transport_unavailable');
if (
typeof realm?.evaluateHandle !== 'function' ||
!(signal instanceof AbortSignal) ||
typeof assertOwned !== 'function'
)
throw fail();
const lifetime = new AbortController();
let handle;
let stopped = false;
let disposing = Promise.resolve();
const dispose = (value) => {
if (!value) return;
disposing = Promise.resolve()
.then(() => value.evaluate((source) => source.close()))
.catch(() => {})
.finally(() => value.dispose().catch(() => {}));
};
const close = () => {
if (stopped) return;
stopped = true;
signal.removeEventListener('abort', close);
lifetime.abort();
dispose(handle);
};
const check = () => {
try {
if (stopped || signal.aborted) throw fail();
const result = assertOwned();
// A Promise here would silently bypass the caller's lease fence.
if (result && typeof result.then === 'function') {
void Promise.resolve(result).catch(() => {});
throw fail();
}
} catch {
close();
throw fail();
}
};
signal.addEventListener('abort', close, { once: true });
try {
check();
// Exactly once. A late handle is retired in its own original realm;
// acquisition is never retried in another page or replacement worker.
// Only our own static functions are serialized. No runtime input is
// interpolated into code, and nothing is installed in another realm.
handle = await realm.evaluateHandle(
session
? `(() => (${attachOriginalChannelSession.toString()})((${openOriginalChannelTransport.toString()})()))()`
: openOriginalChannelTransport,
);
if (stopped) dispose(handle);
check();
const generation = await handle.evaluate((source) => {
source.check();
return source.generation;
});
check();
if (
typeof generation !== 'string' ||
!/^[0-9.]+:[1-9][0-9]*$/.test(generation)
)
throw fail();
const identity = session
? await handle.evaluate((source) => source.identity())
: undefined;
check();
if (
session &&
(identity?.generation !== generation ||
typeof identity?.phone_jid !== 'string' ||
typeof identity?.lid !== 'string')
)
throw fail();
// These two waits follow the borrowed source's lifetime, not Puppeteer's
// ordinary command timeout. Keep the exact handle's CDP session; never
// create another target or change the browser-wide protocol timeout.
const objectId = handle.remoteObject?.().objectId;
const client = handle.client;
if (!objectId || typeof client?.send !== 'function') throw fail();
const waitForEvent = async (method, args) => {
check();
const response = await client.send(
'Runtime.callFunctionOn',
{
objectId,
functionDeclaration: function (name, input) {
this.check();
return this[name](...input);
}.toString(),
arguments: [{ value: method }, { value: args }],
awaitPromise: true,
returnByValue: true,
},
{ timeout: 0 },
);
if (response.exceptionDetails || !response.result) throw fail();
return response.result.value;
};
// One pending socket event, not an HTTP loop or a status poll.
void waitForEvent('closed', []).then(close, close);
const run = async (method, args, cancellation, operationId) => {
if (!(cancellation instanceof AbortSignal)) throw fail();
check();
if (cancellation.aborted) throw fail();
const combined = AbortSignal.any([lifetime.signal, cancellation]);
let abort;
const cancelled = new Promise((_, reject) => {
abort = () => {
// Cancelling an ACK/event wait does not cancel or repeat a
// socket write already dispatched to the original realm.
if (operationId)
void handle
.evaluate(
(source, id) => source.cancel(id),
operationId,
)
.catch(close);
reject(fail());
};
combined.addEventListener('abort', abort, { once: true });
});
try {
check();
if (combined.aborted) throw fail();
const operation =
method === 'next'
? waitForEvent(method, args)
: handle.evaluate(
(source, name, input) => {
source.check();
return source[name](...input);
},
method,
args,
);
const result = await Promise.race([operation, cancelled]);
check();
if (combined.aborted) throw fail();
return result;
} catch {
throw fail();
} finally {
combined.removeEventListener('abort', abort);
}
};
const sessionOperation = (method, input, cancellation) => {
const id = randomUUID();
return run(method, [id, ...input], cancellation, id);
};
return Object.freeze({
generation,
signal: lifetime.signal,
check,
close,
disposed: () => disposing,
send: (node, cancellation) => run('send', [node], cancellation),
request: (node, id, cancellation) =>
run('request', [node, id], cancellation, id),
next: (cancellation) => {
const id = randomUUID();
return run('next', [id], cancellation, id);
},
...(session
? {
identity: () => {
check();
return { ...identity };
},
resolvePeerLid: (target, cancel) =>
sessionOperation('resolvePeerLid', [target], cancel),
getUserDevices: (users, cancel) =>
sessionOperation('getUserDevices', [users], cancel),
encryptSignal: (target, bytes, cancel) =>
sessionOperation(
'encryptSignal',
[target, bytes],
cancel,
),
decryptSignal: (
target,
type,
bytes,
signedIdentity,
cancel,
) =>
sessionOperation(
'decryptSignal',
[target, type, bytes, signedIdentity],
cancel,
),
deviceIdentity: (cancel) =>
sessionOperation('deviceIdentity', [], cancel),
privacyToken: (target, cancel) =>
sessionOperation('privacyToken', [target], cancel),
getGroupInfo: (target, cancel) =>
sessionOperation('getGroupInfo', [target], cancel),
}
: {}),
});
} catch {
close();
throw fail();
}
}
module.exports = {
borrowOriginalChannelTransport,
borrowOriginalChannelSession,
};