@remcp/runtime 0.2.161 → 0.2.162
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.
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// A workflow is explicitly requested by the caller after inspecting the page.
|
|
3
3
|
// No arbitrary JavaScript, cross-target access, file uploads or implicit submits.
|
|
4
4
|
import { assertResolvedPageUrl } from './browser-endpoint.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { evaluate } from './browser-session.mjs';
|
|
6
6
|
|
|
7
7
|
const ACTIONS = new Set(['fill_form','set_value','select','focus','click','wait_for']);
|
|
8
8
|
const CONDITIONS = new Set(['selector','text','url_contains','load']);
|
|
@@ -45,7 +45,28 @@ export function validateBrowserWorkflow(steps, { validateFormFields }) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// A workflow is a single-page plan. Redirects across origins require a new
|
|
49
|
+
// snapshot and a separate deliberate action, even if both URLs are public.
|
|
50
|
+
export function assertWorkflowOrigin(expectedOrigin, currentUrl) {
|
|
51
|
+
let currentOrigin;
|
|
52
|
+
try { currentOrigin = new URL(currentUrl).origin; }
|
|
53
|
+
catch { throw new Error('Browser workflow page origin could not be verified'); }
|
|
54
|
+
if (expectedOrigin !== null && currentOrigin !== expectedOrigin) {
|
|
55
|
+
throw new Error('Browser workflow origin changed');
|
|
56
|
+
}
|
|
57
|
+
return currentOrigin;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Enforce the origin in the exact same page JS evaluation that writes a field.
|
|
61
|
+
// Caller-supplied selectors/values stay JSON data, not arbitrary JavaScript.
|
|
62
|
+
async function guardedPageFunction(session, trustedFunction, args, origin) {
|
|
63
|
+
const expression = '(() => { if (location.origin !== ' + JSON.stringify(origin) +
|
|
64
|
+
') throw new Error("workflow_origin_changed"); return (' + trustedFunction +
|
|
65
|
+
')(...' + JSON.stringify(args) + '); })()';
|
|
66
|
+
return evaluate(session, expression);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function waitForStep(session, step, started, expectedOrigin) {
|
|
49
70
|
const deadline = Math.min(started + 12000, Date.now() + step.timeout_ms);
|
|
50
71
|
const expression = step.condition === 'selector' ?
|
|
51
72
|
'Boolean(document.querySelector('+JSON.stringify(step.criterion)+'))'
|
|
@@ -67,7 +88,8 @@ async function waitForStep(session, step, started) {
|
|
|
67
88
|
await new Promise(resolve=>setTimeout(resolve, 75));
|
|
68
89
|
continue;
|
|
69
90
|
}
|
|
70
|
-
// Never accept a match on
|
|
91
|
+
// Never accept a match on an unsafe page or an unexpected origin.
|
|
92
|
+
assertWorkflowOrigin(expectedOrigin, state?.url);
|
|
71
93
|
await assertResolvedPageUrl(state?.url);
|
|
72
94
|
if (state?.matched) return;
|
|
73
95
|
await new Promise(resolve=>setTimeout(resolve, 75));
|
|
@@ -80,25 +102,28 @@ export async function executeBrowserWorkflow(session, steps, {
|
|
|
80
102
|
}) {
|
|
81
103
|
const started = Date.now();
|
|
82
104
|
const completed = [];
|
|
105
|
+
let boundOrigin = null;
|
|
83
106
|
for (let index = 0; index < steps.length; index++) {
|
|
84
107
|
const step = steps[index];
|
|
85
108
|
if (Date.now() - started > 12000) throw new Error('Workflow exceeded 12 second budget after '+completed.length+' steps');
|
|
86
109
|
try {
|
|
87
110
|
const currentUrl = await evaluate(session, 'location.href');
|
|
111
|
+
const currentOrigin = assertWorkflowOrigin(boundOrigin, currentUrl);
|
|
88
112
|
await assertResolvedPageUrl(currentUrl);
|
|
113
|
+
if (boundOrigin === null) boundOrigin = currentOrigin;
|
|
89
114
|
if (step.action === 'fill_form') {
|
|
90
|
-
await
|
|
115
|
+
await guardedPageFunction(session,pageFillForm,[step.fields],boundOrigin);
|
|
91
116
|
} else if (step.action === 'wait_for') {
|
|
92
|
-
await waitForStep(session,step,started);
|
|
117
|
+
await waitForStep(session,step,started,boundOrigin);
|
|
93
118
|
} else if (step.action === 'click') {
|
|
94
|
-
const bounds=await
|
|
119
|
+
const bounds=await guardedPageFunction(session,pageElementAction,[step.selector,null,'click',''],boundOrigin);
|
|
95
120
|
const x=Number(bounds.x)+Number(bounds.width)/2;
|
|
96
121
|
const y=Number(bounds.y)+Number(bounds.height)/2;
|
|
97
122
|
await session.send('Input.dispatchMouseEvent',{type:'mouseMoved',x,y,button:'none'});
|
|
98
123
|
await session.send('Input.dispatchMouseEvent',{type:'mousePressed',x,y,button:'left',clickCount:1});
|
|
99
124
|
await session.send('Input.dispatchMouseEvent',{type:'mouseReleased',x,y,button:'left',clickCount:1});
|
|
100
125
|
} else {
|
|
101
|
-
await
|
|
126
|
+
await guardedPageFunction(session,pageElementAction,[step.selector,null,step.action,step.value??''],boundOrigin);
|
|
102
127
|
}
|
|
103
128
|
completed.push({index:index+1,action:step.action,ok:true});
|
|
104
129
|
} catch (error) {
|