Files

49 lines
2.0 KiB
JavaScript
Raw Permalink Normal View History

/**
* Result callback for in-app browser actions.
*
* The client that owns the browser view posts here with the outcome of a
* request it received over the event stream. Only the request id is trusted to
* correlate; an unknown id is accepted with `matched: false` rather than an
* error, because a client answering after a timeout has done nothing wrong.
*/
export function registerBrowserControlRoutes(app, { express, broker }) {
// Claiming is separate from answering so that a client learns whether it may
// act *before* it acts. Deciding by whose result arrives first would be too
// late: by then every client has already clicked.
app.post('/api/browser-control/claim', express.json({ limit: '4kb' }), (req, res) => {
const requestId = typeof req.body?.requestId === 'string' ? req.body.requestId.trim() : '';
if (!requestId) {
res.status(400).json({ error: 'requestId is required' });
return;
}
res.json({ granted: broker.claim(requestId) });
});
// This server attaches body parsing per route rather than globally. Without
// it `req.body` is undefined here, the client's result is rejected, and the
// agent sees an unexplained timeout instead of its answer. A page snapshot
// carries the visible text plus every interactive element, so the limit is
// sized for that rather than for a small control message.
app.post('/api/browser-control/result', express.json({ limit: '2mb' }), (req, res) => {
const body = req.body;
if (!body || typeof body !== 'object') {
res.status(400).json({ error: 'A JSON body is required' });
return;
}
const requestId = typeof body.requestId === 'string' ? body.requestId.trim() : '';
if (!requestId) {
res.status(400).json({ error: 'requestId is required' });
return;
}
const matched = broker.resolve(requestId, {
ok: body.ok === true,
data: body.data ?? null,
error: typeof body.error === 'string' ? body.error : '',
});
res.json({ matched });
});
}