Excel Add-in — Office.js Task Pane

apps/addin-excel · React + TypeScript, Vite dev server on :3000 · deployed as static assets behind Cloudflare Pages

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/addin-excel is the Office.js task pane that runs inside Excel Desktop (Windows and Mac, WebView2 / WKWebView respectively). It is the primary capture surface for ExcelRewind: it hooks worksheets.onChanged and related Office.js events, normalizes every cell edit into the CellChangeEvent shape defined by packages/format, buffers the running session in IndexedDB for crash recovery, and — on Stop/Save — assembles a .rewind ZIP container (writeRewind) that is either saved to disk via the File System Access API or downloaded as a legacy anchor. It also plays sessions back inside Excel on disposable temporary sheets (replay-excel.ts), so an Excel-native user never has to leave the app to review a recording.

The pane is a single React app (src/App.tsx, ~4,250 lines — effectively the controller for the whole surface) composed with a handful of feature components (SignIn, ReplayPanel, EncryptionSettings, Kiosk, BranchMerge, BugReport, MicTest/CameraTest/VideoPiP) and a set of pure, unit-tested modules that do the actual work: recorder.ts (capture), replay-excel.ts (in-Excel playback), buffer-crypto.ts/key-pin.ts/keyring-client.ts (encryption), auth.ts (Supabase session + licence gate), companion.ts (optional native-helper client), and persistence.ts (IndexedDB crash-recovery buffer + settings).

Build/deploy: npm run addin serves the Vite dev server over HTTPS on localhost:3000 (certs via npm run certs); npm run manifest generates manifest.prod.xml/manifest.dev.xml and the install page from .env origins; npm run deploy builds and pushes the static dist/ to Cloudflare Pages, then runs a deploy-time smoke test. There is no application server for this app — every backend interaction is a direct Supabase REST/RPC/Storage call over HTTPS from the browser using the public anon key, or a call to a Supabase Edge Function. The only other network peer is the optional Windows/Mac native "Companion" helper, reached over 127.0.0.1 loopback HTTP(S).

2. Architecture diagram

Internal component map — files, classes, and the state/stores they own, grouped by concern.

flowchart TB
  subgraph UI["React UI (src/App.tsx + feature components)"]
    App["App.tsx (controller — phase state machine:\nidle / recording / stopped)"]
    SignIn["SignIn.tsx\n(email OTP + Google/Apple handoff UI)"]
    ReplayPanel["ReplayPanel.tsx\n(in-Excel replay scrubber)"]
    EncSettings["EncryptionSettings.tsx\n(personal key setup/rotation UI)"]
    Kiosk["Kiosk.tsx\n(exam lockdown UI)"]
    BranchMerge["BranchMerge.tsx\n(diff/merge UI)"]
    BugReport["BugReport.tsx\n(attach + upload to bug-reports bucket)"]
    MediaTest["MicTest.tsx / CameraTest.tsx / VideoPiP.tsx"]
  end

  subgraph CAPTURE["Recording engine"]
    Recorder["recorder.ts\nExcelRecorder class\n(onChanged/onCalculated/onFormatChanged\nhandlers, event pump, drag-fill coalesce)"]
    FillCoalesce["fill-coalesce.ts\n(pure: merge same-formula runs into one 'fill' event)"]
    ExcelUtil["excel-util.ts\nrunRetryingEditMode / isEditModeError"]
    Snapshot["snapshot.ts\ncaptureWorkbookSnapshot()\n(.xlsx bytes via getFileAsync)"]
  end

  subgraph REPLAY["In-Excel replay engine"]
    ReplayExcel["replay-excel.ts\nExcelReplayer class\n(temp sheets, chart/shape/format gating,\ncross-sheet ref rewrite)"]
    GapDiff["gap-diff.ts\ncountGap/diffGap\n(edits made outside a session)"]
    ResumeContent["resume-content.ts\nclassifySheets/finalSheetCells\n(resume-from-import content restore)"]
  end

  subgraph CRYPTO["Encryption / key layer"]
    KeyringClient["keyring-client.ts\ngetEncryptSpec / dbUnlock / cacheFileSecret\n(in-memory-only key cache)"]
    BufferCrypto["buffer-crypto.ts\nnewBufferKey / sealBuffer / openBuffer\n(crash-buffer AES-GCM)"]
    KeyPin["key-pin.ts\ncheckKeyPins / acknowledgePins\n(TOFU pubkey pinning)"]
  end

  subgraph PERSIST["Local persistence"]
    IDB[("IndexedDB (idb-keyval)\nrewind.active-session\nrewind.audio-chunk-N\nrewind.buffer-key\nrewind.autosave-handle")]
    LocalStorage[("localStorage\nrewind.settings, rewind.device-id,\nrewind.keypin.*, rewind.companion-token")]
    Persistence["persistence.ts\nSessionDraft, AddinSettings,\nautosave decision helpers"]
  end

  subgraph AUTHMOD["Auth / access"]
    Auth["auth.ts\nSupabase client, OTP + OAuth handoff,\ncheckAccess(), effectiveFeatures()"]
    Access["access.ts\n(pure) evaluateAccess/evaluateDevice/offlineAccess"]
    Telemetry["telemetry.ts\ntrack() → usage_events (metadata only)"]
  end

  subgraph COMPANIONMOD["Companion client"]
    Companion["companion.ts\nscanPorts/pair/pollEvents/openRef\n(loopback HTTP(S), Bearer token)"]
  end

  subgraph OFFICEJS["Office.js host (Excel Desktop)"]
    ExcelHost["Excel JS API\nworksheets.onChanged / onCalculated /\nonFormatChanged / charts.onAdded"]
  end

  subgraph FORMAT["@excelrewind/format (shared contract)"]
    FormatPkg["writeRewind / readRewind / replayTo\nCellChangeEvent, RewindDocument types\nAES-GCM+RSA envelope crypto"]
  end

  App --> Recorder
  App --> ReplayExcel
  App --> Persistence
  App --> Auth
  App --> Companion
  App --> KeyringClient
  App --> BufferCrypto
  App --> KeyPin
  App --> Snapshot
  App --> SignIn
  App --> ReplayPanel
  App --> EncSettings
  App --> Kiosk
  App --> BranchMerge
  App --> BugReport
  App --> MediaTest
  App --> Telemetry

  Recorder --> FillCoalesce
  Recorder --> ExcelUtil
  Recorder --> ExcelHost
  ReplayExcel --> ExcelUtil
  ReplayExcel --> ExcelHost
  ReplayExcel --> GapDiff
  ReplayExcel --> ResumeContent
  ReplayExcel --> FormatPkg

  Persistence --> IDB
  Persistence --> BufferCrypto
  Persistence --> LocalStorage
  App -. loadSettings/saveSettings .-> LocalStorage

  Auth --> Access
  KeyringClient --> Auth
  KeyPin -. sha256Hex .-> FormatPkg
  BufferCrypto --> FormatPkg

  App --> FormatPkg
  Recorder --> FormatPkg

  SignIn --> Auth
  EncSettings --> KeyringClient
  Kiosk --> Auth

3. Data-flow diagrams

3.1 Recording → crash buffer → saved .rewind

flowchart LR
  Edit["User edits a cell in Excel"] -->|"Office.js worksheets.onChanged\n(WorksheetChangedEventArgs)"| Recorder
  Recorder -->|"CellChangeEvent[]\n{t, sheet, cell, from, to,\nformula_from, formula_to, type}"| DraftState["React state: SessionDraft\n(events[], annotations[], authors[])"]
  DraftState -->|"every 1s tick (setInterval)"| SaveDraft["persistence.saveDraft()"]
  SaveDraft -->|"encryption ON: sealBuffer(packEntry(draft), bufferCek)\nAES-256-GCM ciphertext" | IDBActive[("IndexedDB\nrewind.active-session")]
  SaveDraft -->|"encryption OFF: plaintext structured-clone"| IDBActive
  MicChunk["MicRecorder.onChunk\n(~5s WebM chunk)"] -->|"saveAudioChunk(index, blob)"| IDBAudio[("IndexedDB\nrewind.audio-chunk-N")]
  Autosave["autosave tick (settings.autosaveSec)"] -->|"draftToBytes() → saveBytes()"| DiskFile[("Disk file\nvia FileSystemFileHandle\n(File System Access API)")]

  Stop["Stop button"] --> CaptureSnap["captureWorkbookSnapshot()\nOffice.context.document.getFileAsync"]
  Stop --> CaptureGallery["captureChartGallery()\nchart.getImage() PNG per chart"]
  CaptureSnap -->|".xlsx bytes"| FinalDraft
  CaptureGallery -->|"media{id:png}, gallery[]"| FinalDraft["Final SessionDraft"]
  DraftState --> FinalDraft

  SaveClick["Save button"] --> DraftToBytes["draftToBytes()\n(assemble RewindDocument:\ntimeline, annotations, baseline,\naudioTracks, videoTracks, refs, gallery)"]
  FinalDraft --> DraftToBytes
  EncSpec["getEncryptSpec()\n(org/user RSA pubkeys)"] -.->|"EncryptSpec (or undefined = plaintext)"| DraftToBytes
  DraftToBytes -->|"writeRewind(doc, encSpec)\ndeflate + MessagePack + ZIP\n(AES-256-GCM+RSA-OAEP if encrypted)"| RewindBytes["Uint8Array (.rewind bytes)"]
  RewindBytes -->|"saveBytes(): overwrite handle /\nshowSaveFilePicker / anchor download"| DiskFile2[("Disk .rewind file")]
  RewindBytes -.->|"on success"| ClearDraft["clearDraft()\n— wipes IndexedDB buffer + buffer-key"]

3.2 Import → in-Excel replay data flow

flowchart LR
  PickFile["User picks a .rewind file\n(showOpenFilePicker or <input>)"] --> PeekStep["peekRewind(bytes)\n(cheap: inflates meta.json only)"]
  PeekStep -->|"peek.encrypted"| UnlockCard["Unlock card:\npassphrase or recovery code"]
  UnlockCard -->|"unlockBufferKey / dbUnlock"| UnlockOpts["UnlockOptions\n(rsaPriv[], escrow secret)"]
  PeekStep -->|"not encrypted, or unlocked"| ReadRewind["readRewind(bytes, {unlock})\n→ RewindDocument\n(timeline, baseline, snapshot,\naudioTracks, media, gallery, refs)"]
  UnlockOpts --> ReadRewind
  ReadRewind --> ImportedState["React state: imported\n{doc, name, encrypted}"]
  ImportedState -->|"Replay button"| BeginReplay["beginReplay(doc)\n— detect recorded-name/workbook-name\ncollisions"]
  BeginReplay --> Replayer["new ExcelReplayer(doc, opts)\n.open()"]
  Replayer -->|"insertWorksheetsFromBase64(snapshot)\n(ExcelApi 1.13)"| TempSheets["Temporary sheets\n(tab color #7C3AED,\nnamed rewind_<original>)"]
  Replayer -->|"computeWrites(prev, next, managed)\ndiff between grid states"| CellWrites["CellWrite[]\n(chunked, 300/batch)"]
  CellWrites -->|"range.values / range.formulas ="| TempSheets
  ImportedState -->|"Resume & continue"| RestoreContent["restoreRecordedContent()\nwrites final cells into the\nLIVE workbook (never the original\nuntouched by replay)"]

4. Process / sequence diagrams

4.1 Record → Stop → Save (encrypted, happy path + fail-loud path)

sequenceDiagram
  participant User
  participant App as "App.tsx"
  participant Rec as "ExcelRecorder"
  participant Excel as "Excel host (Office.js)"
  participant IDB as "IndexedDB"
  participant Keyring as "keyring-client.ts"
  participant Supa as "Supabase"
  participant FS as "Disk (File System Access API)"

  User->>App: click Start
  App->>Supa: checkAccess() (licence + device gate)
  Supa-->>App: allowed
  App->>Keyring: invalidateKeyCache()
  App->>Rec: spawnRecorder(session, mic)
  Rec->>Excel: worksheets.onChanged.add / onCalculated.add
  Rec->>Excel: registerChartEvents(), pollShapes() interval
  loop every cell edit
    Excel-->>Rec: WorksheetChangedEventArgs
    Rec->>Rec: mapChangeType, readCell, coalesce (fill-coalesce.ts)
    Rec-->>App: onEvents(CellChangeEvent[])
  end
  loop every 1s while recording
    App->>IDB: saveDraft(draft) [sealed if bufferCek set]
  end
  User->>App: click Stop
  App->>Rec: recorder.stop() (removes handlers, final calc sweep)
  App->>App: captureWorkbookSnapshot(), captureChartGallery()
  User->>App: click Save
  App->>Keyring: getEncryptSpec(encOff)
  Keyring->>Supa: SELECT org_keys / user_keys (RLS-scoped, cached 5min)
  Supa-->>Keyring: org_pub / user_pub rows
  alt encryption required but key unavailable
    Keyring-->>App: undefined + encryptionRequired()=true
    App-->>User: THROW "recording NOT saved" (fail-loud, no plaintext write)
  else key resolved (or feature off)
    Keyring-->>App: EncryptSpec | undefined
    App->>App: checkKeyPins(spec) — TOFU compare vs localStorage
    opt pinned key changed
      App-->>User: warn "encryption key changed" — require acknowledge
    end
    App->>App: draftToBytes() → writeRewind(doc, spec)
    App->>FS: saveBytes(bytes) — overwrite handle / showSaveFilePicker / anchor
    FS-->>App: handle | SAVE_CANCELLED
    App->>IDB: clearDraft()
    App->>Supa: track('save_file', {size_kb}) [usage_events, metadata only]
  end

4.2 Sign-in: Email OTP and Google system-browser handoff

sequenceDiagram
  participant User
  participant Pane as "Task pane (SignIn.tsx)"
  participant Supa as "Supabase Auth (GoTrue)"
  participant SysBrowser as "System browser\n(viewer origin /google-signin.html)"
  participant Google as "Google OAuth"

  alt Email OTP
    User->>Pane: enter email, "Send code"
    Pane->>Supa: auth.signInWithOtp({email, data:{ref}})
    Supa-->>User: emails 6-digit code
    User->>Pane: enter code
    Pane->>Supa: auth.verifyOtp({email, token, type:'email'})
    Supa-->>Pane: session
    Pane->>Supa: rpc('register_device', {device_id, label})
  else Google (webview-blocked OAuth)
    Pane->>Pane: nonce = makeNonce()
    Pane->>SysBrowser: window.open(oauthSignInUrl(nonce, 'google'), '_blank')
    Note over Pane,SysBrowser: viewer origin is NOT in manifest AppDomains —\nforces the OS's real browser, since Google\nrejects OAuth inside embedded webviews
    SysBrowser->>Google: OAuth consent flow
    Google-->>SysBrowser: tokens
    SysBrowser->>Supa: rpc('stash_handoff', {nonce, tokens}) (via viewer page)
    loop poll every 2.5s, up to 5 min
      Pane->>Supa: rpc('claim_handoff', {p_nonce: nonce})
      Supa-->>Pane: pending | {access_token, refresh_token}
    end
    Pane->>Supa: auth.setSession({access_token, refresh_token})
    Pane->>Supa: rpc('register_device', ...)
  end
  Pane->>App: onAuthChange fires
  App->>Supa: checkAccess() (app_config + profiles + touch_device)
  Supa-->>App: {allowed, tier, telemetry}
  App->>App: initTelemetry(level, uid), setAuth({status:'ok', ...})

4.3 Crash-recovery buffer unlock

sequenceDiagram
  participant App as "App.tsx (mount)"
  participant Persist as "persistence.ts"
  participant IDB as "IndexedDB"
  participant User
  participant BufCrypto as "buffer-crypto.ts"
  participant Keyring as "keyring-client.ts"
  participant Supa as "Supabase"

  App->>Persist: loadBufferEnvelope()
  Persist->>IDB: get('rewind.buffer-key')
  IDB-->>Persist: EncEnvelope | undefined
  alt envelope present (buffer is ciphertext)
    Persist-->>App: EncEnvelope
    App-->>User: show unlock card (passphrase / recovery code)
    User->>App: submit secret
    App->>BufCrypto: unlockBufferKey(envelope, {passphrase|recoveryCode})
    alt file-escrow unwrap succeeds
      BufCrypto-->>App: CryptoKey (bufferCek)
    else escrow fails — try DB path
      App->>Keyring: dbUnlock(secret, kind)
      Keyring->>Supa: SELECT user_keys / org_keys / org_keys_retired
      Supa-->>Keyring: wrapped rows
      Keyring-->>App: rsaPriv[] → unlockBufferKey(envelope, {rsaPriv})
    end
    App->>Persist: setBufferCek(cek)
    App->>Persist: loadDraft() — openBuffer(ciphertext, cek)
    Persist-->>App: SessionDraft
    App-->>User: "Recovered session — Save or Discard"
  else no envelope (plaintext buffer, encryption was off)
    Persist->>IDB: loadDraft() directly (structured clone)
  end

4.4 Companion pairing + cross-workbook recording

sequenceDiagram
  participant App
  participant Companion as "companion.ts (client)"
  participant Helper as "Companion helper\n(apps/companion-win, loopback :38743-38747)"

  App->>Companion: scanPorts() (mount, then every 3s)
  loop 5 ports x {http,https}
    Companion->>Helper: GET /status
    Helper-->>Companion: {product:'excelrewind-companion', version, excelDetected}
  end
  Companion-->>App: CompanionConn | null

  User->>App: click "Connect companion"
  App->>Companion: pair(base)
  Companion->>Helper: POST /pair (native OS prompt, 30s timeout)
  Helper-->>Companion: {token}
  Companion->>Companion: localStorage['rewind.companion-token'] = token

  User->>App: Start recording (Record-all-workbooks ON)
  App->>Companion: startRecording(base, excludePrimary, captureChanges=true)
  Companion->>Helper: POST /record/start {exclude, captureChanges}
  loop every poll tick
    App->>Companion: pollEvents(base, since)
    Companion->>Helper: GET /record/events?since=N (Bearer token)
    Helper-->>Companion: {events[], dropped, spotlights[]}
    Companion-->>App: mergeEvent() → CellChangeEvent (sheet = 'Wb.xlsx::Sheet1')
  end
  App->>Companion: stopRecording(base)
  Companion->>Helper: POST /record/stop
  Helper-->>Companion: final spotlights[]

5. Interconnections

flowchart LR
  Addin["Excel Add-in\n(apps/addin-excel)"]
  ExcelDesktop["Excel Desktop\n(Office.js host)"]
  SupaAuth["Supabase Auth (GoTrue)\nOTP + OAuth session"]
  SupaDB[("Supabase Postgres\napp_config, profiles, org_keys,\nuser_keys, org_keys_retired,\nusage_events, bug_reports")]
  SupaRPC["Supabase RPCs\nregister_device, touch_device,\neffective_features, my_referral,\npublic_tiers, exam_*, claim_handoff"]
  SupaStorage["Supabase Storage\nbug-reports/<uid>/"]
  ExamFn["exam-storage Edge Function"]
  Viewer["Viewer origin\n(google-signin.html — system browser only)"]
  Companion["Companion helper\n(Windows/Mac, 127.0.0.1:38743-38747)"]
  Stripe["Stripe Payment Links\n(system browser, window.open)"]
  CFPages["Cloudflare Pages\n(static host for the pane itself)"]

  Addin -->|"HTTPS supabase-js: signInWithOtp,\nverifyOtp, setSession, onAuthStateChange"| SupaAuth
  Addin -->|"HTTPS REST (PostgREST) select/insert,\nRLS-scoped by JWT"| SupaDB
  Addin -->|"HTTPS RPC calls"| SupaRPC
  SupaRPC --- SupaDB
  Addin -->|"HTTPS storage.upload(bug-reports/uid/*)"| SupaStorage
  Addin -->|"HTTPS POST /functions/v1/exam-storage\n(Bearer access token)"| ExamFn
  Addin -->|"window.open (system browser handoff\n— OAuth blocked in webviews)"| Viewer
  Viewer -->|"rpc('stash_handoff')"| SupaRPC
  Addin -->|"HTTP(S) loopback, Bearer token\nGET/POST /status /pair /workspace\n/file /open /open-many /record/*\n/spotlight"| Companion
  Addin -->|"window.open (system browser)"| Stripe
  Addin <-->|"Office.js in-process API calls\n(worksheets.onChanged, ranges,\ncharts, getFileAsync)"| ExcelDesktop
  CFPages -.->|"serves dist/ (React SPA)\nover the SourceLocation in manifest*.xml"| Addin
PeerProtocolPurposeClient file(s)
Supabase Auth (GoTrue)HTTPS via @supabase/supabase-jsEmail OTP sign-in, session refresh, OAuth session adoption after system-browser handoffsrc/auth.ts
Supabase Postgres (PostgREST, RLS)HTTPS RESTRead app_config/profiles (licence gate), org_keys/user_keys/org_keys_retired (encryption), insert usage_events/bug_reportssrc/auth.ts, src/keyring-client.ts, src/telemetry.ts, src/BugReport.tsx
Supabase RPCs (security-definer)HTTPS RPCregister_device/touch_device (2-device limit), effective_features (entitlement cascade), my_referral/public_promo_config/public_tiers, set_telemetry_level, claim_handoff (OAuth), exam_list_for_student/exam_get/exam_submitsrc/auth.ts
Supabase StorageHTTPSUpload attached bug-report files under bug-reports/<uid>/ (staff-readable; blocked for encryption-sensitive sessions)src/BugReport.tsx
exam-storage Edge FunctionHTTPS POST, Bearer access tokenSigned upload URL for kiosk/exam .rewind submissionsrc/auth.ts (examUploadUrl)
Viewer origin (google-signin.html)HTTPS, opened in the system browser (deliberately outside manifest AppDomains)Completes Google/Apple OAuth (blocked inside Office webviews), stashes tokens via stash_handoffsrc/auth.ts
Companion helper (native, optional)HTTP (Windows) / HTTPS self-signed (Mac WKWebView mixed-content rule), loopback ports 38743–38747, Bearer tokenRead/record other open workbooks, open attached cross-workbook refs, Provenance Spotlight focussrc/companion.ts
Stripe Payment Linkswindow.open to a hosted Stripe URL (system browser)Tier upgrade checkout; the add-in never writes profiles.tier — only the Stripe webhook (server-side) doessrc/App.tsx (UpgradeCard)
Excel host (Office.js)In-process JS API (no network)Cell events, chart/shape enumeration, workbook file bytes, range writes for replaysrc/recorder.ts, src/replay-excel.ts, src/excel-util.ts, src/snapshot.ts
Cloudflare PagesHTTPS static hostingServes the built pane (dist/) at the manifest SourceLocation; dev/prod have distinct origins and distinct manifest Idspublic/manifest.dev.xml, public/manifest.prod.xml

Manifest story (dev / prod / dev-cloud)

FileApp IdSourceLocationAppDomainsUsed for
manifest.xml (repo root, source template)3f2b6c1e-…-5d8e1f0a6b47https://localhost:3000/https://localhost:3000Local dev sideload against the Vite dev server (npm run sideload)
public/manifest.dev.xml (generated → dev-cloud)8c86f414-…-56a8f2e8800d ("ExcelRewind (DEV)")https://excelrewind-addin-dev.pages.dev/dev Pages origin + dev Supabase project (ragrgchidtdpgkyjkznu.supabase.co)Testing against a deployed dev environment without touching prod data
public/manifest.prod.xml (generated → prod)a4c8e6f0-…-6e8f0b2d4c93https://addin.excelrewind.com/prod Pages origin + prod Supabase project (tohvepsjfeecjycspocg.supabase.co)Production distribution / trusted-catalog sideload

All three deliberately point SourceLocation at the domain root, never /index.html — Cloudflare Pages 308-redirects that path and Office refuses to load a SourceLocation that redirects (blank pane). The viewer origin (google-signin.html's host) is intentionally absent from every manifest's AppDomains — that absence is what forces Office to hand the OAuth link to the system browser instead of navigating the pane's own webview.

6. Key functions & files

FileFunction / exportWhat it doesWhy it matters
src/recorder.tsExcelRecorder (class)Registers worksheets.onChanged/onCalculated/onFormatChanged/chart events, normalizes them to CellChangeEvent[], coalesces drag-fills, dedupes Mac double-dispatch, stamps author + tThe entire capture pipeline; the field-tuned event pump (EVENT_PUMP_MS, Web-Worker heartbeat) is why edits land with real timestamps instead of one giant burst at the next pause
src/recorder.tscaptureBaseline()Reads every sheet's pre-existing (non-empty) cells at record-Start, clipped to MAX_BASELINE_AREA/MAX_BASELINE_CELLS_PER_SHEETWithout this, replay would start from a blank grid for any cell not touched during the session
src/recorder.tsreadCell()Distinguishes real errors (#DIV/0!) from linked-data types (Stocks/Geography) that the classic values API misreports as an error stringNon-obvious Office.js quirk; getting this wrong corrupts rich-cell capture silently
src/recorder.tspumpEvents() / EVENT_PUMP_MS / pump Web WorkerA parked delayForCellEdit no-op sync every 1s, driven from a Worker (immune to WebKit's backgrounded-tab timer clamp) to force Excel to flush queued onChanged eventsField-measured workaround (see CLAUDE.md/finding.md) — removing it silently degrades event timestamps to ~5s+ granularity
src/fill-coalesce.tscoalesce() / flush()Pure state machine that merges a run of same-relative-formula single-cell edits into one fill eventKeeps a drag-fill of 500 cells from becoming 500 timeline entries
src/excel-util.tsrunRetryingEditMode() / isEditModeError()Wraps Excel.run to retry (up to 6x, 700ms) on InvalidOperationInCellEditModeEvery Office.js call can transiently fail while the user is mid-keystroke; shared by recorder AND replay so both survive it identically
src/replay-excel.tsExcelReplayer (class)Drives a session onto disposable temp sheets inside the CURRENT workbook — insert-from-base64, chart/shape/format time-gating, cross-sheet formula rewrite, manual-calc speedupEncodes the hard platform fact that Excel.createWorkbook() is unreachable from the pane, so "replay in a scratch workbook" is impossible — temp sheets are the only safe design
src/replay-excel.tsmanagedCells() / computeWrites()Pure: the set of cells replay is ever allowed to touch, and the minimal diff of writes between two grid statesThis is the safety boundary — replay writes ONLY cells it created and ONLY resolves them by worksheet ID captured at insert time, never a pre-existing sheet
src/replay-excel.tsrewriteSheetRefs() / rewriteInsertedFormulas()Rewrites cross-sheet formula references baked into the inserted snapshot so they bind to the temp sheets, not the user's same-named sheetsFixes a real field bug: formulas referencing "Sheet1" would otherwise silently read the LIVE workbook's Sheet1
src/replay-excel.tsplanChartGates() / planShapeGates() / planFormatGates()Pure planners that decide which charts/shapes/cell-formats must stay hidden until their recorded insert momentExcel snapshots carry FINAL state; without gating a chart added mid-session would be visible from t=0
src/buffer-crypto.tsnewBufferKey() / sealBuffer() / openBuffer()Mints a random per-session buffer-CEK, wraps it to the same recipients as the file's encrypt spec, AES-256-GCM seals every IndexedDB writeCloses the RECORDING_AT_REST gap — the crash-recovery buffer used to be plaintext even when the final file would be encrypted
src/key-pin.tscheckKeyPins() / acknowledgePins()Trust-on-first-use: pins SHA-256(org/user pubkey) in localStorage; a later mismatch is surfaced as a warning requiring explicit acknowledgement before re-pinningDetects a write-capable compromise (leaked service key / rogue admin) silently redirecting future recordings to an attacker's key; explicitly NOT a full PKI (documented residual risk in the file header)
src/keyring-client.tsgetEncryptSpec() / encryptionRequired()Resolves the effective encrypt-on-save policy from org_keys/user_keys rows (org key primary, personal self-wrap unless sealed); encryptionRequired answers "should this have encrypted" independent of whether it succeededBacks the fail-loud contract in App.tsx's encSpecForSave() — a key-load failure throws instead of silently writing plaintext
src/keyring-client.tsdbUnlock()Derives RSA private keys from user_keys/org_keys/org_keys_retired rows using a typed passphrase/recovery codeLets files whose escrow predates a passphrase rotation still open, including walking every retired OMK generation
src/keyring-client.tsclearUnlockCache()Purges every in-memory crypto cache (unwrapped RSA privs, file secrets, org/user key row cache)Called on sign-out — without it, a second user on a shared machine could read the first user's encrypted files or wrap into their key (CRYPTO_REVIEW HIGH-4)
src/auth.tscheckAccess()Full launch-time licence gate: global app_config.enabled + profiles.status + device-limit (touch_device), with a 7-day offline-grace cache fallbackThe single choke point that decides signed-in / blocked / offline-allowed; also the point at which an evicted device is signed out
src/auth.tspollOAuthHandoff()Polls claim_handoff RPC for up to 5 minutes after opening the system browser, then adopts the returned sessionThe entire mechanism behind Google/Apple sign-in inside a webview that cannot itself complete OAuth
src/auth.tseffectiveFeatures()Calls effective_features() RPC (SuperAdmin → Org → Individual cascade)Backs every fb('feature') gate in App.tsx — offline/unknown always resolves to "allowed" so a failed fetch never blocks recording
src/companion.tsscanPorts() / isCompanion()Probes 5 ports × {http,https} for the native helper's /status marker; rejects any response without the exact product markerEvery companion call is designed to degrade to "offline" rather than throw (R1) — the pane behaves identically with or without the helper installed
src/companion.tsmergeEvent()Maps a companion-reported cell event onto a synthetic sheet name '<Workbook.xlsx>::<Sheet>'Lets cross-workbook edits flow through every existing timeline/replay surface as an ordinary sheet without special-casing
src/persistence.tssaveDraft() / loadDraft()Structured-clone (plaintext) or seal/open via buffer-crypto.ts (encrypted) round-trip of the live SessionDraft through IndexedDBThe crash-recovery contract; throws BUFFER_LOCKED when ciphertext is found but no key is set, forcing the caller to show the unlock UI rather than crash
src/persistence.tsshouldAutosave() / autosaveChanged()Pure decision helpers: autosave fires only when enabled, has a write target, nothing else is in flight, and content actually changedUnit-testable without timers/Office — the actual interval effect in App.tsx just calls these
src/download.tssaveBytes() / pickSaveStrategy()Branches between overwrite-existing-handle / showSaveFilePicker / legacy anchor download, based on feature detectionThe "save in place" behaviour (kills duplicate "file (1).rewind") depends entirely on this and is degrade-safe across Office WebView2 builds that lack the picker APIs
src/snapshot.tscaptureWorkbookSnapshot()Streams the live workbook's compressed bytes via Office.context.document.getFileAsync/getSliceAsyncThe ONLY way this add-in ever reads the original file's bytes, and only into an in-memory buffer embedded in the companion .rewind — never sent anywhere except the local save
src/App.tsxencSpecForSave()Wraps getEncryptSpec; throws if encryption is required but the key failed to loadThe single fail-loud choke point every save path (manual save, autosave, exam submit) routes through
src/App.tsxdraftToBytes()Assembles the final RewindDocument (meta, timeline, annotations, audio/video tracks, snapshot, baseline, gallery, refs) and calls writeRewindThe one place that maps the pane's internal SessionDraft shape onto the on-disk .rewind contract
src/App.tsxrestoreRecordedContent() / replaceViaSnapshot()Resume-from-import: writes every recorded sheet's final state into the LIVE workbook, inserting missing sheets and replacing empty/user-confirmed non-empty ones via snapshot rename-asideThe "chain of development" contract — a resumed session must never leave the user with a partial/blank grid
src/App.tsxUpgradeCardRenders Stripe Payment Link buttons from public_tiers(), opened via window.openC-1 invariant made concrete: the client never writes profiles.tier — only the Stripe webhook does; this component only ever sends the user to checkout
packages/format/src/spotlight.tsspotlightCues()Derives Tier-1 spotlight cues from formula external-refs baked into a session's timeline (no companion needed) — the fallback when Tier-2 spans weren't captured or spotlightForceDerived is onProvenance Spotlight works even for viewers/replayers who never had the companion installed at record time
src/companion.tsspotlight() / clearSpotlight()Tells the companion helper to bring a specific source workbook window to the front (and back) during in-Excel replayThe actual OS-level window focus for Tier-2 Provenance Spotlight — impossible from the sandboxed pane alone, hence the companion round-trip
src/ReplayPanel.tsxspotlight orchestration (spotlightActive/spotlightBusy/abortSpotlight())During replay, drives spotlightCues() (or live companion spans) to pause grid pushes and show "source workbook in focus" while the recording's clock and audio keep runningA spotlight round-trip is async and can be interrupted by pause/seek/close at any time; the busy/active refs plus abortSpotlight() are what keep the clock from drifting or double-firing a cue

Provenance Spotlight (Tier 1 derived / Tier 2 recorded)

Spotlight is a replay-time feature, gated by the spotlight feature flag and the provenanceSpotlight setting: when a session references other workbooks, replay briefly brings the source file into focus so the viewer sees where a number came from. Two capture tiers coexist and are switchable in Settings (spotlightForceDerived): Tier 1 needs nothing at record time — spotlightCues() (packages/format) derives cues after the fact from external-reference formulas already in the timeline. Tier 2 requires the Companion helper paired at record time — mergeCompanionSpans() in App.tsx merges live SpotlightSpan[] (start/end replay-time, source filename) polled from the companion into draft.spotlights, which round-trips through the .rewind file as an additive field. At replay, ReplayPanel.tsx uses whichever tier is available/selected to pause the grid push and call companion.spotlight()/ clearSpotlight() to focus and return the source window.

7. Invariants & gotchas

Product-defining constraints enforced in this module

Platform quirks the code specifically works around

Cross-reference: per repo CLAUDE.md, AES-256/user-controlled-key encryption was historically a documented gap (.rewind` encryption gap — OPS-6) but per the memory index it shipped to production in v3.23.0–.2 (this module's buffer-crypto.ts/key-pin.ts/keyring-client.ts are that feature). A SOC 2 Type II readiness audit (SOC2_AUDIT.md) found 48 findings against the whole system as of 2026-07-17; remediation had not started as of the last recorded handoff.