Skip to content

Repository files navigation

Jestr

Jestr Apps

Your browser apps, backed up automatically: encrypted, portable, and readable with the internet off.

Jestr is a Chrome extension that quietly mirrors the SaaS apps you already use into a folder you own. No cloud. No accounts. No OAuth. No API keys. It rides the sessions you are already signed into.

Chrome Extension Manifest V3 TypeScript Vite

Local first Offline Encrypted ECDH PBKDF2 Portable No OAuth Zero telemetry


Supported apps

Google Drive SharePoint Reddit Telegram Claude
App What gets backed up Saved as
Google Drive Every Doc / Sheet / Slide you open, and again on tab close, so in-session edits are captured .docx .xlsx .pptx
SharePoint Team sites you visit: the whole library is indexed, then downloaded in the background Original files + site tree
Reddit Communities you open and everything you subscribe to; posts, comments, icons, full-res images JSON + Markdown + media
Telegram Every private chat, group and channel, with photos and videos. Deleted messages are kept and marked JSON shards + transcript
Claude All claude.ai conversations on a schedule, plus extracted artifacts JSON + Markdown

The whole interface is one side panel

The Jestr side panel, with a callout pointing at the Reddit row: press to open the offline app

Click the toolbar icon and the panel opens beside your page. It shows, top to bottom:

  • Auto redirect, the switch that serves live sites from your local copy (see below).
  • This tab, what Jestr already holds for the app you are currently looking at.
  • Storage, one bar for the shared budget, then a row per app: size on disk, what is collected, a pause switch, a Sync now, and a gear to that app's own settings.

That is it. One screen at a time, one source of truth. Everything after setup is automatic. You do not schedule anything, you do not press anything.


Why you'd want this

Use case I  ·  Offline mode

The internet goes down. You keep working.

You are on a plane, in a dead zone, behind a corporate firewall, or Google is simply having a bad day. Normally that means every doc, every chat, every thread you needed is gone until it comes back.

With Jestr, flip on Auto redirect and your browser keeps working as if nothing happened:

flowchart LR
    NAV["You navigate to<br/>docs.google.com/document/d/..."] --> Q{"Auto redirect armed,<br/>and the app not paused?"}
    Q -->|"no"| LIVE["The live site, as usual"]
    Q -->|"yes"| INT["A short interstitial:<br/>serving from your backup"]
    INT --> LOCAL["Your local copy in Jestr's viewer,<br/>with the network off"]
Loading

Any host Jestr collects is intercepted and served from disk: Drive documents, SharePoint libraries, Reddit feeds with their comments, whole Telegram histories, your Claude conversations. Reading them needs no network at all. Turn Auto redirect off from the panel (or from the interstitial itself) and you are back on the live sites.

Two deliberate details. A paused app is never intercepted, because standing in for a live service with a copy you stopped maintaining is worse than not standing in at all. And the toolbar badge reads a red AR for as long as interception is armed, so you always know which world you are in.

Use case II  ·  Local backup

The account goes away. The data does not.

Accounts get locked. Trials expire. Workspaces get deprovisioned the day you leave a job. A subreddit gets nuked, a Telegram chat gets deleted by the other person, a doc gets revoked from under you. Cloud data is only yours while someone else says it is.

Jestr keeps a complete, offline, greppable copy on your own disk, from the moment you install it rather than from the moment you remember to export:

  • Nothing upstream can delete your copy. A document vanishing from Drive or SharePoint never removes it here. A Telegram message deleted by anyone stays, tagged deletedISO and rendered as "Deleted from Telegram. Jestr kept the copy it made."
  • Version history. The last 5 versions of every document live in .versions/.
  • Integrity rules borrowed from real backup tools: zip-magic validation (a stale Google session returns an HTML login page with HTTP 200, and Jestr will never overwrite a good backup with it), sha256 change-gating, atomic writes.
  • A hard ceiling, never an eviction policy. One shared budget across every app, 5 GB by default. At the limit Jestr stops backing up and tells you: red badge, notification, a line on the Storage screen. It does not delete one backup to make room for another. Ever.

Portable

One folder. Any machine. No lock-in.

Your backup is a plain folder on your disk that you pick once. Not a database, not a proprietary archive, not a container only this extension can open.

your-backup-folder/
├── drive/       <Title> [docId8].docx | .xlsx | .pptx   (+ .versions/, _manifest.json)
├── sharepoint/  <tenant>/sites/<Site>/<Library>/<folders…>/<file>   ← the site's own tree
│                _versions/<id8>/<timestamp>.<ext>                   ← flat, id-keyed
├── reddit/      r/<community>/feed.json, icon.png
│                r/<community>/posts/<date>_<slug>_<id>/post.{json,md}, comments.{json,md}, media/
│                u/<you>/  your own posts and comments
├── telegram/    <slug>_<kind>-<peerId>/chat.json, messages/<n>.json, messages.md, media/
├── claude/      <created>_<slug>_<convId8>/conversation.{json,md} + artifacts/
└── _status.json, README.txt

Copy it to an external drive, sync it with anything, drop it on a new machine. Then in Jestr, Settings → Import pulls it back in: after a reinstall, a new Chrome profile, or a machine you have never used before. Import only fills gaps. A file already present is never overwritten, the folder is never written to, and re-running it continues where a previous pass stopped.

NOTE  ·  Two tiers, one direction. Backups are written to OPFS, the extension's own storage, which needs no permission and exists from install. The folder you pick is a mirror: copied to, never read from during normal operation. That makes the folder the durable copy, since OPFS disappears with the extension or the profile.

Restore is equally boring by design. Drag the .docx files back into Drive, read the Markdown in any editor, feed the raw conversation.json into whatever tooling you like.


Encrypted

Sealed at rest with a key only your passphrase unwraps.

File contents are sealed at rest, in OPFS and in your mirror folder, with a key only your passphrase can unwrap.

Layer What it does
Passphrase → key PBKDF2-SHA256, 2,000,000 iterations, unwraps the vault key
Per-file envelope Fresh ephemeral ECDH P-256 keypair → HKDF → AES-256-GCM
Recovery code 128 bits of real entropy, issued at setup, bound to its own AAD so it cannot be replayed
Filenames Item ids, not titles, so a directory listing is not a table of contents

The lock re-arms on every browser restart. The viewer and the panel's contents sit behind it.

Backups keep running while locked. Sealing needs only the vault's public key, which is stored in the clear, so a restart never interrupts collection. Only reading needs you. One exception: Telegram message collection merges against the stored copy, so it waits for an unlock.

Stated plainly, what is not encrypted: chrome.storage.local, the per-item index of titles, URLs and timestamps, and the SharePoint catalogue in IndexedDB. Neither is in the mirror folder, so a stolen folder yields nothing. A copied Chrome profile shows what you have, not what is in it.

WARNING  ·  There is no reset. Lose the passphrase and the recovery code and the backups cannot be read. That is the point of the design, not a gap in it.

Why Auto redirect sits above the lock

The Auto redirect switch is the one control held above the passphrase gate. Not so arming can skip the passphrase (it cannot), but because of the state where the two collide.

The lock re-arms on every browser restart; Auto redirect deliberately survives one. Restart mid-outage and you are locked and armed. Since disarming needs no passphrase, the switch is the only working control in exactly that state. Hide it, and someone whose browser is intercepting every navigation opens the panel to find nothing but a password box.

When you do arm it from a locked panel, the passphrase you type unlocks the rest of the panel too: one entry, both jobs.


Install it in Chrome

Requirements: Chrome (or any Chromium browser) with Manifest V3 support, and Node 18+ to build.

git clone <this-repo> && cd jester-extension
npm install
npm run build      # typechecks, then outputs dist/

Then:

  1. Open chrome://extensions
  2. Toggle Developer mode on (top right)
  3. Click Load unpacked and select the dist/ folder
  4. The setup page opens automatically → Choose backup folder… (if Chrome offers it, pick "Allow on every visit")
  5. Set your passphrase and save the recovery code somewhere safe
  6. Pin the Jestr icon to your toolbar. Clicking it opens the side panel

That is the whole setup. The first Claude sync starts immediately. Drive documents back up as you open them. Reddit and SharePoint fill in on their own schedule. Telegram syncs whenever you have a signed-in web.telegram.org/k/ tab open.

Verify it's working

# open any Google Doc, then check:
your-folder/drive/<Title> [id].docx        # appears within seconds
your-folder/drive/_manifest.json           # lists it
your-folder/claude/                        # one folder per conversation
your-folder/_status.json                   # last-success timestamps per app

The toolbar badge shows ! only when something actually needs you: signed out, folder access lost. Silence means it is working.

For logs: chrome://extensions → Jestr → service worker console (triggers) or offscreen.html console (fetches and writes).


How it works

flowchart TD
    T1["Tab navigation"] --> SW["Service worker<br/>detects and dispatches, then dies"]
    T2["Scheduled alarm"] --> SW
    T3["Sync now"] --> SW

    SW -->|"hands the job over"| OD["Offscreen document<br/>fetches as you, validates,<br/>hashes, seals, writes"]

    OD --> OPFS[("OPFS<br/>working copy")]
    OPFS -->|"mirrored out"| DIR[("Your folder<br/>the durable copy")]

    OPFS --> VIEW["Viewer / Auto redirect<br/>decrypted in memory,<br/>rendered with no network"]

    DIR -.->|"Import, after a reinstall<br/>or on a new machine"| OPFS
Loading

The split is forced by Manifest V3. A service worker is killed after roughly 30 seconds of idle, so it can never be the thing that runs a long sync loop; it only notices that work is due and hands it over. The offscreen document is a hidden page with a normal page lifetime, which is what makes cookie-authenticated fetches and File System Access writes possible in the first place.

The session hijack

Every backup tool you have used before this one asked for an account: an OAuth consent screen, an API key, a token pasted into a settings box. That is a second credential to issue, store, scope, refresh and eventually leak.

Jestr never asks, because the credential already exists. You are signed into these apps right now. Your browser is holding a live, authenticated session for each of them, and an extension with host permission for a site can issue requests that carry it:

fetch('https://…/export?format=docx', { credentials: 'include' })

Chrome attaches the site's own cookies to that call. From the server's point of view it is you, in your browser, asking for a document you already have open in another tab. Jestr calls the exact same private endpoints the web app calls when you click things, so it gets exactly what the app would have shown you: no more, and no less.

That is the whole trick, and it comes in three grades of difficulty.

flowchart LR
    S(["Your live browser session"]) --> G1["1 - Ride the cookie<br/>credentials: include"]
    S --> G2["2 - Beat the anti-forgery check<br/>SAPISIDHASH + Origin rewrite"]
    S --> G3["3 - Drive the page itself<br/>injected shim, no cookie exists"]

    G1 --> API["The app's own private endpoints"]
    G2 --> API
    G3 --> API
    API --> OUT(["Exactly the data you can already see"])
Loading

1. Riding the cookie. Most apps need nothing else. The offscreen document fetches a document export, a JSON listing, a media file, and the session cookie does all the work.

2. Satisfying the anti-forgery checks. Some private frontends want more than a cookie, precisely to stop requests that did not come from their own page. Google's data frontend is the strict one: it wants a SAPISIDHASH derived from the SAPISID cookie, and an Origin header it recognises, which an extension origin is not. So Jestr reads that one cookie to compute the hash, and rewrites the Origin header on its own outbound calls with a declarativeNetRequest rule. The rule is deliberately scoped so it cannot touch anything but Jestr's requests: the extension holds no host permission for drive.google.com at all, so a real Drive tab's traffic is out of its reach by construction.

3. Driving the page itself. Sometimes there is no cookie to ride. Telegram Web keeps its session as an auth key inside the page and talks MTProto over a WebSocket, so there is no URL to call and no credential to attach. For that case Jestr injects a small shim into a tab you already have signed in and asks the page's own authenticated client to do the work. It asks for no phone number, no login code, and it injects nothing until a backup is actually running. The cost is the one constraint that applies nowhere else: that tab has to be open.

What this deliberately cannot do. The boundaries matter more than the trick:

Never sees your password It rides a session; it does not create one
Stores no credential No token, no key, no cookie copy is written anywhere
Cannot outlive your session Sign out and collection stops, with a ! on the badge
Reaches nothing you cannot The server applies your own permissions to every call
Talks to nobody else Traffic goes to the app's own origin. No Jestr server exists

What happens to a fetched byte

The same pipeline runs for everything, whichever grade it arrived by:

flowchart TD
    B["Fetched bytes"] --> V{"Is it what it claims to be?<br/>zip magic, not a login page"}
    V -->|"no"| REJ["Rejected.<br/>The good copy is left alone"]
    V -->|"yes"| H{"sha256 differs from<br/>what is already stored?"}
    H -->|"no"| SKIP["Nothing written"]
    H -->|"yes"| VER["Previous copy moved<br/>into .versions/, last 5 kept"]
    VER --> SEAL["Sealed: ECDH P-256 into HKDF<br/>into AES-256-GCM, public key only"]
    SEAL --> W["Written atomically to OPFS"]
    W --> M["Copied out to your folder"]
Loading

Each gate earns its place. Validation exists because a stale session famously answers with an HTML login page under HTTP 200, and a plausible-looking failure is the one thing that must never land on top of a good backup. Hash-gating is what keeps a sync that runs all day from churning your disk. Sealing needs only the vault's public key, which is why collection carries on while the vault is locked.

Two rules govern pace throughout. The shared budget is a hard ceiling, so at the limit collection stops and says so rather than deleting anything. And you always outrank the backup: work runs in larger slices while the machine is idle or locked, backs off while you are using it, and where an app enforces a request budget of its own, a reserve is left permanently untouched so Jestr can never be the reason your own tabs start failing.

Repair, the only action that deletes from your folder

Two guards sit around it:

  • It refuses to prune a subtree when the orphans outnumber the files still in OPFS. Pruning is for leftovers such as a renamed document's old filename, or versions whose tombstones were dropped at the cap, which are a minority of a healthy folder by nature. Outnumbering them means OPFS is what is wrong, not the folder. Declined subtrees are reported, not silently skipped.
  • The panel blocks Repair outright while an import is outstanding.

The trade is deliberate: history legitimately pruned to nothing leaves its folder copies in place. Disk hygiene forgone so that a delete never runs on bad evidence.


Development

npm run dev         # vite dev server
npm run build       # tsc --noEmit && vite build → dist/
npm run typecheck   # types only
npm test            # vitest
npm run icons       # regenerate extension icons from assets/
src/
├── apps/         one folder per app: drive, sharepoint, reddit, telegram, claude
│   ├── index.ts      the registry (identity, settings, summaries)
│   ├── collectors.ts what actually fetches
│   ├── triggers.ts   what starts a job
│   └── views.ts      how the offline viewer renders it
├── background/   MV3 service worker, triggers and dispatch only
├── offscreen/    the long-lived worker: fetches and writes
├── panel/        the side panel UI
├── viewer/       the offline reader
└── lib/crypto/   vault, envelopes, keys, sealed maps

Adding an app means adding one folder under src/apps/ and registering it in src/apps/index.ts.

About

Local Backup and offline mode for the most used apps

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages