Trust and CSP
Intent
One claim decides how quario fits a security review, and it’s worth stating before anything else.
quario trusts the host’s configuration. It trusts neither the report definition nor its render data.
The host is the code that creates the instance and gives it a report. quario trusts what it configures, because that lives in your own process. It covers registered functions and the query budgets. It also covers the scheme allowlist, fonts, and the license key. A definition is the opposite. An agent may have written it, or a user through an embedded editor. So the engine treats it as untrusted input, the same as render data. Nothing a definition says executes, and nothing it says becomes markup.
Render data arrives already resolved, because the engine opens no connection of its own. The one place it waits for your code is an asynchronous registered function, which is host code you registered. Supplying the data describes where the data comes from.
Design decisions
Nothing turns a string into JavaScript. Expressions parse into closures
when a definition compiles. There is no eval, no new Function, and no code
generation anywhere in the render path, which is why a page can ship a script
policy that omits unsafe-eval. The engine’s suites run under Node’s
--disallow-code-generation-from-strings, a source scan, and browsers
loading the published files under a strict policy.
Escaping belongs to the target, at its own edge. The event stream carries
unescaped values. @quario/html escapes every character a definition or its
data contributes: literal template text, header cells and interpolated values,
and every generated attribute. A consumer that embeds stream values in markup
owns its own escaping.
There is no unescaped form. {{{ }}} is a definition error rather than an
escape hatch, so no schema syntax can exempt a value from escaping.
Formatting arrives only as declarations. Markup in a definition’s own text
makes no element in any target. Emphasis comes from style and styled runs,
which a target turns into attributes it escapes.
A location is the one value the engine judges. Every other declaration
reaches the target as the author wrote it, because a bad-looking render is a
recoverable fault. A URL isn’t: javascript: is a string-to-code path, and
escaping at the markup edge can’t answer it. So the engine admits an href
before it crosses, against an allowlist the host owns. What no target ever
receives is a string it has to decide about.
A report reads your data and can’t assign to it. Expressions also inherit
the expression engine’s guards, so __proto__, constructor and prototype
never resolve, and a function comes only from the registry you passed. An
expression has no loops and can’t call itself, so a definition can’t spin a
render forever either.
The no-string-to-code guarantee covers script execution, not every
directive. Conditional formatting emits inline style attributes, so a
restrictive style-src has to account for them. An image item’s data: URI
needs img-src data:. The viewer and the editor paint an SVG image from a
data: URI as well, so a page that previews one needs the same grant. A PNG or
JPEG there needs none. A grant for content you asked for is an ordinary
deployment requirement, not a hole.
The unlicensed marking never depends on a grant. A blocked marking would fail open, so it’s the one output that must render without one.
API walkthrough
Bounding what a render may read
The JSONPath selection is the one place a hostile dataset can cost you time rather than correctness. Budgets are an API argument, never a schema field, so a definition can’t raise its own ceiling.
import { quario } from 'quario'
const q = quario({ query: { maxNodes: 10_000, maxDepth: 64, maxResults: 1_000 } })maxNodes bounds how many nodes the traversal visits. maxDepth bounds
how deep it goes. maxResults bounds how many rows it may select.
A budget failure points at data, keeps its RangeError type, and carries
code, limit and actual. Every render starts with fresh counters, so one
expensive dataset can’t exhaust the next.
quario() checks the budgets when you call it, so a bad value fails at startup
even for a report that never runs a query.
Admitting the URLs a document may link to
quario({ schemes }) is that allowlist, defaulting to https: and
mailto:. Each entry is lower-case and ends in a colon. A value the engine
can’t read as an allowlist, null included, throws a TypeError at
quario(), prefixed options.schemes:.
import { quario } from 'quario'
const q = quario({ locale: 'en-IE', schemes: ['https:'] })A refused value drops out, so the text renders and the link doesn’t. A URL
arrives from data, and failing the whole render the day a row changed would
break a report that was working. A URL stating no scheme falls with the rest. A
scheme list can’t say “no scheme” without also admitting //host/path, which
states none and leaves the document all the same.
The allowlist is the host’s, never the document’s. A report that could widen
it would be author data authorising itself. A schemes value the engine can’t
read throws at construction rather than using the default. An allowlist quietly
replaced means a host believes it restricted something it didn’t.
An href beginning # is an internal target naming a group’s label. It
leaves the document by no scheme, so no allowlist reads it.
What the boundary doesn’t claim
A registered function is host code, and quario doesn’t limit what it does once the engine calls it. Whoever registers one is inside the boundary, so register only functions you would run on any definition.
quario guarantees that nothing a definition says executes. It doesn’t guarantee that the report is right. A definition an agent wrote can still read the wrong field or total the wrong column, which is what validation and review are for.
Where the claim holds
In HTML, the target escapes every character a definition or its data contributes, without exception.
In a workbook, the target writes no formula from a definition or a data
value, so a cell holding =SUM(A1:A9) is a string that says so.
In CSV, the target mangles formula-like text on write, whichever of the two it came from. That’s this format’s version of the same rule.
Environments
The source and the builds are runtime-neutral JavaScript. Node.js LTS and Chromium under a strict Content Security Policy are the tested environments. Every package is ESM-only.