Cynario Cynario alpha
Zero-knowledge by design

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.

Local authoring and play do not require an account or a central plaintext scenario store.
Snapshot links keep their decryption key in the fragment; compact live invitations carry an independent room capability. Neither is sent to the web host.
Static hosts, catalogs, signaling, relays, and containers may handle ciphertext and ordinary connection metadata — not scenario plaintext by design.
What your security team is signing off on
Plain-language, before you ever click run.
Data stays on the device
Scenarios live in your browser's local storage and as files you export. Nothing is uploaded to use the app.
Encrypted, key-in-link sharing
AES-256-GCM. The decryption key rides in the URL fragment, which browsers never send to a server.
Auditable, not opaque
Every scenario is one readable, diffable document. Review it like code before it touches a single learner.

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.

Your device
Builder + player run here. Scenario data is encrypted locally before it ever moves.
Encrypted
Matchmaking relay
Helps browsers connect or move ciphertext. It cannot decrypt scenario content, but it may observe ordinary connection metadata.
Encrypted
Your peer
Holds the key from the link. Their browser decrypts and plays; any hosted or relayed copy remains ciphertext.

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.

Inside the sandbox
Read & write the scenario's own variables
Branch to the next node by On-enter / On-exit rules
Render text from state (read-only templating)
No file access · no network calls · no eval · no shell

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' + hashes Only 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 · preload HTTPS is pinned for two years and submitted to the preload list.
  • COOP · nosniff · no-referrer Origin isolation, no MIME sniffing, no referrer leakage.
  • Permissions-Policy Camera, 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.

author markup
──▶ values HTML-escaped on the way in
──▶ markup rendered to HTML
──▶ DOMPurify allow-list ──▶ safe HTML ──▶ screen

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. 1
    Shape

    Is this actually a scenario — the right structure — or just a file pretending to be one?

  2. 2
    Size

    The whole file and every content field are byte-capped, so it can’t bloat your storage or stall the player.

  3. 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. 4
    Reserved keys

    Does it try to write protected, derived state it shouldn’t?

  5. 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.