Skip to content

feat(store): a readable copy of a protected store, on request - #102

Merged
florianmousseau merged 1 commit into
mainfrom
feat/plaintext-export
Jul 31, 2026
Merged

feat(store): a readable copy of a protected store, on request#102
florianmousseau merged 1 commit into
mainfrom
feat/plaintext-export

Conversation

@florianmousseau

Copy link
Copy Markdown
Contributor

First half of the "transparent mode" design that was parked as a draft: make
the plaintext path real and reachable, before deciding anything about flat
files.

The gap

Ask "can I read my own data?" of a protected store and the answer today is
unprotect(). That rewrites the destination, leaves the real backup
unencrypted, and stays that way until the user remembers to reverse it. An
ordinary question with a dangerous answer.

What this adds

exportBackup({ plaintext: true }) writes the same ZIP with selfstore.json
in the clear - openable by any tool, diffable by git, readable by the Python
reference reader.

It changes nothing else. The store stays protected, the password is untouched,
and what sits on the destination stays encrypted. The default export still
encrypts; the flag is the only route to cleartext.

Two refusals, both deliberate

While locked: there is nothing readable to write, so it throws
PASSWORD_REQUIRED rather than produce something wrong.

On a requireEncryption store: refused outright. That flag is the host
saying this store never produces cleartext. Adding a way around it would make
the flag a lie, and the guard that already backstopped exportBlob() now asks
the same question of the explicit request.

Measured while designing this

The format was already half-way there and the draft's premise held: a
password-less backup is generation 1, encryption: "none", and the app's
documents travel clean - the merge clocks ride a separate sidecar, so nothing
of the bookkeeping is mixed into the data.

What the draft had NOT measured, and what matters for the flat-file half:
selfstore.json is a single line of minified JSON with every collection in it.
For git that is the worst shape there is - each save rewrites the whole file
and git diff reports one changed line showing nothing. So "git-diffable" is
not free: it needs pretty-printing with sorted keys and a file per collection,
plus something done about createdAt, which changes on every write and would
make a commit out of an unchanged record. That is the real work in the second
half, and it is now written down.

Verification

Gate green, exit 0. 770 tests over 64 files, 4 new: a readable copy comes out
of a protected store while the destination stays encrypted, the default still
encrypts, locked is refused, and requireEncryption is refused.

Entry under [Unreleased]; no release.

Getting your own data out in a form you can read had exactly one route:
unprotect(), which rewrites the destination and leaves the real backup
unencrypted until you remember to undo it. That is a dangerous price for
an ordinary question.

exportBackup({ plaintext: true }) writes the same ZIP with
selfstore.json in the clear. The store, its password and its destination
are untouched: what sits on the destination stays encrypted, and the
default export still encrypts.

Two refusals on purpose. Locked, because there is nothing readable to
write. And a requireEncryption store, outright - that flag says this
store never produces cleartext, and an escape hatch would make it a lie.
@sonarqubecloud

Copy link
Copy Markdown

@florianmousseau
florianmousseau merged commit eb23613 into main Jul 31, 2026
7 checks passed
@florianmousseau
florianmousseau deleted the feat/plaintext-export branch July 31, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant