openvibers/openvibe.tools Commits

Net page: each run's input is built from the tool's descriptor (plan T8 review): the page fetches GET /api/v1/tools/<id>/schema and sends only the fields its input schema declares (mx., txt., ns., spf., dkim. and dmarc. got a types field their schemas refuse, latency. a count), and the plain domain to dkim. (its selector as its own field) and dmarc. (the descriptor's route adds _dmarc. and <selector>._domainkey.; the page prefixed too). net-page-input.test.js checks every net tool's page input against its real schema.

OP OpenVibers committed 29 Sept 2026 · 78b24f8063 · 2 files, +110 −30


Changes

ddiff --git a/apps/gateway/public/net.html b/apps/gateway/public/net.html
iindex f44d9e7..f8e7846 100644
---- a/apps/gateway/public/net.html
++++ b/apps/gateway/public/net.html
@@@ -924,6 +924,41 @@
return (e && (e.detail || e.title || e.message)) || (body && (body.detail || body.title)) || 'Lookup failed';
}
++ // ── The run input, from the tool's descriptor ────────────
++ // Every net tool's input schema has additionalProperties: false, so a field it does not declare
++ // (types on mx., count on latency.) would refuse the run: send only what the schema lists.
++ const schemas = new Map();
++ function inputSchema(id) {
++ if (!schemas.has(id)) {
++ schemas.set(id, fetch(`${API_BASE}/${encodeURIComponent(id)}/schema`, { credentials: 'same-origin' })
++ .then(r => (r.ok ? r.json() : null))
++ .then(s => (s && s.$defs && s.$defs.input) || null)
++ .catch(() => null)
++ .then(s => { if (!s) schemas.delete(id); return s; }));
++ }
++ return schemas.get(id);
++ }
++ /** What the page collected for `tool`, as run input fields (before the schema filter). */
++ function runFields(tool, ui) {
++ const f = {};
++ if (ui.target) f.target = ui.target;
++ if (ui.types && ui.types.length) f.types = ui.types;
++ if (ui.selector) f.selector = ui.selector;
++ if (tool.count) f.count = tool.count;
++ if (ui.propType) f.type = ui.propType;
++ if (ui.smtpPort) f.port = parseInt(ui.smtpPort, 10);
++ const path = String(ui.robotsPath || '').trim();
++ if (path) { f.path = path; f.ua = ui.robotsUa; }
++ return f;
++ }
++ /** The fields `schema` declares (no schema, say the descriptor could not be fetched: the target only). */
++ function runInput(schema, fields) {
++ const props = (schema && schema.properties) || { target: {} };
++ const input = {};
++ for (const [k, v] of Object.entries(fields)) if (v !== undefined && Object.prototype.hasOwnProperty.call(props, k)) input[k] = v;
++ return input;
++ }
++
// ── Run the active tool ──────────────────────────────────
async function runTool() {
const input = document.getElementById('search-input');
@@@ -943,36 +978,22 @@
resultsEl.innerHTML = `<div class="loading"><i class="fa-solid fa-circle-notch fa-spin"></i><p>Running ${tool.name}…</p></div>`;
try {
-- // The tool's run API (plan T8, Phase 1): the same handlers the old /api/net routes called,
-- // in the run response shape. The request is the route's query as an `input` object.
-- const input = {};
-- if (target) input.target = target;
--
-- // DNS type overrides
-- if (tool.defaultTypes) input.types = tool.defaultTypes.split(',');
-- if (tool.dnsTypes) {
-- const sel = document.getElementById('dns-types');
-- if (sel) {
-- const selected = [...sel.selectedOptions].map(o => o.value);
-- if (selected.length) input.types = selected;
-- }
-- }
-- // DKIM special: query selector._domainkey.domain
-- if (activeTool === 'dkim' && target) {
-- const selector = prompt('DKIM selector (e.g., "default", "google", "s1"):', 'default') || 'default';
-- input.target = `${selector}._domainkey.${target}`;
-- }
-- // DMARC: prepend _dmarc.
-- if (activeTool === 'dmarc' && target) input.target = `_dmarc.${target}`;
-- // SPF: just TXT
-- if (activeTool === 'spf') input.types = ['TXT'];
-- if (tool.count) input.count = tool.count;
-- if (tool.propTypes) input.type = document.getElementById('prop-type').value;
-- if (tool.smtpPorts) input.port = parseInt(document.getElementById('smtp-port').value, 10);
-- if (tool.robotsTest) {
-- const p = document.getElementById('robots-path').value.trim();
-- if (p) { input.path = p; input.ua = document.getElementById('robots-ua').value; }
-- }
++ // The tool's run API (plan T8, Phase 1): the fields the page collected, kept only where the
++ // tool's own input schema (its descriptor, GET /api/v1/tools/<id>/schema) declares them. The
++ // descriptor adds the rest itself: MX/TXT/NS types, the _dmarc. and <selector>._domainkey.
++ // targets, the latency count.
++ let selector;
++ if (activeTool === 'dkim' && target) selector = prompt('DKIM selector (e.g., "default", "google", "s1"):', 'default') || 'default';
++ const fields = runFields(tool, {
++ target,
++ types: tool.dnsTypes && document.getElementById('dns-types') ? [...document.getElementById('dns-types').selectedOptions].map(o => o.value) : undefined,
++ selector,
++ propType: tool.propTypes ? document.getElementById('prop-type').value : undefined,
++ smtpPort: tool.smtpPorts ? document.getElementById('smtp-port').value : undefined,
++ robotsPath: tool.robotsTest ? document.getElementById('robots-path').value : undefined,
++ robotsUa: tool.robotsTest ? document.getElementById('robots-ua').value : undefined,
++ });
++ const input = runInput(await inputSchema(activeTool), fields);
const token = localStorage.getItem('ov_token') || getCookie('ov_token');
const headers = { 'Content-Type': 'application/json' };
ddiff --git a/apps/gateway/test/net-page-input.test.js b/apps/gateway/test/net-page-input.test.js
nnew file mode 100644
iindex 0000000..5a49ce8
---- /dev/null
++++ b/apps/gateway/test/net-page-input.test.js
@@@ -0,0 +1,59 @@
++'use strict';
++// net.html builds each run's input from the tool's descriptor (plan T8 review): every net tool's input
++// schema has additionalProperties: false, so the page must send only what that schema declares — no
++// `types` to mx./txt./ns./spf./dkim./dmarc. (their descriptor adds the query), no `count` to latency.,
++// and the plain domain to dkim. and dmarc. (the descriptor's route builds _dmarc.<d> and
++// <selector>._domainkey.<d>; a page that prefixed too asked for _dmarc._dmarc.<d>). The page's own
++// runFields/runInput are evaluated here against every descriptor's real schema with the run API's Ajv.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const { SPECS } = require('../server/net/descriptors');
++const { schemaDoc } = require('../../_shared/tools/descriptor');
++const { ajvFrom, createInputValidator } = require('../../_shared/tools/run');
++
++const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'net.html'), 'utf8');
++const slice = (from, to) => { const a = html.indexOf(from), b = html.indexOf(to, a); assert.ok(a >= 0 && b > a, `net.html: ${from}`); return html.slice(a, b); };
++const TOOLS = new Function(`${slice('const TOOLS = {', 'const CATS = {')}; return TOOLS;`)();
++const { runFields, runInput } = new Function(`${slice('function runFields(', '// ── Run the active tool')}; return { runFields, runInput };`)();
++
++const { Ajv, addFormats } = ajvFrom(require);
++const validate = createInputValidator(Ajv, addFormats);
++
++let checked = 0;
++for (const d of SPECS) {
++ if (!d.api || !d.input) continue;
++ const tool = TOOLS[d.id];
++ assert.ok(tool, `net.html has ${d.id}`);
++ // Everything the page's options could hold for this tool (the UI's own values).
++ const ui = {
++ target: 'example.com',
++ types: tool.dnsTypes ? ['A', 'MX'] : undefined,
++ selector: d.id === 'dkim' ? 's1' : undefined,
++ propType: tool.propTypes ? 'MX' : undefined,
++ smtpPort: tool.smtpPorts ? '587' : undefined,
++ robotsPath: tool.robotsTest ? '/private' : undefined,
++ robotsUa: tool.robotsTest ? 'GPTBot' : undefined,
++ };
++ const input = runInput(schemaDoc(d).$defs.input, runFields(tool, ui));
++ assert.deepStrictEqual(validate(d, input), [], `${d.id}: the page's input ${JSON.stringify(input)} passes its schema`);
++ if (d.input.required && d.input.required.includes('target')) assert.strictEqual(input.target, 'example.com', `${d.id}: the plain target (the descriptor builds any prefix)`);
++ checked++;
++}
++assert.ok(checked > 25, `every net tool with an API (${checked})`);
++
++// The specific cases the old page got wrong.
++const one = (id, ui) => runInput(schemaDoc(SPECS.find((d) => d.id === id)).$defs.input, runFields(TOOLS[id], { target: 'example.com', ...ui }));
++assert.deepStrictEqual(one('mx', {}), { target: 'example.com' }, 'mx: no types (the descriptor adds MX)');
++assert.deepStrictEqual(one('spf', {}), { target: 'example.com' });
++assert.deepStrictEqual(one('dmarc', {}), { target: 'example.com' }, 'dmarc: not _dmarc.example.com (the route adds it)');
++assert.deepStrictEqual(one('dkim', { selector: 'google' }), { target: 'example.com', selector: 'google' }, 'dkim: the selector as its own field');
++assert.deepStrictEqual(one('latency', {}), { target: 'example.com' }, 'latency: no count (the route asks for 10)');
++assert.deepStrictEqual(one('dig', { types: ['TXT'] }), { target: 'example.com', types: ['TXT'] }, 'dig keeps the chosen types');
++assert.deepStrictEqual(one('smtp', { smtpPort: '587' }), { target: 'example.com', port: 587 });
++assert.deepStrictEqual(runInput(null, { target: 'example.com', types: ['A'] }), { target: 'example.com' }, 'no schema (the fetch failed): the target only');
++const route = SPECS.find((d) => d.id === 'dmarc').route;
++assert.strictEqual(route.target, '_dmarc.{target}', 'the descriptor still owns the prefix');
++assert.ok(!/_dmarc\.\$\{|\._domainkey\.\$\{/.test(html), 'the page builds no _dmarc./_domainkey. target itself');
++
++console.log(`net page input: ${checked} tools' inputs built from their descriptors pass their schemas; dkim/dmarc unprefixed, no types/count where undeclared: all checks passed`);