Repository navigation
Conversation
Vault file format and crypto: - subclave-vault.json: AES-256-GCM (ring) over the JSON payload, header (format, v, kdf) bound as associated data; Argon2id 64 MiB / t=3 / p=4 with caps refused before any derivation runs - atomic 0600 writes for the vault file and its .bak on Unix; load falls back to the .bak only when the primary is structurally unreadable, and a v above the current one refuses without touching the .bak - wrong password, tamper and truncation share one opaque error sentence Vault state and commands (22, all async with spawn_blocking): - create, unlock, lock, touch, change master, retry save, status - entries: get, reveal, upsert, move, trash, restore, delete, restore-version; groups: upsert, delete (reserved ids refused) - every save parks a seal on failure and clears it on success, so the 10 s retry can never land an older ciphertext over newer data - an expired idle deadline locks (and wipes) before any command runs Idle lock on a boot-time clock (CLOCK_BOOTTIME / CLOCK_MONOTONIC / GetTickCount64), one 1 s tick thread for clipboard clear, deadline and retry; clipboard copies concealed and compare-then-cleared. TOTP (RFC 6238 vectors, three algorithms, 6 and 8 digits), generator (rejection sampling, injectable RNG, chi-square tested), zxcvbn strength. Tests: 56 passing, including the canary scan over the vault directory, header AAD binding, .bak fallback and the full create -> mutate -> save -> reload -> lock -> unlock flow. All 22 commands are on the UNINVOKED ledger until the UI lands.
* feat: M2 vault UI, settings and tray
First run, unlock and the three panes:
- FirstRunScreen, CreateVaultScreen (strength meter, no-recovery
acknowledgement) and UnlockScreen (Caps Lock warning, one sentence for
a wrong password and a corrupt file, so the two stay indistinguishable)
- VaultWorkspace: group tree, tag strip, entry list and entry detail,
with the entry editor on top; the panel group moved out of App.tsx and
the header gained the lock button and the search slot
- entries scoped by All, Favourites, tags and a Rust-side search that
never matches a password
Editor and secrets:
- draft.ts keeps "unchanged" apart from "set" on the wire, so an
untouched password, TOTP URI or hidden custom value is never rewritten
- a password field left empty keeps the stored password instead of
saving an empty one
- generator options persist on Use; every reveal goes through one
SecretField that clears on blur, after 30 s and when the entry changes
Preferences, lock and quit:
- Rust reads autoLockMinutes, clipboardClearSeconds, lockOnMinimize and
closeToTray from subclave-settings.json at use time, with per-key
clamps and 0 meaning never
- Settings > Security holds all six rows, including the change-master
dialog and the autostart row moved out of General
- status bar countdown from the 5 s status poll, lock on minimize,
close to tray, and a tray with Open, Lock and Quit
- a quit with a parked write asks first: close and exit are vetoed until
the user decides, and the parked seal now survives a retry in flight
(it is copied, not taken, and has_pending stays honest)
Vault core:
- vault_status reports saveBlocked, backupAt and locksInMs
- vault_restore_snapshot moves a broken primary to .corrupt and rewrites
- vault_search and totp_preview; an unlock retries the .bak when the
primary parses but fails to open
- every mutating command refuses before touching the payload while a
restore is pending, so an edit that cannot be saved is never lost
silently, and the save lock spans seal plus write
Tests: 72 passing (16 new) and the Xvfb smoke run. The UNINVOKED ledger
is empty; two new verify scripts cover the draft wire semantics and the
group forest.
* fix(ci): stop the bundle matrix failing on a case collision and a missing .pc
Two independent failures, both invisible on a Linux dev machine:
- `src/modules/groups/GroupTree.tsx` and `src/modules/groups/groupTree.ts`
share a stem, so a case-insensitive checkout holds one of them and
`import { Chip } from "./GroupTree"` resolves to the helper module instead
of the component: all four bundle jobs failed with TS2305/TS1261. Renamed
the helper to `groupTreeModel.ts` and added
`scripts/case-collision-verify.ts` to `pnpm verify`, so the next one fails
on Linux in seconds instead of in the bundle job ten minutes in.
- `tauri build` resolves the tray library with
`pkg-config ayatana-appindicator3-0.1` and panics with "Can't detect any
appindicator library" when the .pc file is absent, which killed the Linux
bundle after the release build had already succeeded. The runtime library
is not enough - the -dev package carries the .pc file. Added
libayatana-appindicator3-dev to LINUX_DEPS in ci.yml and release.yml.
Cross-device sync over S3-compatible storage or WebDAV, with the merge, the wire format, the keyfile and the two round trips in Rust, and the triggers, the Settings form and the first-run join screen in the webview. The merge is `merge_entries` in `src-tauri/src/modules/vault/merge_history.rs`, pinned by commutativity, associativity and idempotency property tests over 20,000 seeded pairs and triples. A pull holds no vault lock across the network: it returns a report and the apply path re-merges against the live payload, so an edit made while the request was in flight survives. Object keys are HMACs under a root key that never leaves the vault, sealed in `subclave-sync.json`'s device state. The passphrase is typed once, at create or join, and never stored; the root key is what re-opens a session. `put_if_absent` narrows the empty-prefix race on S3 when the endpoint is known to honour a conditional write, which Settings > Sync exposes as a switch. Cargo.lock gains direct reqwest, rustls, tokio and parking_lot entries.
#5) * refactor: split oversized modules into cohesive files M0-M3 files had grown past the point where a reader can hold one in their head: sync/engine.rs was 3158 lines, vault/mod.rs 2566, webdav.rs 2121, s3.rs 1682, lib.rs 1060. Split each along its existing seams. Behavior is unchanged: exported symbols, Tauri command names and every generate_handler! entry keep their paths, all Rust tests moved with the code they cover, and the s3/webdav HTTP shell is now one shared providers/http.rs instead of two copies. Also split the frontend files over 300 lines (customTheme, ThemeSection, SyncSection, GroupTree, settings store, scheduler, JoinSyncScreen, EntryEditorDialog, ShortcutsSection, recoveredStore) and the 1554-line citation checker. Verified: cargo fmt/clippy clean, 244 Rust tests pass, tsc, typecheck:scripts, lint:imports, prettier, 16/16 verify scripts, vite build, and a headless render of the built app for the refactored tabs. * chore: remove dead code found by the ponytail audit Deletions only, plus the small helpers a few of them collapsed into. Also drops 33 files of local harness state under .omc/ that a tool left in the tree, and ignores the directory. CSS and theme: drop the scroll-fade/shimmer utility layer and accordion keyframes (540 of 621 lines, no class in src/ emits them), the Tervia-era tokens nothing reads (tab-*, most ansi-*, icon-*, diff-removed, chart-*, --radius-*), the duplicated ScrollArea support CSS, and the 21 colour tokens that only the theme editor wrote. ThemeColors drops 56 -> 35 keys; every preset, COLOR_FIELDS, COLOR_VAR_MAP and the fallback palettes follow, so the theme editor no longer offers dead rows. The fast-path fallback now comes from themePresets/base, whose light mutedForeground differs by one shade, so a legacy shadow missing that key paints a slightly different grey on the first frame. Frontend: six unused shadcn primitives and twelve unused exports inside the ones that stay; dead lib exports and three one-line barrels; the dead shortcut parser and readOnly flag; About's private copy of the updater state machine (it renders the shared UpdaterDialog now); ThemeProvider's second preferences hydration; SettingsApp's second error boundary; dead props across seven components; duplicated shadow, colour and boot helpers. Rust: the single-impl LockExt trait, three copies of the epoch helper, the duplicated bulk-entry skeleton, the Caps struct, nine copies of the spawn_blocking tail, the duplicated webdav classify arms, plus the shell's no-op Windows style re-assert, the empty ledger algebra and eight capability grants no caller uses. The hand civil-calendar, hand XML and hand hex folds are now chrono, quick-xml and hex (all three already in Cargo.lock; no new transitive crate). Behaviour called out because it is not a deletion: - Linux: configure_linux_rendering no longer probes the compositor and GPU. WEBKIT_DISABLE_DMABUF_RENDERER=1 is set whenever the user has not set it, so every Linux session takes WebKitGTK's fallback path instead of the DMA-BUF one. Setting the variable to 0 still opts back in. - The sync providers' XML reader is quick-xml with allow_dangling_amp, allow_unmatched_ends and check_end_names off, matching the hand scanners it replaced. Scripts and deps: eight dead exports in scripts/lib/ast.ts, the citation checker's prose-shape rules (the resolvability and file:line rules stay), the unused react-doctor dependency, the vite manualChunks split, and the Android/iOS icon generation for a desktop-only app. Not in this commit: the per-target lock map in modules/fs/atomic.rs is untouched. It is the fix for a shipped bug (two concurrent writes to one target share a staging temp, and the loser of the rename race reports os error 2), with the trade against per-write temp names documented there, so it is not dead code. Net -2544 lines. Verified: cargo fmt/clippy clean, tests pass, tsc, typecheck:scripts, lint:imports, prettier, 16/16 verify scripts, vite build, and a byte-identical headless render of the main window and the General, Theme, Sync, Shortcuts and Security tabs. * fix: address the review of the refactor and cut Four findings from an adversarial review of the two commits before this one. Each was reproduced before it was fixed. The min-size clamp was untestable and its replacement test asserted its own inline copy of the comparison, so flipping `<` to `<=` or dropping a max would have kept the suite green. The clamp is now a real function, clamp_to_floor, that enforce_configured_min_size calls and the table test drives. The XML rewrite made the sync providers stricter than the hand scanners they replaced: quick-xml's default reader rejects a dangling `&`, an unmatched end tag or a stray end tag, where scanning text yielded rows, so one bad byte in a listing failed the whole pull. The reader now sets allow_dangling_amp, allow_unmatched_ends and check_end_names off, and three tests per provider pin that a body of each class still yields its rows. The error taxonomy is unchanged. parse_http_date accepted a lowercase month name because chrono's RFC-2822 parser is case-insensitive, where the old literal match required the capitalized form; the month token is now gated to the exact three-letter spelling, with a test for the refused form. About rendered useUpdater(), which schedules an unattended check 8 s after mount and every 6 h, so opening Settings cost a GitHub round trip the user did not ask for. The hook takes an opt-in autoCheck flag; About passes false and its button starts the check on click, the way it did before, while the status-bar pill keeps the schedule. Also: local_name, malformed, Element, tree and attach were byte-identical in the two provider files after the rewrite (~250 lines); they move to providers/xml_tree.rs, the same way this branch shared providers/http.rs. Both parse tripwires in theme-verify.ts compared a declaration count against a hand-written floor, which silently tolerated partial loss; each now compares two differently-scanned views of the same file, so there is no number to go stale. A stale comment in the citation resolver's SKIP_DIRS now matches the list. Verified: cargo fmt/clippy clean, 248 tests pass (241 plus the seven new parser and month-case tests), tsc, typecheck:scripts, lint:imports, prettier, 16/16 verify scripts, vite build. The hook change was proved with a harness that mounts the real hook and records timers: no timer and no check on About's mount, one 8 s timeout and one 6 h interval for the default caller. Not fixed here: the Linux DMA-BUF default (documented in the previous commit message, not smokeable on this machine, and it needs a GNOME or KDE Wayland session), and the Windows-only window behaviours, which have no CI.
Add the browser channel milestone M4a calls for: the `subclave-proxy` native messaging host, the app-side local socket server with peer checks, pairing and the HMAC handshake, per-OS native messaging manifests, and the MV3 extension with its popup, fill command and "Generate for this site". Rust: - Cargo workspace with the `subclave-proxy` crate, whose `frame.rs` codec is shared by the proxy's blocking stdio and the app's async socket server. - `modules/browser/`: protocol, auth (pinned cross-language pairing and HMAC vectors), URL matching over `psl`, the socket server and its connection state machine, pairing, the extension-facing actions, and the per-OS manifests including the AppImage proxy copy. - `BrowserClient` on `DeviceState` (serde default, cleared with the payload on lock), browser and generator preferences, and the six async commands. - The `build-sidecar` script, `bundle.externalBin`, and the sidecar guards in CI and `verify-bundles.sh`. Extension: - Three-entry Vite build, one manifest per target, and the service worker: connect on demand, 30s idle close, handshake recovery, content-script injection fallback, and the fill command. - `content/fill.ts` (visibility-gated native value setter) and the React popup. - Playwright specs covering the popup, the fill command, hidden fields, the injection fallback, the locked/absent/unpaired states, the pinned crypto vectors, and connection lifetime. App UI: - Settings > Browser, the pairing dialog, and the typed command wrappers. Verified: `cargo test --workspace` (303 passed), clippy and fmt clean, the proxy crate checked for the msvc target, `pnpm verify` (17/17), and the extension suite (30 passed).
The content script draws an icon in each login field and opens a picker of this host's logins in the top layer, inside a closed shadow root. A fill, generate or update pick runs only after six guards pass: a trusted event at least 500 ms after the picker was shown or moved, nothing else in the top layer, no fade or effect on the page or host, an untouched host, the pointer over the host, and IntersectionObserver v2 visibility. The service worker answers inline requests only from the top-frame content script, for the sender's own URL at scope "host" and via "inline". A picker fill stays in the picked field's form or root, a vanished field fills nothing, and every fill message names the page it was released for, so a fill after a navigation never lands in the new document. Rust: IPv4 hosts never domain-match, get-logins returns the page's registrable domain for the picker footer, and an inline get-credential accepts an entry when any of its URLs is a same-host match.
* fix(dev): let pnpm tauri:dev start the app beforeDevCommand had "wait": true, so tauri dev waited for vite to exit before it built the app, and the app never started. The plain string form still builds the sidecar before vite starts, and tauri dev waits for the dev server URL as usual. * feat: M5 import, backup, release docs and the autofill checklist - CSV import from KeePassXC, Bitwarden, Chrome and Firefox with a staged preview and an apply into an "Imported <date>" group, plus the delete of the imported file. - Encrypted .subclave-backup export and import; the restore merges each record through merge_into_payload, the path a sync pull uses. - Plaintext CSV export in Bitwarden's columns behind a confirmation. - Shared header AAD, private file write and Trash check. - ARCHITECTURE.md, KNOWN-LIMITS.md, the SECURITY.md threat model, the README Uninstall section, and extension/test/sites.md. * fix(extension): icons for late login forms, skip decoy password fields A page with no login field now keeps watching for 5 seconds after load instead of stopping at load, so single-page apps that render their form late (X, Discord, Stripe, Notion) get the inline icon. An input that is out of the tab order and inside aria-hidden is a decoy no user can reach, and detection and every fill path now skip it: Booking.com keeps one such password field in front of its email step, which left the email unfilled. * test(extension): automate the live-site autofill checklist pnpm --filter subclave-extension test:sites loads each site in the checklist with the E2E extension and its fake app, fills it through the inline picker, the popup and the fill command, and writes the run table to extension/test-results/sites-run.md. A page that shows no login field within 15 seconds is blocked and goes to a hand check. The run is headed, stays out of pnpm test and CI, and needs no accounts. sites.md now has three steps (the automated run, the hand check of blocked sites, two real sign-ins), a gate of 90% of tested sites, and corrected URLs for Amazon, Docker Hub, AWS and Adobe, with Mastodon in place of Nextcloud. The citation check skips test-results/, whose browser profiles hold lock links that dangle once the browser exits. * docs(release): date v0.1.0 and record the automated sites run CHANGELOG.md: the Unreleased heading becomes [0.1.0] - 02-10-2026. extension/test/sites.md: the v0.1.0 run, built from PR #8 at d38e1fb, with all 50 rows from test:sites (42 yes, 1 no, 7 blocked). The hand check of the blocked rows and the two real-app sign-ins are still pending. * fix(icons): give Subclave its own mark instead of Tervia's The scaffold renamed Tervia's mark and kept its drawing, so the desktop bundle, installer, tray, favicon, extension and README logo all showed Tervia's "T". subclave-mark.svg is now a gold one-stroke "S" on a navy tile, and every raster is rendered from it. gen-icons.mjs also writes public/icon.png and the four extension icons, which it skipped before, so one run regenerates all 23 tracked rasters. * fix(browser): silence the Windows and macOS dead_code warnings Only Linux detection reads Roots.home, and only Linux and macOS read Roots.config and call push_slot. The Windows bundle warned that home and config are never read and push_slot is never used; the macOS bundle warned about home. home and config now carry the same cfg_attr(allow(dead_code)) as data and local_data. push_slot is compiled only on Linux and macOS; the slots_unix stub for every other target never calls it. * fix(fs): create the parent directory before a store write Every store file resolves against appDataDir(), and only save_vault created that directory. On a first run, every store write before the first vault save failed with "os error 3" on Windows. fs_write_file now runs create_dir_all on the target's parent before the atomic write. The error string is unchanged, so the frontend's "(os error N)" match still holds. * fix(vault): center the locked-state screens <main> is a row flex container, and the first-run, create, join and unlock screens had no flex-1, so each shrank to its card and sat at the left edge. They now fill <main> and center the card. * fix(header): shorten the header row The header row in both windows goes from 44 px to 36 px (h-9), which still fits the 28 px toolbar buttons. The icon is now size-4 and the app name 13px. The search field in the header shrinks from 36 px to 28 px, so it keeps space above and below it in the shorter row. * fix(proxy): open the Windows pipe overlapped so pairing completes The proxy opened the app's pipe for synchronous I/O and read responses on a duplicate of that handle. Windows serializes every operation on a synchronous handle and its duplicates, so once the reader thread sat in its read, each later write waited behind it and never reached the app. The first request on a connection went through and every one after it hung. Pairing sends status and then associate, so it always stopped, and the extension's 30 s idle close reported "Subclave is not running". The pipe is now opened with FILE_FLAG_OVERLAPPED. Each read and write starts its own overlapped operation with its own event and waits for it, so Read and Write still block, but only their own thread. a_write_completes_while_a_read_is_pending covers it on Windows: a write made while another thread's read is pending has to finish, and the read then receives the reply. * fix(header): move the search field next to the import and export menu The search field sat right after the app name, with the drag spacer between it and the trailing buttons. The spacer now comes first, so the field sits on the right, just before the import and export menu. Its width cap and height are unchanged.
The first-run join screen started from DEFAULT_SYNC_CONFIG (cas: false) and rendered no switch for it, so a device that joined an S3 remote stored cas: false and ran on unconditional writes until someone opened Settings > Sync. Settings' Behaviour block moves into SyncBehaviour, shared by both forms: the "Endpoint honours conditional writes" switch with its off-note on S3, the "nothing here to turn on" note on WebDAV. The join screen renders it into the config it already sends to sync_join and stores. The default stays off: the app never probes the endpoint. Closes #3
After a trusted sign-in submit that follows a user gesture, the content script reads the form's username and password and the service worker keeps the pair per tab in chrome.storage.session, stamped with the submit's sender.url and a random id. The next page load in that tab, or the same page once the password has left its form, asks for the prompt: the new check-login action compares the pair with the submitted host's exact-host entries (new, changed, unchanged) and answers other-site when the asking page is not on the submit's site by the Domain rule. It never returns a password. The prompt is the inline picker in the viewport's top-right corner, never focused, with Add, Update <title> (or a list of the host's entries) and Cancel. Add and Update pass the six guards and send save-login with via: "inline" and the pair's id, so they save only the pair the prompt shows; a new submit closes a prompt that is showing. No prompt when the pair is already stored, the vault is locked or the app is not running. A pending pair is dropped once answered, when its tab closes, after 3 page loads, or on the first page load after 5 minutes. Card codes and one-time codes in password fields are not captured; a password field toggled to text before the submit still counts. Docs: SECURITY.md (web pages), KNOWN-LIMITS.md, ARCHITECTURE.md flow 5, SUBCLAVE.md extension rules, CHANGELOG.md. Closes #9.
* fix(settings): hide the spin buttons on the Security number fields "Auto-lock after" and "Clear the clipboard after" are type="number" inputs, and the webview drew its own spin buttons on them: unthemed, and crowding the w-20 field. NumberRow now sets appearance:textfield on the input and appearance:none on its -webkit-inner-spin-button and -webkit-outer-spin-button, the pseudo-elements WebKit and Chromium draw. No Tauri webview is Gecko, so there is no -moz- rule. The field stays type="number": min, max, the spinbutton role and ArrowUp/ArrowDown stepping are unchanged, and commit still clamps to 0..=max. Closes #10 * chore(release): drop the live-site checklist and date v0.1.0 extension/test/sites.md made every release tag wait on a 15-minute live-site run, a hand check of the sites it reported blocked, and two real-app sign-ins. Subclave has one user, so that is more process than it needs: an autofill bug gets fixed when it shows up. sites.md, sites.spec.ts (which read its Sites table), sites.config.ts and the test:sites script are deleted, along with the spec's testIgnore entries and the release step and CI carve-out in SUBCLAVE.md. The specs CI runs are unchanged. CHANGELOG.md: [0.1.0] is dated 03-10-2026. CONTRIBUTING.md: the CI list gains pnpm typecheck:extension, which the frontend job runs.
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Merges
devintomainfor v0.1.0, the first Subclave release.Why
mainstill holds only the Tervia import. Taggingv0.1.0onmainruns.github/workflows/release.yml, which builds the signed updates and a draftGitHub Release with notes from the
[0.1.0]section ofCHANGELOG.md.Included:
signing key
spin buttons hidden
How
Merge commit, not squash, so
dev's history lands onmainunchanged.mainis an ancestor of
dev, so there is nothing to resolve.Testing
CI passed on every merge into
devthrough feat(extension): save prompt for logins typed on a page #12. PR chore(release): prepare v0.1.0 #13, whose tree is thecurrent
devhead, passed all seven checks: frontend, rust, extension, andthe macOS arm64, macOS x64, Linux x64 and Windows x64 bundles.
node scripts/release-notes.mjs v0.1.0prints the 0.1.0 section and themacOS footer.
The version is 0.1.0 in
package.json,extension/package.json,src-tauri/tauri.conf.json,src-tauri/Cargo.toml,src-tauri/subclave-proxy/Cargo.tomland both extension manifests.TAURI_SIGNING_PRIVATE_KEYandTAURI_SIGNING_PRIVATE_KEY_PASSWORDare setfor the release workflow.
pnpm exec tsc --noEmitcleanManual smoke-test of the affected feature
(If you touched
src-tauri/)cargo checkclean(If UI) tested in
pnpm tauri:dev(isolated dev data dir)CI covers the two checked boxes. This PR adds no code of its own; each feature
PR records its own manual testing.
Screenshots / GIFs
None. Each feature PR carries its own.
Notes for reviewer
date moved first.