apps/addin-excel · React + TypeScript, Vite dev server on :3000 · deployed as static assets behind Cloudflare Pages
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).
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
.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"]
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)"]
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
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', ...})
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
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[]
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
| Peer | Protocol | Purpose | Client file(s) |
|---|---|---|---|
| Supabase Auth (GoTrue) | HTTPS via @supabase/supabase-js | Email OTP sign-in, session refresh, OAuth session adoption after system-browser handoff | src/auth.ts |
| Supabase Postgres (PostgREST, RLS) | HTTPS REST | Read app_config/profiles (licence gate), org_keys/user_keys/org_keys_retired (encryption), insert usage_events/bug_reports | src/auth.ts, src/keyring-client.ts, src/telemetry.ts, src/BugReport.tsx |
| Supabase RPCs (security-definer) | HTTPS RPC | register_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_submit | src/auth.ts |
| Supabase Storage | HTTPS | Upload attached bug-report files under bug-reports/<uid>/ (staff-readable; blocked for encryption-sensitive sessions) | src/BugReport.tsx |
exam-storage Edge Function | HTTPS POST, Bearer access token | Signed upload URL for kiosk/exam .rewind submission | src/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_handoff | src/auth.ts |
| Companion helper (native, optional) | HTTP (Windows) / HTTPS self-signed (Mac WKWebView mixed-content rule), loopback ports 38743–38747, Bearer token | Read/record other open workbooks, open attached cross-workbook refs, Provenance Spotlight focus | src/companion.ts |
| Stripe Payment Links | window.open to a hosted Stripe URL (system browser) | Tier upgrade checkout; the add-in never writes profiles.tier — only the Stripe webhook (server-side) does | src/App.tsx (UpgradeCard) |
| Excel host (Office.js) | In-process JS API (no network) | Cell events, chart/shape enumeration, workbook file bytes, range writes for replay | src/recorder.ts, src/replay-excel.ts, src/excel-util.ts, src/snapshot.ts |
| Cloudflare Pages | HTTPS static hosting | Serves the built pane (dist/) at the manifest SourceLocation; dev/prod have distinct origins and distinct manifest Ids | public/manifest.dev.xml, public/manifest.prod.xml |
| File | App Id | SourceLocation | AppDomains | Used for |
|---|---|---|---|---|
manifest.xml (repo root, source template) | 3f2b6c1e-…-5d8e1f0a6b47 | https://localhost:3000/ | https://localhost:3000 | Local 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-…-6e8f0b2d4c93 | https://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.
| File | Function / export | What it does | Why it matters |
|---|---|---|---|
| src/recorder.ts | ExcelRecorder (class) | Registers worksheets.onChanged/onCalculated/onFormatChanged/chart events, normalizes them to CellChangeEvent[], coalesces drag-fills, dedupes Mac double-dispatch, stamps author + t | The 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.ts | captureBaseline() | Reads every sheet's pre-existing (non-empty) cells at record-Start, clipped to MAX_BASELINE_AREA/MAX_BASELINE_CELLS_PER_SHEET | Without this, replay would start from a blank grid for any cell not touched during the session |
| src/recorder.ts | readCell() | Distinguishes real errors (#DIV/0!) from linked-data types (Stocks/Geography) that the classic values API misreports as an error string | Non-obvious Office.js quirk; getting this wrong corrupts rich-cell capture silently |
| src/recorder.ts | pumpEvents() / EVENT_PUMP_MS / pump Web Worker | A 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 events | Field-measured workaround (see CLAUDE.md/finding.md) — removing it silently degrades event timestamps to ~5s+ granularity |
| src/fill-coalesce.ts | coalesce() / flush() | Pure state machine that merges a run of same-relative-formula single-cell edits into one fill event | Keeps a drag-fill of 500 cells from becoming 500 timeline entries |
| src/excel-util.ts | runRetryingEditMode() / isEditModeError() | Wraps Excel.run to retry (up to 6x, 700ms) on InvalidOperationInCellEditMode | Every 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.ts | ExcelReplayer (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 speedup | Encodes 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.ts | managedCells() / computeWrites() | Pure: the set of cells replay is ever allowed to touch, and the minimal diff of writes between two grid states | This 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.ts | rewriteSheetRefs() / rewriteInsertedFormulas() | Rewrites cross-sheet formula references baked into the inserted snapshot so they bind to the temp sheets, not the user's same-named sheets | Fixes a real field bug: formulas referencing "Sheet1" would otherwise silently read the LIVE workbook's Sheet1 |
| src/replay-excel.ts | planChartGates() / planShapeGates() / planFormatGates() | Pure planners that decide which charts/shapes/cell-formats must stay hidden until their recorded insert moment | Excel snapshots carry FINAL state; without gating a chart added mid-session would be visible from t=0 |
| src/buffer-crypto.ts | newBufferKey() / 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 write | Closes the RECORDING_AT_REST gap — the crash-recovery buffer used to be plaintext even when the final file would be encrypted |
| src/key-pin.ts | checkKeyPins() / 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-pinning | Detects 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.ts | getEncryptSpec() / 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 succeeded | Backs the fail-loud contract in App.tsx's encSpecForSave() — a key-load failure throws instead of silently writing plaintext |
| src/keyring-client.ts | dbUnlock() | Derives RSA private keys from user_keys/org_keys/org_keys_retired rows using a typed passphrase/recovery code | Lets files whose escrow predates a passphrase rotation still open, including walking every retired OMK generation |
| src/keyring-client.ts | clearUnlockCache() | 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.ts | checkAccess() | Full launch-time licence gate: global app_config.enabled + profiles.status + device-limit (touch_device), with a 7-day offline-grace cache fallback | The single choke point that decides signed-in / blocked / offline-allowed; also the point at which an evicted device is signed out |
| src/auth.ts | pollOAuthHandoff() | Polls claim_handoff RPC for up to 5 minutes after opening the system browser, then adopts the returned session | The entire mechanism behind Google/Apple sign-in inside a webview that cannot itself complete OAuth |
| src/auth.ts | effectiveFeatures() | 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.ts | scanPorts() / isCompanion() | Probes 5 ports × {http,https} for the native helper's /status marker; rejects any response without the exact product marker | Every companion call is designed to degrade to "offline" rather than throw (R1) — the pane behaves identically with or without the helper installed |
| src/companion.ts | mergeEvent() | 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.ts | saveDraft() / loadDraft() | Structured-clone (plaintext) or seal/open via buffer-crypto.ts (encrypted) round-trip of the live SessionDraft through IndexedDB | The 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.ts | shouldAutosave() / autosaveChanged() | Pure decision helpers: autosave fires only when enabled, has a write target, nothing else is in flight, and content actually changed | Unit-testable without timers/Office — the actual interval effect in App.tsx just calls these |
| src/download.ts | saveBytes() / pickSaveStrategy() | Branches between overwrite-existing-handle / showSaveFilePicker / legacy anchor download, based on feature detection | The "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.ts | captureWorkbookSnapshot() | Streams the live workbook's compressed bytes via Office.context.document.getFileAsync/getSliceAsync | The 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.tsx | encSpecForSave() | Wraps getEncryptSpec; throws if encryption is required but the key failed to load | The single fail-loud choke point every save path (manual save, autosave, exam submit) routes through |
| src/App.tsx | draftToBytes() | Assembles the final RewindDocument (meta, timeline, annotations, audio/video tracks, snapshot, baseline, gallery, refs) and calls writeRewind | The one place that maps the pane's internal SessionDraft shape onto the on-disk .rewind contract |
| src/App.tsx | restoreRecordedContent() / 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-aside | The "chain of development" contract — a resumed session must never leave the user with a partial/blank grid |
| src/App.tsx | UpgradeCard | Renders Stripe Payment Link buttons from public_tiers(), opened via window.open | C-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.ts | spotlightCues() | 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 on | Provenance Spotlight works even for viewers/replayers who never had the companion installed at record time |
| src/companion.ts | spotlight() / clearSpotlight() | Tells the companion helper to bring a specific source workbook window to the front (and back) during in-Excel replay | The actual OS-level window focus for Tier-2 Provenance Spotlight — impossible from the sandboxed pane alone, hence the companion round-trip |
| src/ReplayPanel.tsx | spotlight 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 running | A 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 |
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.
getFileAsync for the snapshot). Replay writes exclusively to worksheet IDs it created itself (managedCells/sheetIds in replay-excel.ts) — there is no code path from replay to a pre-existing sheet, with one documented, explicitly consented exception (renameUser collision mode, see below)..rewind save (via writeRewind/saveBytes) and, if the user explicitly attaches a session to a bug report, the bug-reports Storage bucket — gated off entirely (sessionSensitive) when the session is or would be encrypted, so a paid customer's data is never re-serialized as plaintext into a staff-readable bucket.getEncryptSpec()/encSpecForSave() enforce fail-loud: if the account's encryption policy is ON but the key can't be loaded, the save throws rather than writing plaintext. All secrets and unwrapped RSA private keys live only in module-level, in-memory variables (keyring-client.ts — explicitly documented as never touching localStorage/IndexedDB) and are wiped on sign-out (clearUnlockCache).SESSION_TAGS) — the only "why" signal that leaves the client as structured data.telemetry.ts's sanitizeProps/flattenSettings strip anything that isn't a primitive, redact free-text fields (displayName → boolean), and drop opaque device ids (micId) — cell contents, notes, and filenames never reach usage_events.onChanged events to the pane on a context.sync; under continuous typing, dispatch can starve entirely until the user pauses (field-evidenced both Windows and Mac, different failure shapes). recorder.ts's Web-Worker-driven 1s delayForCellEdit pump is the field-tuned fix — see the file header comment before changing EVENT_PUMP_MS.InvalidOperationInCellEditMode is routine (any poll/write can land mid-keystroke); runRetryingEditMode in excel-util.ts is the single shared retry wrapper for both recorder and replay.visible property and live in a separate collection — replay-excel.ts gates charts by physically moving them off-view (setPosition) and gates shapes via Shape.visible, using two entirely different mechanisms.#VALUE!. readCell()'s heuristic (values says error, displayed text differs) is the only reliable tell; replay restores the real entity via copyFrom against a hidden stash sheet, since plain value writes cannot recreate a rich entity.Excel.createWorkbook() opens a workbook the pane is NOT attached to, making a "disposable scratch workbook" unreachable — this is why in-Excel replay runs on temporary sheets inside the current workbook instead, and why the manifest deliberately omits a shared runtime (see the comment in every manifest*.xml).AppDomains so Office is forced to hand the link to the system browser; the pane then polls a Supabase RPC (claim_handoff) rather than receiving a redirect, since the pane cannot receive a URL/hash callback.http://127.0.0.1:PORT first (Windows fast path) then https://127.0.0.1:PORT, since Mac fails the http attempt instantly pre-network.setInterval instead of the main thread's.insertWorksheetsFromBase64 plus everything after it) can fail as a whole with a generic GeneralException; writes are chunked at WRITE_CHUNK/MATERIALIZE_CHUNK (300 cells/batch) throughout.rewind_* temp sheets (rewriteSheetRefs); the opt-in renameUser fallback temporarily renames the USER's colliding sheets instead — the only code path that ever touches a user's own sheet, gated behind an explicit per-replay consent dialog, and protected by a crash ledger (rewind.replay-user-renames in localStorage) so a dead pane can always offer a one-click restore.key-pin.ts's header explicitly documents the residual risk: a compromise on the very first encrypt to a key can't be detected (nothing to compare against yet), and losing localStorage silently re-pins on next use. It never blocks opening (decrypting) a file — only future encrypt-to-key operations..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.