Hosts do not get your plaintext.
The key stays with participants.
Cynario is a branching-scenario builder and player. Scenarios are plain files you own, and they run in the browser. Snapshot-link keys and compact live-room capabilities stay with participants; hosting and collaboration infrastructure receives no scenario plaintext.
How your data stays yours
Scenario plaintext stays with participants — even when infrastructure helps move it
Local authoring needs no central scenario service. Snapshot links use fragment-held decryption keys; compact live invitations use independent room capabilities and validated peer snapshots. Static hosts, signaling, and relays receive no scenario plaintext or raw bearer capability.
Relay can't decrypt.
by your peer.
The decryption key lives in the link's #fragment, which browsers do not include in ordinary HTTP requests. Lose every copy of the key and the ciphertext cannot be decrypted; leak the complete link and its holder can read the content.
Built for people who have to vouch for the tool
Three guarantees, in plain language
Local-first
The builder and player are static and client-side. Local authoring and play need no application database; optional sharing, catalog, and collaboration infrastructure stays outside the plaintext core.
Zero-knowledge sharing
By design, shared content is encrypted in your browser before it leaves and the link-fragment key is not sent in ordinary requests. Hosts and relays can still observe ciphertext and network metadata.
Inspectable content
No opaque package format. Each scenario is one human-readable file you can read, diff, and version in Git. Approve sensitive procedures before anyone runs them.
Sandboxed by design
Scenario logic is data, not a program with system access
Logic in Cynario runs on our own interpreter, not the browser's — no eval, no Function, none of the mechanisms that turn text into running code. Rules are a small tree of operations from a closed allow-list: compare, combine, count, branch, basic arithmetic, and a fixed menu of helper functions. Anything outside that menu is rejected, not run. Even the formula bar is parse-only — expressions are tokenized (never evaluated), mapped through a strict allow-list, and re-validated before they're stored, so the parser can't widen the sandbox. Rich media — slides, video, embeds — is a separate layer: it renders inside a locked-down, origin-less sandbox, contained by the same strict Content-Security-Policy.
What it deliberately can't do
- Touch your filesystem, clipboard, or other browser tabs
- Make network requests or phone activity home
- Execute arbitrary code — logic is declarative rules, not scripts
- Loop, hang, or run forever — the language has no iteration at all
- Persist anything beyond the scenario you’re running
That's why an imported scenario from a colleague is safe to open: its logic is declarative rules that can't run scripts, touch your files, or reach the network. Rich media renders inside a locked-down, origin-less sandbox with no access to your library or the page — and a strict Content-Security-Policy is the backstop.
It always finishes
There are no loops or iteration operators in the language, and recursion is depth-capped. A hostile rule can’t hang your tab by spinning forever.
It only sees its own variables
Variable lookups read own keys only and reject __proto__, prototype, and constructor — a scenario can’t reach into JavaScript’s internals or pollute them.
The math can’t misbehave
Non-numbers coerce to 0, division by zero yields 0, and NaN or Infinity never leak into your state.
Actions — the only thing that writes state — are just as constrained: they write declared variables only, never reserved or derived fields, and produce a fresh state map instead of mutating in place. Randomness and player input are recorded once and re-read on replay, so a run reproduces identically.
Defense in depth
Four surfaces, four boundaries
A scenario touches four things on its way to a learner — its content, its logic, the file it arrives as, and the network it crosses. Each is treated as untrusted, and each has a shipped control that contains it — independent layers, each assuming the one before it might have failed.
Content
Text and rich media authored into a scenario.
Rendered inside a locked-down, origin-less iframe — no access to your library, your session, or the page. Values bound into text are HTML-escaped on the way in.
Logic
The rules a scenario runs to branch and score.
A declarative allow-list — no eval, no Function, no network. Strict depth and size caps, reserved-key guards, and own-variables-only reads. It always terminates.
File format
The .cynario / JSON a scenario arrives as.
Untrusted by default. The import gate checks shape, caps, and the logic allow-list on your device before anything is saved — and fails closed, with no "load anyway".
Transport
How a shared scenario crosses the network.
Snapshot links carry AES-GCM ciphertext plus a fragment key. Compact live invites carry only a random room capability; a validated Scenario comes from a ready peer after Join. Transport services can still observe ordinary connection metadata.
The transport backstop
Strict security headers ship with every response. They're the origin-level floor the whole sandbox story stands on.
-
script-src 'self' + hashesOnly our own inline bootstraps, pinned by hash; no broad inline or eval permission. -
object-src 'none' · frame-ancestors 'none'No plugins, and the app can’t be framed by another site. -
HSTS · preloadHTTPS is pinned for two years and submitted to the preload list. -
COOP · nosniff · no-referrerOrigin isolation, no MIME sniffing, no referrer leakage. -
Permissions-PolicyCamera, mic, geolocation, USB — and FLoC — are switched off.
Player input is coerced at the door
The one value that enters the engine from outside the sandbox — what a player types — is bounded and cleaned before it's ever recorded or shown.
- Hard length cap — no runaway paste can bloat the run log or your storage
- Unicode NFC-normalized, so the recorded value replays identically
- Control, zero-width and bidi-override characters stripped (Trojan-source spoofing)
- Numbers must be finite and in range — a bad value blocks the submit, fail-closed
Rendering untrusted content
Author text is escaped, then scrubbed — one door, and it's locked
A scenario carries author-written content: markup, and values bound into that markup from scenario state. Both are untrusted. So content reaches the screen through a single chokepoint — there is no second path that skips it.
No scripts
<script> never survives.
No inline handlers
onerror=, onclick=, and every other on… attribute are removed.
No dangerous URLs
javascript: links are dropped. Links may only use http(s):, mailto:, tel:, page anchors, or relative paths.
No silent fetches to strangers
Anything the browser auto-loads with zero clicks — src, srcset, poster, background — may point only at inline (data:/blob:) or local targets. A URL reaching a remote server is dropped, so a scenario can’t quietly beacon out that you opened it. This mirrors the img-src CSP as a second, independent layer.
No form or style smuggling
<style>, <form>, <input>, <button>, <textarea>, <select>, and inline style= are all forbidden.
Fail closed
If the sanitizer is ever asked to run somewhere it can't safely do its job, it strips all tags rather than emit a single byte of unsanitized HTML. The safe failure is "show less," never "show more."
Before any of this, oversized or hostile markup is refused outright: byte caps at import, plus byte, tag-count, and nesting-depth caps in front of the template engine — so a malicious document degrades to a safe "too complex to render" notice instead of hanging your tab.
The import gate
Every file is checked on your device before it's ever saved
When you open a .cynario file, paste a scenario, or pull one from a catalog, it passes through an import gate that runs entirely on your device. The gate inspects the file without loading, running, or saving any part of it — and shows you a plain-language ledger of what it checked.
- 1 Shape
Is this actually a scenario — the right structure — or just a file pretending to be one?
- 2 Size
The whole file and every content field are byte-capped, so it can’t bloat your storage or stall the player.
- 3 Allow-list
Does every rule it contains use only the safe, fixed menu of operations — or does it ask for something that could be code?
- 4 Reserved keys
Does it try to write protected, derived state it shouldn’t?
- 5 Depth
Do its rules nest within the allowed limit, rather than burrowing deep enough to choke the player?
It fails closed, and stops at the first problem
If any check fails, the whole file is refused — there is no "load anyway" button. Checks after the failing one read as "not run," because we never partly trust a file. Only when a file passes is it sanitized into a fresh copy and saved.
The same gate sits in front of every way a scenario can enter — opened from disk, pasted as JSON, or fetched from a catalog. Even the samples bundled with the app go through it. There's no side door.
These boundaries aren't just described — they're exercised by a hostile-input test suite: script and beacon payloads through the real render path across every markup engine, import-gate abuse cases (forbidden operations, prototype pollution, depth and size bombs), and sanitizer fail-closed checks, all part of the project's test suite.
Provenance, not a safety gate
Signatures identify a signing key, not a person. The sandbox is what keeps you safe.
Cynario can pull scenarios from federated catalogs — every source is just a URL you choose to trust, with no central gatekeeper. The tempting move is to make a signature the thing that decides whether opening a file is safe. We deliberately don't: the sandbox, the sanitizer, and the import gate apply identically to catalog content, signed or not. That frees signing to do the job it's actually good at — proving that the holder of a known private key signed particular bytes, and that those bytes arrived intact. Mapping that key to a person or organization is a separate trust decision.
Integrity — sha-256
If a catalog entry declares a content hash, the bytes we fetch must hash to exactly that value. Any mismatch and we hand back nothing — tampered content is never loaded.
Key continuity — Ed25519
If a publisher key is pinned on a source, the entry must carry a valid signature over the same content. That proves the matching private key signed the bytes — not the legal identity of a person. Missing or invalid signatures fail closed.
Both checks are enforced fail-closed when the markers are present; bundled official content ships inside the app and skips the network checks. Honest status: the verification code is implemented and fails closed, but no live public catalog ships hashes or keys yet, so it hasn't been exercised against a real publisher. Either way, your safety doesn't depend on it — the sandbox protects signed and unsigned content alike.
What we don't claim
Where the model stops — stated plainly
A security page that only lists wins isn't trustworthy. Here's what this design deliberately does not do.
Signed results don’t prove fair play
A portable result can establish that its bytes are intact and that a particular key signed them. It does not prove which human held that key, or that the participant did not skip ahead, edit local state, or retry. High-stakes proctoring still needs an external trusted authority. Portable results are currently on feat/content-logic and awaiting promotion.
The share link is the real key
Encrypted sharing puts the decryption key in the link’s #fragment, which browsers do not send in ordinary HTTP requests. Anyone who gets the complete link gets the content, and the link can remain in browser history — treat it like the secret it is.
Slide decks are contained, not sanitized
Slides-mode content can run scripts — that’s what makes rich decks work. The containment is the boundary: an opaque-origin sandboxed iframe with no access to your library, your session, the share key, or the page, backed by the CSP. We claim containment, not absence.
Encryption does not erase metadata
A static host, catalog source, signaling service, relay, or container operator may see IP addresses, timing, request paths, and ciphertext sizes. Zero-knowledge describes the content boundary, not network anonymity.
Train on your hardest scenarios. Keep them entirely yours.
Open the builder, draw a scenario, and play locally — or choose encrypted links, live collaboration, files, and protected static sources when the scenario needs to travel.