Web Viewer — apps/viewer

Zero-backend replay SPA for .rewind session files. Part of the ExcelRewind architecture documentation suite.

Contents
  1. Overview
  2. Architecture diagram
  3. Data-flow diagrams
  4. Process / sequence diagrams
  5. Interconnections
  6. Key functions & files
  7. Invariants & gotchas

1. Overview

apps/viewer is the recipient-facing surface of ExcelRewind: a React 18 + TypeScript single-page app (Vite, port 5173 in dev) that opens a .rewind companion file — dropped, picked, fetched as a demo/sample, or eventually delivered via a share link — and replays the recorded cell-change timeline entirely client-side. It never uploads the file and requires no account: per CLAUDE.md's constraints, the web viewer must require zero install for recipients and never expose the raw .rewind for download by default. This module is the load-bearing enforcement point for that promise.

The app has exactly two real UI states, both driven from apps/viewer/src/App.tsx: Landing (no file loaded — drop zone, live demo, sample, or an UnlockDialog if the dropped file is encrypted) and Viewer (a loaded RewindDocument being replayed — grid, sidebar, player bar, and a set of optional overlays for charts, cross-workbook Provenance Spotlight, webcam PiP, and exam/kiosk audit). All parsing, decompression, integrity verification, and (for encrypted files) AES-256-GCM decryption happen in-browser via @excelrewind/format — the shared container/crypto contract also used by the Excel add-in and Google Sheets add-on.

A second, unrelated entry point ships from the same Vite build: google-signin.html / src/google-signin.ts. This is not part of file replay — it is the OAuth handoff page the Excel add-in's task pane opens in the user's system browser (Google/Apple block OAuth flows inside Office.js webviews), because the viewer's origin is a plain HTTPS site outside any Office AppDomains restriction. It exchanges the PKCE code for a Supabase session and stashes the tokens via the stash_handoff RPC for the add-in to poll and claim. It is documented here because it physically lives in this app and deploys to the same origin, but it has no code-path overlap with file replay.

Build/deploy: npm run build (workspace @excelrewind/viewer) runs vite build with two Rollup entry points (index.html, google-signin.html); npm run deploy at the repo root runs wrangler pages deploy apps/viewer/dist --project-name excelrewind-viewer --branch master to Cloudflare Pages, serving at viewer.excelrewind.com. public/_headers sets a strict CSP (self + the Supabase project origin only), X-Robots-Tag: noindex (share links must never be indexed), and Cache-Control: no-store on index.html so a redeploy is picked up immediately. There is no server-side component for this app at all — no API routes, no SSR. Beyond same-origin GETs for its own static assets (the app shell, /demo.rewind, /sample-session.rewind), the only network calls it makes are outbound, anonymous telemetry inserts and (on the sign-in page only) Supabase auth calls — see §5 for the full external-systems table.

2. Architecture diagram

Internal components of apps/viewer/src, grouped by responsibility. Solid arrows are direct imports/calls; dashed arrows mark an interaction that isn't a plain top-level import (a DOM click handler, a lazy dynamic import()). The tan-filled OAUTHPAGE subgraph — google-signin.ts — has no edges into the rest of the diagram at all: it is bundled into the same build (a second Vite/Rollup entry point, google-signin.html) but shares no code path with file replay.

flowchart TB
  subgraph BOOT["Bootstrap"]
    MAIN["main.tsx"]
    APP["App.tsx
(top-level state machine)"]
  end

  subgraph FMT["@excelrewind/format (shared contract)"]
    CONTAINER["container.ts
readRewind / peekRewind / writeRewind"]
    CRYPTO["crypto.ts
AES-256-GCM, PBKDF2, RSA-OAEP"]
    REPLAY["replay.ts
replayTo / currentEventIndex"]
    HISTORY["history.ts
cellHistory"]
    SPOTFMT["spotlight.ts
spotlightCues"]
    A1["a1.ts
parseA1 / toA1 / colToIndex"]
  end

  subgraph LANDING_GRP["Pre-load UI"]
    LANDING["Landing.tsx
dropzone, demo, sample"]
    UNLOCK["UnlockDialog.tsx
passphrase / recovery code"]
    UNLOCKCACHE["unlock-cache.ts
in-memory last-good secret"]
  end

  subgraph VIEWERSHELL["Viewer shell (Viewer.tsx)"]
    CLOCK["playback clock
(rAF / audio-driven)"]
    SCHEDULE["schedule.ts
activeTrackAt / stripSegments"]
    SILENCE["silence.ts
findSilentIntervals (WebAudio)"]
  end

  subgraph GRIDVIEW["Grid & history"]
    GRID["Grid.tsx
spreadsheet render + chart anchors"]
    HISTPANEL["HistoryPanel.tsx
'explain this cell'"]
  end

  subgraph SIDE["Sidebar & transport"]
    SIDEBAR["Sidebar.tsx
Notes / Changes tabs"]
    PLAYERBAR["PlayerBar.tsx
scrubber, speed, skip-silence"]
    SEGSTRIP["SegmentStrip.tsx
multi-author track chips"]
    HINTS["hints.ts
'where in Excel' tips"]
  end

  subgraph MEDIA["Media overlays"]
    CHARTSTRIP["ChartStrip.tsx
floating chart images"]
    VIDEOPIP["VideoPiP.tsx
draggable webcam overlay"]
    SPOTOVERLAY["SpotlightOverlay.tsx
lazy xlsx (SheetJS) render"]
    REFSBAR["RefsBar.tsx
attached-workbook strip"]
    SPOTVIEW["spotlight.ts
activeCueAt / cueKey"]
  end

  subgraph SUPPORT["Cross-cutting"]
    THEME["theme.ts / ThemeButton.tsx"]
    TELEMETRY["telemetry.ts
anonymous PostgREST insert"]
    REF["ref.ts
captureRef / storedRef (?ref=)"]
    EXTREF["externalRef.ts
hasExternalRef / fmtSize"]
    UTIL["util.ts
fmtTime / fmtValue / downloadBytes"]
  end

  subgraph OAUTHPAGE["Separate entry point"]
    GSIGNIN["google-signin.ts
PKCE exchange + stash_handoff"]
  end

  MAIN --> APP
  APP --> LANDING
  APP --> UNLOCK
  APP -->|"VIEWERSHELL"| CLOCK
  LANDING -->|"onFile / onBytes"| APP
  UNLOCK --> CONTAINER
  UNLOCK --> UNLOCKCACHE
  APP --> CONTAINER
  CONTAINER --> CRYPTO
  APP --> TELEMETRY
  APP --> REF

  CLOCK --> SCHEDULE
  CLOCK --> SILENCE
  CLOCK --> REPLAY
  CLOCK --> SPOTFMT
  CLOCK --> SPOTVIEW
  CLOCK --> GRID
  CLOCK --> SIDEBAR
  CLOCK --> PLAYERBAR
  CLOCK --> REFSBAR
  CLOCK --> SPOTOVERLAY
  CLOCK --> VIDEOPIP
  CLOCK --> CHARTSTRIP
  CLOCK --> TELEMETRY

  GRID --> A1
  GRID --> UTIL
  HISTPANEL --> HISTORY
  HISTPANEL --> UTIL
  GRID -.->|"click cell"| HISTPANEL

  SIDEBAR --> HINTS
  SIDEBAR --> EXTREF
  SIDEBAR --> UTIL
  PLAYERBAR --> UTIL
  SEGSTRIP --> SCHEDULE
  PLAYERBAR --> SEGSTRIP

  SPOTOVERLAY --> A1
  SPOTOVERLAY -.->|"dynamic import('xlsx')"| XLSX["xlsx (SheetJS)
lazy-loaded"]
  REFSBAR --> EXTREF
  REFSBAR --> UTIL

  APP --> THEME
  VIEWERSHELL --> THEME

  %% GSIGNIN has no import edge to MAIN/APP on purpose: google-signin.html loads
  %% google-signin.ts directly via its own script tag (see vite.config.ts's
  %% second rollup input) — it shares nothing with the replay component tree.

  style OAUTHPAGE fill:#f3f0e6,stroke:#b9822f

3. Data-flow diagrams

3.1 File ingestion — bytes to renderable state

Every open path (drag-drop, file picker, sample fetch, demo fetch, or a future share-link fetch) converges on the same two functions in App.tsx: loadFileloadBytes. Nothing is sent over the network at this stage; the only network calls in this diagram are the two static-asset fetch calls for the bundled demo/sample files and the outbound-only telemetry beacon.

flowchart LR
  DROP["User drops / picks a .rewind file"] -->|"File object"| LOADFILE["App.loadFile()"]
  FETCHDEMO["GET /demo.rewind or
/sample-session.rewind
(static, same-origin)"] -->|"ArrayBuffer"| LOADBYTES
  LOADFILE -->|"Uint8Array"| LOADBYTES["App.loadBytes()"]
  LOADBYTES --> PEEK["peekRewind(bytes)
(container.ts — inflates ONLY meta.bin)"]
  PEEK -->|"encrypted:false"| READ["readRewind(bytes,
{limited:true})"]
  PEEK -->|"encrypted:true"| CACHECHECK{"cached secret
in unlock-cache.ts?"}
  CACHECHECK -->|"yes, retry silently"| READENC1["readRewind(bytes,
{limited:true, unlock})"]
  READENC1 -->|"success"| DOC
  READENC1 -->|"throws (different key)"| DIALOG["UnlockDialog opens"]
  CACHECHECK -->|"no"| DIALOG
  DIALOG -->|"passphrase / recovery code"| READENC2["readRewind(bytes,
{limited:true, unlock})"]
  READENC2 -->|"success"| SETCACHE["setCachedSecret()
(memory only, this tab)"]
  SETCACHE --> DOC
  READ --> DOC["RewindDocument
{meta, timeline, annotations,
audio/video tracks, snapshot,
baseline, gallery, media, refs}"]
  DOC --> ONDOC["App.onDoc()"]
  ONDOC --> STATE["React state: doc, fileName, autoPlay"]
  ONDOC --> TRACK1["track('file_open', {size_kb,
event_count, note_count,
duration_s, has_audio,
has_snapshot, platform})"]
  ONDOC --> TRACK2["track('viewer_opened',
{recipient})"]
  STATE --> VIEWERCMP["Viewer.tsx renders"]
  TRACK1 -.->|"POST (anon key)"| SUPA["Supabase
viewer_events table"]
  TRACK2 -.-> SUPA

3.2 In-memory document to on-screen playback

Once a RewindDocument is loaded, everything downstream is pure client-side computation over the in-memory object — no further parsing of the ZIP occurs. The playhead t is the single source of truth that every panel derives from.

flowchart TB
  DOC["RewindDocument (in memory)"] --> TIMELINE["doc.timeline
CellChangeEvent[]"]
  DOC --> ANNOT["doc.annotations"]
  DOC --> MEDIA["doc.media / doc.gallery
(PNG chart snapshots)"]
  DOC --> REFS["doc.refs
(attached source .xlsx bytes)"]
  DOC --> AUDIO["doc.audioTracks
(webm bytes per narrator)"]
  DOC --> VIDEO["doc.videoTracks
(webcam webm)"]
  DOC --> SNAPSHOT["doc.snapshot
(final .xlsx, optional)"]

  CLOCKT["playhead t (seconds)"] --> REPLAYTO["replayTo(timeline, t, baseline)
-> GridState"]
  REPLAYTO --> GRIDRENDER["Grid.tsx table cells"]
  CLOCKT --> CURIDX["currentEventIndex(changes, t)"]
  CURIDX --> SIDEBARHL["Sidebar.tsx active row highlight"]
  CLOCKT --> ACTIVECUE["activeCueAt(spotlightCues(doc), t)"]
  ACTIVECUE --> SPOTOVERLAY2["SpotlightOverlay renders
refs[cue.file] via xlsx"]
  CLOCKT --> SCHEDULER["activeTrackAt(schedTracks, t)"]
  SCHEDULER -->|"play/pause/seek"| AUDIOELS["<audio> elements
(one per track, Blob URL)"]
  CLOCKT --> VIDEOSYNC["VideoPiP seeks <video>
to t - startT"]

  TIMELINE --> HASHISTORY["Grid: hasHistory set
(click affordance)"]
  MEDIA --> CHARTURLS["URL.createObjectURL
per chart id"]
  CHARTURLS --> GRIDRENDER
  CHARTURLS --> CHARTSTRIP2["ChartStrip (unanchored charts)"]
  SNAPSHOT -->|"click 'Workbook'"| DOWNLOAD["downloadBytes()
browser download, local only"]
  REFS -->|"click download in RefsBar"| DOWNLOAD

  GRIDRENDER -->|"click a touched cell"| CELLHIST["cellHistory(doc, sheet, cell)
(history.ts)"]
  CELLHIST --> HISTPANEL2["HistoryPanel.tsx"]

4. Process / sequence diagrams

4.1 Open a plaintext file and play the demo

sequenceDiagram
  participant User
  participant Landing
  participant App as "App.tsx"
  participant Format as "container.ts"
  participant Viewer as "Viewer.tsx"
  participant Supabase

  User->>Landing: click "Try the live demo"
  Landing->>Landing: fetch("/demo.rewind")
  alt asset missing (fixture not built)
    Landing-->>User: sampleError "Demo not found — run npm run fixture"
  else 200 OK
    Landing->>App: onBytes(bytes, "demo.rewind", "demo")
    App->>Format: peekRewind(bytes)
    Format-->>App: {encrypted:false, ...}
    App->>Format: readRewind(bytes, {limited:true})
    Format->>Format: unzipSync + bounded inflate per entry
    Format->>Format: SHA-256 integrity sweep (meta.integrity)
    alt hash mismatch
      Format-->>App: throw "Integrity check failed for X"
      App-->>Landing: setError(ERR_TAMPERED)
    else all hashes match
      Format-->>App: RewindDocument
      App->>App: onDoc(doc, name, "demo", bytes.length)
      App->>Supabase: POST viewer_events (file_open, metadata only)
      App->>Supabase: POST viewer_events (viewer_opened, recipient:false)
      App->>Viewer: render, autoPlay=true (source==="demo")
      Viewer->>Viewer: rAF loop advances t, PlayerBar/Grid/Sidebar re-render each frame
    end
  end

4.2 Open an encrypted file — cache retry, unlock dialog, tamper detection

sequenceDiagram
  participant User
  participant App as "App.tsx"
  participant Cache as "unlock-cache.ts"
  participant Dialog as "UnlockDialog.tsx"
  participant Format as "container.ts / crypto.ts"

  User->>App: drop encrypted-2.rewind
  App->>Format: peekRewind(bytes)
  Format-->>App: {encrypted:true}
  App->>Cache: getCachedSecret()
  alt a secret worked earlier this tab
    Cache-->>App: {kind, value}
    App->>Format: readRewind(bytes,{limited:true,unlock})
    alt same org key
      Format-->>App: RewindDocument
      App->>App: onDoc(...) — no prompt shown
    else different key (throws)
      Format-->>App: reject
      App->>Dialog: open (fresh file, no error shown)
    end
  else no cached secret
    App->>Dialog: open
  end

  User->>Dialog: type passphrase, submit
  Dialog->>Format: readRewind(bytes,{limited:true,
unlock:{passphrase}}) Format->>Format: unwrapCek(envelope, unlock)
PBKDF2-SHA256 x310,000 iters (~0.5-1s) alt wrong passphrase (no recipient wrap opens) Format-->>Dialog: throw "Wrong passphrase or key" Dialog-->>User: "That didn't unlock this file." (fails++) Dialog-->>User: after 2 fails, show "rotated key / open via add-in" hint else correct key, GCM auth fails on an entry (tamper) Format-->>Dialog: throw "Integrity check failed for X" Dialog-->>User: "The secret is correct, but the file failed
its integrity check — corrupted or tampered." else correct key, all entries authenticate Format-->>Dialog: RewindDocument Dialog->>Cache: setCachedSecret({kind, value}) Dialog->>App: onUnlocked(doc) App->>App: onDoc(doc, name, source, bytes.length) end
The viewer only ever supplies {passphrase} or {recoveryCode} to UnlockOptions — it has no Supabase session, so the org KEK and pre-unwrapped rsaPriv unlock paths in unwrapCek() (used by the add-in/team console) are dead code from the viewer's perspective. Recovery-code unlock walks the escrows chain embedded in the file itself (secret → master AES key → unwrap RSA priv → RSA-unwrap the CEK), so it works fully offline with no DB round-trip.

4.3 Playback clock — legacy single narrator vs. multi-track wall-clock

sequenceDiagram
  participant User
  participant Viewer as "Viewer.tsx"
  participant Audio as "<audio> elements"
  participant Schedule as "schedule.ts"

  User->>Viewer: click Play
  alt exactly one audio track (legacy path, zero tracks falls to the wall-clock branch below)
    Viewer->>Audio: audioRefs[0].play()
    loop every rAF tick
      Audio-->>Viewer: currentTime
      Viewer->>Viewer: skip-silence check (skipRef intervals)
      alt inside a silent interval
        Viewer->>Audio: currentTime = interval.end
      end
      Viewer->>Viewer: setT(audio.currentTime)
    end
  else no audio, or multiple narration tracks
    loop every rAF tick (wall-clock master)
      Viewer->>Viewer: setT(prev + dt * speed)
      Viewer->>Schedule: activeTrackAt(schedTracks, t)
      alt an active track index found
        Viewer->>Audio: seek(if drifted >0.25s), set playbackRate, play()
        Viewer->>Audio: pause all other tracks
      else t falls in a gap between tracks
        Viewer->>Audio: pause all tracks
      end
    end
  end
  Viewer->>Viewer: t >= duration -> setPlaying(false)
  User->>Viewer: close tab / navigate away
  Viewer->>Viewer: pagehide -> sendPlaybackEnd()
  Viewer->>Viewer: track('playback_end',
{watched_s, completed}) via keepalive fetch

4.4 Google OAuth handoff (system-browser page hosted by this app)

sequenceDiagram
  participant Pane as "Excel add-in task pane"
  participant Browser as "System browser"
  participant Page as "google-signin.ts"
  participant Google
  participant Supabase

  Pane->>Browser: window.open("https://viewer.excelrewind.com/
google-signin.html#nonce=X&provider=google") Browser->>Page: load, no ?code= yet Page->>Page: stash nonce+provider in sessionStorage Page->>Supabase: signInWithOAuth({provider, redirectTo: self}) Supabase-->>Google: redirect Google-->>Browser: user consents Google-->>Page: redirect back with ?code=... (PKCE) Page->>Page: read nonce from sessionStorage (hash was dropped by the round-trip) alt nonce missing (link expired / opened stale) Page-->>Browser: "This link has expired — start sign-in again" else nonce present Page->>Supabase: exchangeCodeForSession(code) alt exchange fails Page-->>Browser: "Google sign-in failed: ..." else session obtained Page->>Supabase: rpc('stash_handoff', {p_nonce, p_access, p_refresh}) Supabase-->>Page: ok / error Page-->>Browser: "Signed in — return to Excel. Close this tab." Note over Pane: pane's pollOAuthHandoff() (add-in side)
claims the tokens by nonce and adopts the session end end

4.5 Provenance Spotlight — cross-workbook overlay while replaying

sequenceDiagram
  participant Viewer as "Viewer.tsx"
  participant SpotFmt as "spotlightCues() (format)"
  participant SpotView as "activeCueAt() (viewer)"
  participant Overlay as "SpotlightOverlay.tsx"
  participant XLSX as "xlsx (SheetJS, dynamic import)"

  Viewer->>SpotFmt: spotlightCues(doc, {attachedFiles})
  SpotFmt-->>Viewer: SpotlightCue[] (recorded spans, or
derived from formula external-refs) loop every t tick Viewer->>SpotView: activeCueAt(cues, t) alt a cue's [atT, atT+holdS) window contains t, spotlight is on,
not user-dismissed, and not at end of playback SpotView-->>Viewer: cue Viewer->>Overlay: mount with cue + doc.refs[cue.file] bytes Overlay->>XLSX: import('xlsx') (lazy — only paid the first time a cue fires) XLSX-->>Overlay: parsed workbook Overlay->>Overlay: bound to MAX_ROWS=200 x MAX_COLS=50,
compute highlight rect from cue.range Overlay-->>Viewer: renders source sheet, scrolls hit cell into view Viewer->>Viewer: track('feature_use', {feature:'spotlight'}) (once per doc) else no active cue, or window not attached (doc.refs missing) SpotView-->>Viewer: null end end Note over Viewer: the master clock never pauses for the overlay —
it is purely a visual layer over ongoing playback

5. Interconnections

flowchart LR
  VIEWER["Web Viewer
(viewer.excelrewind.com)"]
  SUPA[("Supabase
tohvepsjfeecjycspocg.supabase.co")]
  GOOGLE["Google OAuth"]
  APPLE["Apple OAuth"]
  ADDIN["addin.excelrewind.com
install page"]
  ADDINPANE["Excel add-in task pane
(addin.excelrewind.com — different origin)"]
  STATIC["Same-origin static assets
/demo.rewind, /sample-session.rewind"]

  VIEWER -->|"POST /rest/v1/viewer_events
(PostgREST, anon key, metadata-only JSON)"| SUPA
  VIEWER -->|"only on /google-signin.html:
signInWithOAuth / exchangeCodeForSession"| SUPA
  VIEWER -->|"rpc('stash_handoff')"| SUPA
  VIEWER -->|"OAuth redirect"| GOOGLE
  VIEWER -->|"OAuth redirect"| APPLE
  VIEWER -->|"GET (static)"| STATIC
  VIEWER -->|"'Get the add-in' CTA link,
utm_source/campaign/ref query params"| ADDIN
  ADDINPANE -.->|"window.open(google-signin.html#nonce=...)"| VIEWER
  ADDINPANE -.->|"polls stashed tokens via Supabase, not directly"| SUPA
External systems this module talks to
TargetProtocol / callPurposeTrigger / file
Supabase PostgREST — viewer_events table HTTPS POST to {VITE_SUPABASE_URL}/rest/v1/viewer_events, anon key in apikey/Authorization, Prefer: return=minimal, keepalive:true Anonymous usage telemetry: viewer_open, file_open, playback_end, feature_use, viewer_opened, cta_install_click. Metadata only — never file names, workbook/sheet names, or note text. src/telemetry.ts track(), called from App.tsx and Viewer.tsx
Supabase Auth — signInWithOAuth / exchangeCodeForSession @supabase/supabase-js, PKCE flow, detectSessionInUrl:false Completes the Google/Apple OAuth round-trip for the Excel add-in's sign-in (runs in the system browser, not a webview) src/google-signin.ts — only reachable at /google-signin.html, not part of the replay app
Supabase RPC — stash_handoff supabase.rpc('stash_handoff', {p_nonce, p_access, p_refresh}) Hands the freshly obtained access/refresh tokens back to the Excel add-in pane, keyed by a one-time nonce generated by the pane src/google-signin.ts
Google / Apple OAuth endpoints Browser redirect (302), standard OAuth2/PKCE Identity provider consent screens Indirect, via Supabase's signInWithOAuth
Same-origin static files: /demo.rewind, /sample-session.rewind fetch(), same origin, no auth Bundled demo/sample content for the "Try the live demo" and "load the DCF sample" CTAs (built by npm run fixture) src/components/Landing.tsx
addin.excelrewind.com/install.html Plain <a href> link (new tab), hardcoded same-product HTTPS literal + utm_source=share&utm_medium=viral and optionally utm_campaign/ref Post-replay "Record your own" viral CTA and the "Get the add-in" link src/components/Viewer.tsx installUrl()
xlsx (SheetJS) In-browser library, no network — parses bytes already embedded in the opened .rewind file Renders the referenced source workbook inside the Provenance Spotlight overlay src/components/SpotlightOverlay.tsx, dynamic import('xlsx')
@excelrewind/format (workspace package, not network) Direct TS import The entire container/crypto/replay contract — this is the module boundary that defines what a .rewind file is throughout — see §6

Notably absent: there is no call to any ExcelRewind API server for file replay (none exists — see CLAUDE.md "Still no API server"), no call ever carries spreadsheet content, and the raw .rewind bytes never leave the browser tab that opened them.

6. Key functions & files

Load-bearing files, in read order of relevance
FileFunction / exportWhat it doesWhy it matters
src/App.tsxloadBytes()The single funnel every open path goes through: peekRewind → cached-secret silent retry → UnlockDialog fallback → readRewind.The one place to change if the unlock/open flow needs new behavior; every entry point (drop/picker/demo/sample) is required to call this, not read bytes directly.
src/App.tsxonDoc()Shared success path for plaintext AND unlocked-encrypted opens: sets state, fires file_open/viewer_opened telemetry.Guarantees telemetry and UI state can never diverge between the two unlock paths — a common source of "works when unencrypted, broken when encrypted" bugs elsewhere.
src/unlock-cache.tsgetCachedSecret / setCachedSecretModule-level (not React state) in-memory cache of the last secret that worked.Explicitly documented as never touching localStorage/sessionStorage/disk — a secret must not outlive the tab. Auditors should verify no other code path persists it.
packages/format/src/container.tsreadRewind()Unzips (bounded, decompression-bomb-capped when limited:true), decrypts if meta.enc is present, verifies every entry against meta.integrity SHA-256 hashes, reconstructs the RewindDocument.The viewer always calls this with {limited:true} — untrusted-input bounds (256MB/entry, 512MB total, 1024 entries, 100:1 inflate ratio) are ON for every file a recipient opens, unlike the add-in's own-file path.
packages/format/src/container.tspeekRewind()Inflates only meta.bin (or its plaintext envelope) to report encrypted/size/duration cheaply before committing to a full parse.Lets App.tsx decide unlock-dialog-vs-not without paying for a full container read, even on multi-GB audio/video files.
packages/format/src/container.tsunwrapCek()Tries, in order: direct passphrase/org recipient wraps, pre-unwrapped RSA privs, then the file-embedded escrow chain (secret → master key → RSA priv → CEK).The viewer only ever exercises the passphrase and escrow-recoveryCode branches (no Supabase session available) — the org/rsaPriv branches are add-in/team-console-only, dead from this module's perspective.
packages/format/src/crypto.tsderiveKek(), encryptBytes/decryptBytesPBKDF2-SHA256 @ 310,000 iterations → AES-256-GCM with per-entry random IV and entry-name AAD.The ~0.5-1s deriveKek cost is why UnlockDialog has a busy state; AAD binding to the entry name means ciphertext can't be silently swapped between container entries even by someone with write access to the ZIP.
src/components/UnlockDialog.tsxmessage branching in submit()Distinguishes "wrong secret" from "right secret, tampered file" by checking for the string Integrity check failed in the thrown error.A subtle but important UX/security distinction — never tell the user their passphrase might be wrong when the real problem is a corrupted/tampered file (which would otherwise train users to keep guessing).
packages/format/src/replay.tsreplayTo(timeline, t, baseline)Pure reducer: replays every event up to time t into a GridState (per-sheet cell map).Re-run on every playhead tick from a fixed baseline+timeline — no incremental/mutable state, so seeking backward is just as cheap as forward and there's no drift risk.
packages/format/src/history.tscellHistory(doc, sheet, cell)Full change history for one cell, including baseline seed and fill-expansion entries.Backs the "explain this cell" popover — the closest thing the viewer has to a per-cell audit trail.
packages/format/src/spotlight.tsspotlightCues(doc, opts)Recorded-span cues (Tier 2, doc.meta.spotlights) win when present; else derives cues from formula external-refs (Tier 1).Explains why some sessions show Spotlight windows that don't exactly match a formula's edit moment — recorded spans have real start/end, derived ones are inferred and capped by MAX_HOLD_S.
src/spotlight.tsactiveCueAt(cues, t)Picks which cue's window is "live" at time t: on overlap, the cue with the latest atT <= t wins.Pure/unit-testable without any DOM/xlsx dependency — see src/__tests__/spotlight.test.ts.
src/schedule.tsactiveTrackAt(tracks, t)Which of N narration audio tracks is audible at wall-clock time t, and its internal offset.The core of multi-author narration playback — ensures only one narrator's audio ever plays even if learned track durations momentarily overlap.
src/silence.tsfindSilentIntervals()Decodes narration audio at 16kHz via AudioContext, computes an adaptive RMS-threshold silence map, protects windows around recorded events/notes.Runs entirely client-side, lazily, only when the user enables "Skip silence" — not run on multi-track sessions (flagged as "not yet available").
src/components/Grid.tsxrender-window clamping (MAX_RENDER_ROWS/MAX_RENDER_COLS)Bounds the DOM table size independent of the logical sheet bounds a malicious/corrupt session could claim.A hostile .rewind claiming a write to row 1,048,576 renders a bounded window (2000×256), not a browser-crashing DOM.
src/components/SpotlightOverlay.tsxdynamic import('xlsx'), MAX_ROWS/MAX_COLSSheetJS is loaded only when a Spotlight cue first fires; parse is bounded to 200 rows × 50 cols.Keeps SheetJS (a heavy dependency) out of every viewer session that never touches a cross-workbook reference.
src/telemetry.tsshouldSend(), sanitizeProps()Hard gate on DNT/GPC/missing env; strips any telemetry prop down to ≤10 primitive-typed keys.The client-side half of the "metadata only, never content" guarantee — server-side CHECK constraints in the Supabase migration are the actual trust boundary, this is defense-in-depth.
src/ref.tscaptureRef(), storedRef()Validates (^[A-Za-z0-9_-]{1,32}$) and persists a viral ?ref= code to localStorage.Threads attribution through to the "Get the add-in" CTA and viewer_opened telemetry without ever accepting free-text or PII in the URL.
src/google-signin.tsmain()PKCE code exchange + stash_handoff RPC, gated on a nonce round-tripped via sessionStorage (URL hash is dropped by the OAuth redirect).Only page in this app that touches auth/session tokens at all; whitelists provider to google|apple before ever using it in a request.
apps/viewer/public/_headersCloudflare Pages headers configCSP restricted to 'self' + the Supabase project origin; X-Robots-Tag: noindex; no-cache on index.html.Deploy-time enforcement of the "share links never enter search" and "no third-party script/style origins" constraints — a change here must keep the Supabase origin in connect-src or telemetry/OAuth silently breaks (see memory note on CSP silent breakage in the add-in).
apps/viewer/vite.config.tsbuild.rollupOptions.inputDeclares two independent HTML entry points (main, google-signin) in one build.Explains why google-signin.html deploys to the same origin as the replay app despite having nothing to do with it — it must live off any Office AppDomains list.
src/components/Viewer.tsxkioskAudit / pasteCount (exam-audit header block)When doc.meta.kiosk is set, renders a banner (org, exam name, student identity, time limit) and a paste-event count derived by filtering the timeline for event.flag === 'paste'.Not in the original architecture sketch (only namechecked in §1) — the Kiosk/Exam workstream (WS-4). Purely a read-only replay of what the recorder chose to stamp into meta.identity/meta.exam and per-event flag; the viewer performs no verification of its own — see the invariant below.
src/components/PlayerBar.tsxpaste markers in the scrubberEvery CellChangeEvent with flag === 'paste' gets a distinct red marker on the transport track, clickable like an event/note marker.The only other UI surface for kiosk data — makes paste events (a proctoring signal) scannable without opening the Sidebar's Changes list.

7. Invariants & gotchas

Product-defining constraints enforced in this module

Platform / implementation quirks worth knowing before touching this code