openvibers/openvibe.bench Commits
Showcase: a generated answer renders from its entry type's field (#23)
Changes
@@@ -245,7 +245,9 @@ function createBenchAi({ config, fetchImpl = globalThis.fetch, log = console })const provider = String(prov.provider || run.provider || (slot && slot.provider) || 'ai').slice(0, 80);const ui_check = run.ui_check || (run.output && run.output.ui_check) || null;const html = /html_page/.test(kind || '') ? sanitizeHtml(text, { maxBytes: ai.maxHtmlBytes }) : text;-- const payload = /html_page/.test(kind || '') ? { html } : { text };++ // A showcase entry renders from its type's field (render.js payloadFor): { html } for a page that runs in the++ // isolated frame (not sanitized: its scripts are the point), { svg } or { glsl }. html_page stays the inert sandbox.++ const payload = /html_page/.test(kind || '') ? { html } : require('./render').payloadFor(kind, text);return {payload,html,@@@ -48,6 +48,29 @@ const MAX_ENTRY_BYTES = MAX_BYTES;const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));++/** The payload field each showcase entry type renders from. A model answers in text; this is where that text goes. */++const PAYLOAD_FIELD = Object.freeze({++ html_game: 'html', three_scene: 'html', motion_html: 'html', ui_page: 'html', dataviz_page: 'html', component_set: 'html',++ svg_art: 'svg', shader_glsl: 'glsl',++});++/** The code inside a model's answer: the first fenced block when it wrote one (```html … ```), else the whole answer. */++function codeOf(text) {++ const t = String(text == null ? '' : text);++ const m = /```[\w-]*[^\S\n]*\n([\s\S]*?)```/.exec(t);++ return (m ? m[1] : t).trim();++}++/** A generated answer as the payload its entry type renders: { html } | { svg } | { glsl }, or { text } for the rest. */++function payloadFor(entryType, text) {++ const field = PAYLOAD_FIELD[entryType];++ return field ? { [field]: codeOf(text) } : { text: String(text == null ? '' : text) };++}++/** An entry stored before payloadFor existed held only { text }: read it as its entry type's field. */++function renderablePayload(kind, payload) {++ const p = payload || {};++ if (p.html || p.svg || p.glsl || p.gltf_base64 || typeof p.text !== 'string') return p;++ return { ...p, ...payloadFor(kind && kind.entry, p.text) };++}++/** The CSP a model document runs under. The nonce is ours; model scripts get it, nothing else does. */function cspFor(nonce) {return [@@@ -116,7 +139,7 @@ function importMap() {/** The model document for a script kind: the model's own HTML, under our nonce and our CSP. */function modelDocument(kind, payload, opts) {const nonce = (opts && opts.nonce) || crypto.randomBytes(16).toString('base64');-- const p = payload || {};++ const p = renderablePayload(kind, payload);let body = '';if (typeof p.html === 'string' && p.html.trim()) body = p.html;else if (typeof p.glsl === 'string' && p.glsl.trim()) body = glslPage(p.glsl);@@@ -215,5 +238,5 @@ function wrapperDocument(opts) {module.exports = {RENDER_ORIGIN, PINNED_ORIGINS, VENDOR, SANDBOX, WRAPPER_SANDBOX, DEFAULT_LIMIT_MS, MAX_ENTRY_BYTES,-- FRAME_ANCESTORS, wrapperCsp, cspFor, applyNonce, sanitizeSvg, limitMsOf, modeOf, modelDocument, wrapperDocument,++ FRAME_ANCESTORS, PAYLOAD_FIELD, codeOf, payloadFor, renderablePayload, wrapperCsp, cspFor, applyNonce, sanitizeSvg, limitMsOf, modeOf, modelDocument, wrapperDocument,};@@@ -58,7 +58,8 @@ function createRenderRoutes(ctx) {const kind = entry.kind;const nonce = crypto.randomBytes(16).toString('base64');if (kind && renderLib.modeOf(kind) === 'static') {-- const svg = renderLib.sanitizeSvg(entry.payload && entry.payload.svg ? entry.payload.svg : (entry.payload && entry.payload.html) || '');++ const p = renderLib.renderablePayload(kind, entry.payload);++ const svg = renderLib.sanitizeSvg(p.svg || p.html || '');return bare(res).set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox").type('image/svg+xml')@@@ -113,6 +113,15 @@ const HOSTILE = [assert.ok(own.includes('<script nonce="N2" type="module">') && !own.includes('guess'), 'a guessed nonce is replaced by ours');});++ await check('a generated answer renders from its entry type\'s field (10-30: every seeded entry was { text } and showed nothing)', async () => {++ assert.deepStrictEqual(renderLib.payloadFor('ui_page', 'Here you go:\n```html\n<p>hi</p>\n```\nEnjoy.'), { html: '<p>hi</p>' });++ assert.deepStrictEqual(renderLib.payloadFor('svg_art', '<svg viewBox="0 0 1 1"></svg>'), { svg: '<svg viewBox="0 0 1 1"></svg>' });++ assert.deepStrictEqual(renderLib.payloadFor('shader_glsl', '```glsl\nvoid main(){}\n```'), { glsl: 'void main(){}' });++ assert.deepStrictEqual(renderLib.payloadFor('model_answer', 'plain'), { text: 'plain' });++ const doc = renderLib.modelDocument({ id: 'design.ui', entry: 'ui_page' }, { text: '```html\n<!doctype html><html><head></head><body><b>seeded</b></body></html>\n```' }, { nonce: 'N3' });++ assert.ok(doc.includes('<b>seeded</b>') && !doc.includes('Nothing was generated'), 'an old { text } entry renders');++ });++await check('the render origin vendors three 0.170.0 for opaque-origin frames', async () => {const v = await t.get('/vendor/three.module.js');assert.strictEqual(v.status, 200);
@@ -245,7 +245,9 @@ function createBenchAi({ config, fetchImpl = globalThis.fetch, log = console })
@@ -245,7 +245,9 @@ function createBenchAi({ config, fetchImpl = globalThis.fetch, log = console })
const provider = String(prov.provider || run.provider || (slot && slot.provider) || 'ai').slice(0, 80);
const provider = String(prov.provider || run.provider || (slot && slot.provider) || 'ai').slice(0, 80);
const ui_check = run.ui_check || (run.output && run.output.ui_check) || null;
const ui_check = run.ui_check || (run.output && run.output.ui_check) || null;
const html = /html_page/.test(kind || '') ? sanitizeHtml(text, { maxBytes: ai.maxHtmlBytes }) : text;
const html = /html_page/.test(kind || '') ? sanitizeHtml(text, { maxBytes: ai.maxHtmlBytes }) : text;
- const payload = /html_page/.test(kind || '') ? { html } : { text };
+ // A showcase entry renders from its type's field (render.js payloadFor): { html } for a page that runs in the
+ // isolated frame (not sanitized: its scripts are the point), { svg } or { glsl }. html_page stays the inert sandbox.
+ const payload = /html_page/.test(kind || '') ? { html } : require('./render').payloadFor(kind, text);
return {
return {
payload,
payload,
html,
html,
@@ -48,6 +48,29 @@ const MAX_ENTRY_BYTES = MAX_BYTES;
@@ -48,6 +48,29 @@ const MAX_ENTRY_BYTES = MAX_BYTES;
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+/** The payload field each showcase entry type renders from. A model answers in text; this is where that text goes. */
+const PAYLOAD_FIELD = Object.freeze({
+ html_game: 'html', three_scene: 'html', motion_html: 'html', ui_page: 'html', dataviz_page: 'html', component_set: 'html',
+ svg_art: 'svg', shader_glsl: 'glsl',
+});
+/** The code inside a model's answer: the first fenced block when it wrote one (```html … ```), else the whole answer. */
+function codeOf(text) {
+ const t = String(text == null ? '' : text);
+ const m = /```[\w-]*[^\S\n]*\n([\s\S]*?)```/.exec(t);
+ return (m ? m[1] : t).trim();
+}
+/** A generated answer as the payload its entry type renders: { html } | { svg } | { glsl }, or { text } for the rest. */
+function payloadFor(entryType, text) {
+ const field = PAYLOAD_FIELD[entryType];
+ return field ? { [field]: codeOf(text) } : { text: String(text == null ? '' : text) };
+}
+/** An entry stored before payloadFor existed held only { text }: read it as its entry type's field. */
+function renderablePayload(kind, payload) {
+ const p = payload || {};
+ if (p.html || p.svg || p.glsl || p.gltf_base64 || typeof p.text !== 'string') return p;
+ return { ...p, ...payloadFor(kind && kind.entry, p.text) };
+}
+
/** The CSP a model document runs under. The nonce is ours; model scripts get it, nothing else does. */
/** The CSP a model document runs under. The nonce is ours; model scripts get it, nothing else does. */
function cspFor(nonce) {
function cspFor(nonce) {
return [
return [
@@ -116,7 +139,7 @@ function importMap() {
@@ -116,7 +139,7 @@ function importMap() {
/** The model document for a script kind: the model's own HTML, under our nonce and our CSP. */
/** The model document for a script kind: the model's own HTML, under our nonce and our CSP. */
function modelDocument(kind, payload, opts) {
function modelDocument(kind, payload, opts) {
const nonce = (opts && opts.nonce) || crypto.randomBytes(16).toString('base64');
const nonce = (opts && opts.nonce) || crypto.randomBytes(16).toString('base64');
- const p = payload || {};
+ const p = renderablePayload(kind, payload);
let body = '';
let body = '';
if (typeof p.html === 'string' && p.html.trim()) body = p.html;
if (typeof p.html === 'string' && p.html.trim()) body = p.html;
else if (typeof p.glsl === 'string' && p.glsl.trim()) body = glslPage(p.glsl);
else if (typeof p.glsl === 'string' && p.glsl.trim()) body = glslPage(p.glsl);
@@ -215,5 +238,5 @@ function wrapperDocument(opts) {
@@ -215,5 +238,5 @@ function wrapperDocument(opts) {
module.exports = {
module.exports = {
RENDER_ORIGIN, PINNED_ORIGINS, VENDOR, SANDBOX, WRAPPER_SANDBOX, DEFAULT_LIMIT_MS, MAX_ENTRY_BYTES,
RENDER_ORIGIN, PINNED_ORIGINS, VENDOR, SANDBOX, WRAPPER_SANDBOX, DEFAULT_LIMIT_MS, MAX_ENTRY_BYTES,
- FRAME_ANCESTORS, wrapperCsp, cspFor, applyNonce, sanitizeSvg, limitMsOf, modeOf, modelDocument, wrapperDocument,
+ FRAME_ANCESTORS, PAYLOAD_FIELD, codeOf, payloadFor, renderablePayload, wrapperCsp, cspFor, applyNonce, sanitizeSvg, limitMsOf, modeOf, modelDocument, wrapperDocument,
};
};
@@ -58,7 +58,8 @@ function createRenderRoutes(ctx) {
@@ -58,7 +58,8 @@ function createRenderRoutes(ctx) {
const kind = entry.kind;
const kind = entry.kind;
const nonce = crypto.randomBytes(16).toString('base64');
const nonce = crypto.randomBytes(16).toString('base64');
if (kind && renderLib.modeOf(kind) === 'static') {
if (kind && renderLib.modeOf(kind) === 'static') {
- const svg = renderLib.sanitizeSvg(entry.payload && entry.payload.svg ? entry.payload.svg : (entry.payload && entry.payload.html) || '');
+ const p = renderLib.renderablePayload(kind, entry.payload);
+ const svg = renderLib.sanitizeSvg(p.svg || p.html || '');
return bare(res)
return bare(res)
.set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox")
.set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox")
.type('image/svg+xml')
.type('image/svg+xml')
@@ -113,6 +113,15 @@ const HOSTILE = [
@@ -113,6 +113,15 @@ const HOSTILE = [
assert.ok(own.includes('<script nonce="N2" type="module">') && !own.includes('guess'), 'a guessed nonce is replaced by ours');
assert.ok(own.includes('<script nonce="N2" type="module">') && !own.includes('guess'), 'a guessed nonce is replaced by ours');
});
});
+ await check('a generated answer renders from its entry type\'s field (10-30: every seeded entry was { text } and showed nothing)', async () => {
+ assert.deepStrictEqual(renderLib.payloadFor('ui_page', 'Here you go:\n```html\n<p>hi</p>\n```\nEnjoy.'), { html: '<p>hi</p>' });
+ assert.deepStrictEqual(renderLib.payloadFor('svg_art', '<svg viewBox="0 0 1 1"></svg>'), { svg: '<svg viewBox="0 0 1 1"></svg>' });
+ assert.deepStrictEqual(renderLib.payloadFor('shader_glsl', '```glsl\nvoid main(){}\n```'), { glsl: 'void main(){}' });
+ assert.deepStrictEqual(renderLib.payloadFor('model_answer', 'plain'), { text: 'plain' });
+ const doc = renderLib.modelDocument({ id: 'design.ui', entry: 'ui_page' }, { text: '```html\n<!doctype html><html><head></head><body><b>seeded</b></body></html>\n```' }, { nonce: 'N3' });
+ assert.ok(doc.includes('<b>seeded</b>') && !doc.includes('Nothing was generated'), 'an old { text } entry renders');
+ });
+
await check('the render origin vendors three 0.170.0 for opaque-origin frames', async () => {
await check('the render origin vendors three 0.170.0 for opaque-origin frames', async () => {
const v = await t.get('/vendor/three.module.js');
const v = await t.get('/vendor/three.module.js');
assert.strictEqual(v.status, 200);
assert.strictEqual(v.status, 200);