Le formulaire New entry n'avait pas le champ et AddEntry ecrivait
totp_secret = '' en dur : il fallait enregistrer l'entree puis la rouvrir
pour poser un second facteur, alors que l'editeur portait deja le champ.
Meme intitule que l'editeur ("2FA secret"), masque pour une note comme
site et identifiant, et achemine par &o= — t, s, u, p, k, d etaient
pris.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The desktop keeps twenty previous versions per entry and never sent any. It
now sends three.
Three, not twenty, because the trade is not symmetrical. On the desktop they
sit encrypted column by column and never move. In the snapshot everything
travels in the clear inside the container, and comes down into a phone cache
sealed only by the device key - not by the master password. An old password
stays sensitive: it is often still valid somewhere else. The real use of the
list is short - "I changed it recently, what was it?" - and three cover it.
The other seventeen would be copies with no need facing them.
The phone reads them ON DEMAND from the raw snapshot, never into TVault:
loading them with the vault would put every entry's old passwords in the clear
in memory from the moment it opens, for something one looks at now and then.
Copying goes by RANK, not by value. The page displays the text but never sends
it back - that is the bridge's rule everywhere else, and an old password has
no reason to be the exception just because it happens to be on screen.
They leave with the plaintext at lock, on both sides.
Fetching https://<site>/favicon.ico directly would resolve a DNS name - and
open a connection - to EVERY domain kept in the vault. The list of sites where
someone holds an account is precisely what this vault protects; handing it to
the network for a sixteen-pixel drawing would be a poor trade. The relay sees
only that someone is interested in one domain, which is why the call is asked
for entry by entry and never happens on its own.
Two tries: the full host, then the second-level domain - chat.qwen.ai has no
icon at the relay, qwen.ai does. The type comes from the first bytes, never
from the extension: the relay answers PNG under a .ico address.
A dedicated patch rather than PatchEntryField, because icon_custom is a NUMBER
in the snapshot and writing it as a string would give "0" - which the desktop
reads with GetValue<Boolean> and would choke on. It is set to zero: the icon
comes from an automatic lookup, not from a choice, and that flag is what stops
the desktop from ever replacing a chosen icon during a refresh.
Base64 without line breaks. Delphi's encoder wraps at 76 characters by
default, a wrapped data: URI does not display, and that trap has already been
paid once on the containers.
Two defects found by re-reading before adding more, along with a test plan.
MergeSnapshots merged entries and tombstones but not folders. A folder created
on the phone - offline, or just before another device published - vanished
from the list while the entries kept its name: filed under a folder nothing
displays, unfindable without knowing it. That is exactly what EnsureFolder was
written against, come back through another door.
The pending dot sat on the padlock, which only exists in the list. After
locking one lands on the home screen - precisely where one decides to sync -
and it said nothing there. It is on Sync as well now.
TESTS.md records what the reading checked and what it left alone, with reasons
for the two defects deliberately not fixed, and a scenario that says what each
step must PRODUCE rather than only what to do.
Two questions, and the code was only the second one.
WHERE THE FILE LANDS. The private directory is useless - no file manager
opens it and it leaves with the uninstall - and direct external storage has
been closed since Android 10. Of the three remaining doors, MediaStore ->
Downloads is the only one needing neither a permission nor an extra screen:
since API 29 an app may write what it inserted itself. The destination picker
would need an activity result, which FMX makes painful; sharing would need a
FileProvider in the manifest. Below API 29 it would take
WRITE_EXTERNAL_STORAGE - one of the ten permissions just removed - so it
refuses and says so.
WHERE THE CONTENT IS. TVault drops it on purpose: a single attachment took
the cache from 13 to 174 Ko. It is re-read from the raw snapshot, which has
been in memory ever since saving stopped fetching before publishing. The same
data now serves two needs that had nothing to do with each other.
Decoding and writing happen off the main thread, under the waiting veil: a
few megabytes would freeze the screen, and the veil would not even paint.
What leaves the application leaves its protection: the file lands in the
clear in Downloads. The confirmation names the place rather than saying
"saved".
Release configuration compiles too - checked, since that is what section 5
caught out.
The floating menu was wrong for a list. It closed on the next tap, so opening
an entry afterwards took two gestures and felt like a button had got stuck.
Android's contextual bar instead: a long press selects, the top bar takes the
place of search, says how many rows are held, and offers duplicate and
delete. It goes away when nothing is held. One does not search while choosing
what to delete, and two rows of controls would fight over the same thumb.
Selection is multiple, because the cost is in the bar, not in the work: the
patch closure loops, and ten deletions become ONE publish. The container goes
up whole on every write - deleting ten entries one by one would pay that trip
ten times, and give nine chances to fail halfway.
A tap during selection picks rather than opens, which is also the way out:
unpicking the last row closes the bar. The hardware back cancels the
selection before locking the vault - locking because someone wanted to cancel
a choice is the opposite of what the gesture means.
Both menus stay, with distinct jobs: the bar acts on rows chosen in the list,
the detail's three dots act on the entry in front of you. Duplicate copies
everything, attachments included, as the desktop does - a copy that silently
lost the attached document would be a surprise discovered too late.
Copying a password or a username opened the entry instead - about half the
time, which is the worst kind of failure to diagnose. Since those buttons
carry an icon, the tap target is the <svg> or one of its strokes, so reading
the attribute off the target returned null and the handler fell through to
"open". A finger landing sometimes on the glyph and sometimes on the margin
made it look random. Both handlers now resolve to the button first.
The new-entry form gets a folder, like the editor: an entry filed from the
start saves having to move it.
And a folder can now be created, not only chosen. Filing an entry under a
name is not enough - it is `folders` that fills the desktop's column, and a
name missing from it leaves the entry in a folder nothing displays, looking
lost. The two now go together, in one place, for both editing and creation.
'All' is never registered: it is the default, not a folder.
Until now the two situations never met: a local vault had no repository, a
pulled vault had no local container. The day both exist, the rule "the local
container is the authority" means saves never reach the repository - and the
two diverge in silence, which is the worst outcome available.
Sync now merges them once and then deletes the local container: the
repository becomes the authority and the device behaves like any other. That
makes divergence impossible rather than unlikely. Order matters - publish
first, delete after; the reverse would lose the whole vault if the publish
failed.
The remote is the base, never the local. It carries what the phone cannot
rebuild: attachment bodies, the avatar, folder colours, and whatever the
desktop adds next. Starting from the local would amputate all of it, and the
desktop would adopt the amputated version on its next pull.
Arbitration is the desktop's, term for term: last write wins on updated_at, a
tombstone beats an older entry but not one edited after it - that one was
revived on purpose. Timestamps are ISO 8601 UTC on both sides, milliseconds
included, so lexicographic order IS chronological order.
MergeSelfCheck asserts the four rules at startup in debug. The merge never
runs against a real vault before it has run there.
Both, or nothing: the row leaves the snapshot AND a tombstone takes its
place. Removing the row alone does not delete - the desktop still holds its
own copy, and the next pull would bring it back as an entry we never had.
The time matters too: the desktop's arbitration keeps an entry edited AFTER
the deletion rather than killing it a second time.
No trash on the phone, deliberately. The desktop's is a `deleted` flag that
does not travel in sync, so a deletion stopping there would propagate
nowhere. Hence a button at the very bottom, in two taps, that disarms itself
after four seconds - a button left armed gets touched by accident much
later, and this is the one action with no way back.
The confirmation is in the page because the engine cannot offer one: a
JavaScript prompt needs a WebChromeClient FMX does not provide.
Not exercised on the device: it would have destroyed a real entry.
A vault you cannot add to is not a vault, so an account created here was
useless without this. A plus button next to the padlock, a form, and a
kind - login or note, which the vault distinguishes and whose body is the
password field either way.
One save path now, two transports, and the rule that decides between them
is: if a local container exists it is the authority. A vault born here has
no repository; a vault pulled from one has no local container. The case
where both exist - a local vault that later gains a repository - needs a
merge, and that is section 7 step 4.
The patch arrives as a function rather than an already-patched snapshot:
the remote path must modify WHAT IT HAS JUST PULLED, or it would overwrite
whatever another device published in between. That constraint is why the
two transports could not simply share a finished payload.
Two traps closed while passing: the form left the typed password sitting
in the document after the save, and a refusal left its Save button dead
with the text behind it.
Verified on the device that the button and the form appear and read
correctly. Saving was not exercised - the vault on this phone syncs to a
real repository, and a test entry would have been published to it with no
way to delete it from here yet.
Until now the phone could only read a vault that came from elsewhere:
without a URL, a token and a sync password it showed nothing. It now
signs in like the desktop does - username and master password - and can
create its own vault, with no network at all.
The local container carries exactly what the repository would carry: same
format, same encryption under the password. Two reasons, not one. It is
the desktop's model, where the master password protects the vault at rest
rather than the hardware alone; and the format staying the repository's
means sync keeps working with nothing to convert. No Keystore on that
file - it is already encrypted under the password, and binding it to the
device would make it unrecoverable after a reinstall. vault.bin keeps
that role: it is the fast-open cache, and only it needs sealing.
The password opens the container - the decryption itself is the check, no
secret is ever compared - and the username, read from inside once open,
says whose vault it is.
The home screen now knows which of three situations it is in: nothing
here yet, a container without a cache, or a cache ready to open.
Verified on the device: with the cache set aside, the screen offers
username, master password and Create vault, alongside the download path.
Creating an account could not be exercised to the end - the device locked
its screen mid-run - and the vault that was set aside is back in place,
same size, still sealed under its code.
Section 4 step 4. TVault reads a subset of the fields, so re-serialising the
vault from what it understood would drop attachment contents, created_at, the
account avatar, tombstones, and whatever the desktop adds next - and the
desktop would adopt that amputated version on its next pull, the merge being
last-write-wins on updated_at. Silent, and final.
So nothing is rebuilt: the received JSON is edited in place and the rest is
handed back untouched. What the phone does not understand travels intact by
construction rather than by vigilance.
A corollary worth naming, because it removes the step the plan feared most:
since the patch applies to the snapshot just pulled, there is no merge to
write. Anything untouched is already the server newest version. The only
possible conflict is another device publishing between our read and our write,
which Gitea refuses on the sha.
The self-check tests what is PRESERVED, not what changes: a field unknown to
TVault, an attachment content_b64, the untouched sibling entry, the
tombstones. It reports "patch keeps what it does not understand" on the device
at every debug launch.
Also confirmed by this log: the write token now survives a restart, so the 304
settings fix holds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>