TRD
Technical Requirements
Stack, the Web Crypto modules behind the workbench and playgrounds, the simulator engines, the comments backend and how the site is deployed on its own domain.
This document records how nviaOTP is built and the constraints each part must respect. The overriding constraint is that cryptography, simulations and learner code run in the visitor’s browser. The only server-side state is the shared discussion thread; everything else is either static content shipped in the bundle or learner progress stored on the device.
Application stack
- UI: React 19 with TypeScript in strict mode, file-based routing with server rendering for every content page, and a utility-first CSS layer driven by semantic design tokens.
- Editor: CodeMirror 6 with JavaScript highlighting for playgrounds and the policy engine; line wrapping is enabled where JSON rules are long.
- Motion: a declarative animation library for the celebration, diff transitions and simulator timelines; all motion respects reduced-motion preferences.
- Icons: a single open-source outline icon set; no emoji in UI chrome.
- Fonts: Source Serif 4 for prose, Inter for interface text, JetBrains Mono for code, loaded with preconnect and swap.
Content model
Chapters are typed data, not markdown files. Each chapter is an ordered list of blocks — paragraph, heading, list, callout, code, playground, diff, figure, quiz, attacker/defender panel, risk table and lab module. A single renderer maps blocks to components. Interactive blocks are kept in a separate inserts module keyed by chapter slug and spliced in before the quiz, so prose files stay readable and editors cannot accidentally break a playground while fixing a sentence. Reading time is computed at module load from the words a reader actually reads (prose, lists, callouts, captions, quiz text); code is excluded.
Cryptographic modules
All cryptography uses the browser’s Web Crypto API. There is no third-party crypto library. The OTP core exposes base32 encode/decode, big-endian 8-byte counter serialisation, HMAC with SHA-1, SHA-256 or SHA-512, and a HOTP function that returns every intermediate value: the counter bytes, the full MAC, the offset, the four selected bytes, the 31-bit integer and the final code. The workbench and the home card both use it.
const msg = counterBytes(C); // 8 bytes, big-endian
const mac = await hmac("SHA-1", key, msg); // 20 bytes
const offset = mac[19] & 0x0f;
const bin = ((mac[offset] & 0x7f) << 24) | (mac[offset+1] << 16)
| (mac[offset+2] << 8) | mac[offset+3];
const code = String(bin % 10 ** digits).padStart(digits, "0");Correctness is enforced by the RFC test vectors. On load the workbench recomputes the ten HOTP values of RFC 4226 Appendix D, the count-zero HMAC, and the six SHA-1 TOTP values of RFC 6238 Appendix B, and displays each as a pass or fail chip. The WebAuthn mock generates a non-extractable P-256 key pair in the tab, builds clientDataJSON and authenticatorData exactly as the specification lays them out, signs authenticatorData ‖ SHA-256(clientDataJSON) and verifies with the public key.
Playground sandbox
Learner code runs in a dedicated Web Worker created from an in-memory blob. Before user code executes, the worker removes network and storage globals (fetch, XMLHttpRequest, WebSocket, EventSource, importScripts, IndexedDB, caches). User code is wrapped in an async function receiving three injected objects: a simulated messaging SDK kept for compatibility, a lab helper object (hmac, sha256, base32, counter, random bytes, hex, table and pass), and a console that forwards to the output panel. The page arms a six-second watchdog per run; if the worker has not reported completion it is terminated and an inline error explains the likely infinite loop. Edits are debounced by 450 ms and each run replaces the previous worker.
Simulator engines
The attack simulator is a pure, deterministic function from a scenario and a set of enabled defenses to a timeline, an attempt count, simulated elapsed time, an outcome and a residual severity. It never waits on real timers; time is a number advanced by the model. Randomness, used only for the MFA-fatigue approval model, comes from a seeded generator so the same toggles always produce the same result. Each defense either changes the path or records why it had no effect, which keeps the simulator aligned with the lessons — for example, rate limiting has no effect on a SIM swap because the attacker needs exactly one code. The policy engine parses JSON on every keystroke, validates decisions and rule shape, and evaluates first-match-wins rules against fixed sample sessions.
Comments backend
Shared comments live in a managed Postgres database. The browser never reads or writes the comments table directly: row-level security is enabled with no table policies, and three database functions form the entire public interface. list_comments(chapter) returns only visible comments and safe columns. post_comment(chapter, parent, name, body, session) validates lengths, link count, nesting depth and duplicates, enforces rate limits, and inserts. report_comment(id, session) records one report per browser per comment and hides the comment at three reports. Functions run with definer rights and a fixed search path; execute rights are granted only on these three functions.
The browser generates a random 72-character session token once and keeps it in local storage. The server never stores it: it stores a SHA-256 hash, used for rate limiting (one comment per 30 seconds, eight per hour) and for de-duplicating reports. The token is not an identity and is documented as spam-light protection, not authentication.
Deployment
The site is built into a server-rendered edge bundle and served from the nviaotp.me domain with HTTPS, with the www host redirecting to the apex. Content pages render on the server so that titles, descriptions and link previews are correct without JavaScript. Browser-only code — workers, Web Crypto, CodeMirror, local storage reads — is gated behind client-only boundaries and lazy imports so server rendering never touches browser globals. Server code reads configuration only inside request handlers.
Quality gates
- Type check with no errors; no
anyin content or lab modules. - Browser run of every chapter with a playground: zero FAIL lines and zero page errors.
- Content script: 6 series, 31 chapters, every chapter ≥ 900 words with a figure and a quiz.
- Accessibility: keyboard reachability of every control, labelled figures, live regions for playground output and simulator results, colour contrast at AA in both themes.